Spawn the player server using the given options
Pass start_link arguments to MIDISynth Simplify event creation
This commit is contained in:
parent
808370cf7f
commit
2b38e95436
6 changed files with 92 additions and 87 deletions
16
README.md
16
README.md
|
@ -21,7 +21,7 @@ brew install fluidsynth
|
||||||
Clone this repo or put the [Hex dependency](https://hex.pm/packages/midi_player) in your mix.exs:
|
Clone this repo or put the [Hex dependency](https://hex.pm/packages/midi_player) in your mix.exs:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
{:midi_player, "~> 0.1.0"}
|
{:midi_player, "~> 0.2.0"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
@ -33,13 +33,13 @@ First, let's create some events.
|
||||||
This plays a piano sound for the C note for 1 second:
|
This plays a piano sound for the C note for 1 second:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
iex> piano = MIDIPlayer.Event.Note.new(0, 60, 0, 1000, 127)
|
iex> piano = MIDIPlayer.Event.note(0, 60, 0, 1000, 127)
|
||||||
```
|
```
|
||||||
|
|
||||||
We can change the instrument to a violin after one second like so:
|
We can change the instrument to a violin after one second like so:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
iex> change = MIDIPlayer.Event.ChangeProgram.new(0, 1000, 41)
|
iex> change = MIDIPlayer.Event.change_program(0, 1000, 41)
|
||||||
```
|
```
|
||||||
|
|
||||||
(Note that it could be simpler to use another MIDI channel for another instrument.)
|
(Note that it could be simpler to use another MIDI channel for another instrument.)
|
||||||
|
@ -47,22 +47,22 @@ iex> change = MIDIPlayer.Event.ChangeProgram.new(0, 1000, 41)
|
||||||
Finally, play two notes on the violin at the same time:
|
Finally, play two notes on the violin at the same time:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
iex> violin1 = MIDIPlayer.Event.Note.new(0, 67, 1000, 3000, 127)
|
iex> violin1 = MIDIPlayer.Event.note(0, 67, 1000, 3000, 127)
|
||||||
iex> violin2 = MIDIPlayer.Event.Note.new(0, 64, 1000, 3000, 127)
|
iex> violin2 = MIDIPlayer.Event.note(0, 64, 1000, 3000, 127)
|
||||||
```
|
```
|
||||||
|
|
||||||
Now we are ready to play these events.
|
Now we are ready to play these events.
|
||||||
First start the player like so:
|
First start the player like so:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
iex> MIDIPlayer.start_link()
|
iex> {:ok, player} = MIDIPlayer.start_link([])
|
||||||
```
|
```
|
||||||
|
|
||||||
Then load the events, and play them!
|
Then load the events, and play them!
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
iex> MIDIPlayer.generate_schedule([piano, change, violin1, violin2], 3000)
|
iex> MIDIPlayer.generate_schedule(player, [piano, change, violin1, violin2], 3000)
|
||||||
iex> MIDIPlayer.play()
|
iex> MIDIPlayer.play(player)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
|
@ -14,10 +14,12 @@ defmodule MIDIPlayer do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Start the MIDI player.
|
Start the MIDI player.
|
||||||
|
|
||||||
|
Arguments are the same as `MIDISynth.start_link/2`.
|
||||||
"""
|
"""
|
||||||
@spec start_link() :: GenServer.on_start()
|
@spec start_link(keyword(), GenServer.options()) :: GenServer.on_start()
|
||||||
def start_link do
|
def start_link(args, opts \\ []) do
|
||||||
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
|
GenServer.start_link(__MODULE__, args, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -31,49 +33,51 @@ defmodule MIDIPlayer do
|
||||||
|
|
||||||
See `MIDIPlayer.Event` to create events.
|
See `MIDIPlayer.Event` to create events.
|
||||||
"""
|
"""
|
||||||
@spec generate_schedule([MIDIPlayer.Event.t()], non_neg_integer()) :: :ok
|
@spec generate_schedule(GenServer.server(), [MIDIPlayer.Event.t()], non_neg_integer()) :: :ok
|
||||||
def generate_schedule(events, duration) when duration > 0 do
|
def generate_schedule(player, events, duration) when duration > 0 do
|
||||||
GenServer.call(__MODULE__, {:generate_schedule, events, duration})
|
GenServer.call(player, {:generate_schedule, events, duration})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Play the current MIDI schedule from the start.
|
Play the current MIDI schedule from the start.
|
||||||
"""
|
"""
|
||||||
@spec play() :: :ok
|
@spec play(GenServer.server()) :: :ok
|
||||||
def play do
|
def play(player) do
|
||||||
GenServer.call(__MODULE__, :play)
|
GenServer.call(player, :play)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Set the player on repeat or not.
|
Set the player on repeat or not.
|
||||||
"""
|
"""
|
||||||
@spec set_repeat(boolean()) :: :ok
|
@spec set_repeat(GenServer.server(), boolean()) :: :ok
|
||||||
def set_repeat(repeat) when is_boolean(repeat) do
|
def set_repeat(player, repeat) when is_boolean(repeat) do
|
||||||
GenServer.call(__MODULE__, {:set_repeat, repeat})
|
GenServer.call(player, {:set_repeat, repeat})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Stop the player and cancel the pause.
|
Stop the player and cancel the pause.
|
||||||
"""
|
"""
|
||||||
@spec stop_playing() :: :ok
|
@spec stop_playing(GenServer.server()) :: :ok
|
||||||
def stop_playing do
|
def stop_playing(player) do
|
||||||
GenServer.call(__MODULE__, :stop_playing)
|
GenServer.call(player, :stop_playing)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Pause the player. See `MIDIPlayer.Player.resume/0` for resuming playback.
|
Pause the player.
|
||||||
|
|
||||||
|
See `MIDIPlayer.resume/1` for resuming playback.
|
||||||
"""
|
"""
|
||||||
@spec pause() :: :ok | {:error, :already_paused | :not_started}
|
@spec pause(GenServer.server()) :: :ok | {:error, :already_paused | :not_started}
|
||||||
def pause do
|
def pause(player) do
|
||||||
GenServer.call(__MODULE__, :pause)
|
GenServer.call(player, :pause)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Resume playback on the player after it has been paused.
|
Resume playback on the player after it has been paused.
|
||||||
"""
|
"""
|
||||||
@spec resume() :: :ok | {:error, :not_paused}
|
@spec resume(GenServer.server()) :: :ok | {:error, :not_paused}
|
||||||
def resume do
|
def resume(player) do
|
||||||
GenServer.call(__MODULE__, :resume)
|
GenServer.call(player, :resume)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -83,16 +87,16 @@ defmodule MIDIPlayer do
|
||||||
corresponding bitstream of MIDI commands to be played at that time.
|
corresponding bitstream of MIDI commands to be played at that time.
|
||||||
The list is guaranteed to be ascending in time.
|
The list is guaranteed to be ascending in time.
|
||||||
"""
|
"""
|
||||||
@spec get_schedule() :: schedule()
|
@spec get_schedule(GenServer.server()) :: schedule()
|
||||||
def get_schedule do
|
def get_schedule(player) do
|
||||||
GenServer.call(__MODULE__, :get_schedule)
|
GenServer.call(player, :get_schedule)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Server callbacks
|
# Server callbacks
|
||||||
|
|
||||||
@impl GenServer
|
@impl GenServer
|
||||||
def init(_arg) do
|
def init(args) do
|
||||||
{:ok, synth} = MIDISynth.start_link([])
|
{:ok, synth} = MIDISynth.start_link(args)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -11,24 +11,6 @@ defmodule MIDIPlayer.Event do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
defstruct channel: 0, tone: 0, start_time: 0, end_time: 0, velocity: 0
|
defstruct channel: 0, tone: 0, start_time: 0, end_time: 0, velocity: 0
|
||||||
|
|
||||||
@spec new(
|
|
||||||
MIDISynth.Command.channel(),
|
|
||||||
non_neg_integer(),
|
|
||||||
non_neg_integer(),
|
|
||||||
non_neg_integer(),
|
|
||||||
MIDISynth.Command.velocity()
|
|
||||||
) :: %Note{}
|
|
||||||
def new(channel, tone, start_time, end_time, velocity)
|
|
||||||
when start_time >= 0 and end_time > start_time do
|
|
||||||
%__MODULE__{
|
|
||||||
channel: channel,
|
|
||||||
tone: tone,
|
|
||||||
start_time: start_time,
|
|
||||||
end_time: end_time,
|
|
||||||
velocity: velocity
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule ChangeProgram do
|
defmodule ChangeProgram do
|
||||||
|
@ -37,12 +19,6 @@ defmodule MIDIPlayer.Event do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
defstruct channel: 0, time: 0, program: 0
|
defstruct channel: 0, time: 0, program: 0
|
||||||
|
|
||||||
@spec new(MIDISynth.Command.channel(), non_neg_integer(), non_neg_integer()) ::
|
|
||||||
%ChangeProgram{}
|
|
||||||
def new(channel, time, program) do
|
|
||||||
%__MODULE__{channel: channel, time: time, program: program}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
|
@ -50,6 +26,30 @@ defmodule MIDIPlayer.Event do
|
||||||
"""
|
"""
|
||||||
@type t :: %Note{} | %ChangeProgram{}
|
@type t :: %Note{} | %ChangeProgram{}
|
||||||
|
|
||||||
|
@spec note(
|
||||||
|
MIDISynth.Command.channel(),
|
||||||
|
non_neg_integer(),
|
||||||
|
non_neg_integer(),
|
||||||
|
non_neg_integer(),
|
||||||
|
MIDISynth.Command.velocity()
|
||||||
|
) :: %Note{}
|
||||||
|
def note(channel, tone, start_time, end_time, velocity)
|
||||||
|
when start_time >= 0 and end_time > start_time do
|
||||||
|
%Note{
|
||||||
|
channel: channel,
|
||||||
|
tone: tone,
|
||||||
|
start_time: start_time,
|
||||||
|
end_time: end_time,
|
||||||
|
velocity: velocity
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec change_program(MIDISynth.Command.channel(), non_neg_integer(), non_neg_integer()) ::
|
||||||
|
%ChangeProgram{}
|
||||||
|
def change_program(channel, time, program) do
|
||||||
|
%ChangeProgram{channel: channel, time: time, program: program}
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Converts the event to a list of MIDI commands.
|
Converts the event to a list of MIDI commands.
|
||||||
"""
|
"""
|
||||||
|
|
2
mix.exs
2
mix.exs
|
@ -4,7 +4,7 @@ defmodule MIDIPlayer.MixProject do
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :midi_player,
|
app: :midi_player,
|
||||||
version: "0.1.0",
|
version: "0.2.0",
|
||||||
elixir: "~> 1.10",
|
elixir: "~> 1.10",
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
deps: deps(),
|
deps: deps(),
|
||||||
|
|
|
@ -6,23 +6,23 @@ defmodule MIDIPlayer.EventTest do
|
||||||
|
|
||||||
test "note event" do
|
test "note event" do
|
||||||
assert %Event.Note{channel: 0, tone: 60, start_time: 1, end_time: 1000, velocity: 127} =
|
assert %Event.Note{channel: 0, tone: 60, start_time: 1, end_time: 1000, velocity: 127} =
|
||||||
Event.Note.new(0, 60, 1, 1000, 127)
|
Event.note(0, 60, 1, 1000, 127)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "change program event" do
|
test "change program event" do
|
||||||
assert %Event.ChangeProgram{channel: 0, time: 1, program: 40} =
|
assert %Event.ChangeProgram{channel: 0, time: 1, program: 40} =
|
||||||
Event.ChangeProgram.new(0, 1, 40)
|
Event.change_program(0, 1, 40)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "note event conversion" do
|
test "note event conversion" do
|
||||||
note = Event.Note.new(0, 60, 1, 1000, 127)
|
note = Event.note(0, 60, 1, 1000, 127)
|
||||||
[note_on, note_off] = Event.convert(note)
|
[note_on, note_off] = Event.convert(note)
|
||||||
assert {1, <<0x90, 60, 127>>} = note_on
|
assert {1, <<0x90, 60, 127>>} = note_on
|
||||||
assert {1000, <<0x80, 60, 64>>} = note_off
|
assert {1000, <<0x80, 60, 64>>} = note_off
|
||||||
end
|
end
|
||||||
|
|
||||||
test "change program event conversion" do
|
test "change program event conversion" do
|
||||||
change_program = Event.ChangeProgram.new(0, 1, 40)
|
change_program = Event.change_program(0, 1, 40)
|
||||||
assert [{1, <<0xC0, 40>>}] = Event.convert(change_program)
|
assert [{1, <<0xC0, 40>>}] = Event.convert(change_program)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,55 +3,56 @@ defmodule MIDIPlayerTest do
|
||||||
doctest MIDIPlayer
|
doctest MIDIPlayer
|
||||||
|
|
||||||
alias MIDIPlayer, as: Player
|
alias MIDIPlayer, as: Player
|
||||||
|
alias MIDIPlayer.Event
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
{:ok, _pid} = Player.start_link()
|
{:ok, player} = Player.start_link([])
|
||||||
:ok
|
[player: player]
|
||||||
end
|
end
|
||||||
|
|
||||||
setup_all do
|
setup_all do
|
||||||
events = Enum.map(1..4, &MIDIPlayer.Event.Note.new(9, 51, &1 * 500, (&1 + 1) * 500, 127))
|
events = Enum.map(1..4, &Event.note(9, 51, &1 * 500, (&1 + 1) * 500, 127))
|
||||||
duration = 2000
|
duration = 2000
|
||||||
[events: events, duration: duration]
|
[events: events, duration: duration]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "play", %{events: events, duration: duration} do
|
test "play", %{player: player, events: events, duration: duration} do
|
||||||
assert :ok = Player.generate_schedule(events, duration)
|
assert :ok = Player.generate_schedule(player, events, duration)
|
||||||
assert :ok = Player.play()
|
assert :ok = Player.play(player)
|
||||||
Process.sleep(2500)
|
Process.sleep(2500)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "pause & resume", %{events: events, duration: duration} do
|
test "pause & resume", %{player: player, events: events, duration: duration} do
|
||||||
Player.generate_schedule(events, duration)
|
Player.generate_schedule(player, events, duration)
|
||||||
Player.play()
|
Player.play(player)
|
||||||
Process.sleep(1100)
|
Process.sleep(1100)
|
||||||
assert :ok = Player.pause()
|
assert :ok = Player.pause(player)
|
||||||
Process.sleep(500)
|
Process.sleep(500)
|
||||||
assert :ok = Player.resume()
|
assert :ok = Player.resume(player)
|
||||||
Process.sleep(1400)
|
Process.sleep(1400)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "pause & resume edge cases", %{events: events, duration: duration} do
|
test "pause & resume edge cases", %{player: player, events: events, duration: duration} do
|
||||||
Player.generate_schedule(events, duration)
|
Player.generate_schedule(player, events, duration)
|
||||||
assert {:error, :not_started} = Player.pause()
|
assert {:error, :not_started} = Player.pause(player)
|
||||||
assert {:error, :not_paused} = Player.resume()
|
assert {:error, :not_paused} = Player.resume(player)
|
||||||
Player.play()
|
Player.play(player)
|
||||||
Player.pause()
|
Player.pause(player)
|
||||||
assert {:error, :already_paused} = Player.pause()
|
assert {:error, :already_paused} = Player.pause(player)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "event conversion" do
|
test "event conversion", %{player: player} do
|
||||||
event1 = MIDIPlayer.Event.ChangeProgram.new(0, 1, 40)
|
event1 = Event.change_program(0, 1, 40)
|
||||||
event2 = MIDIPlayer.Event.Note.new(0, 60, 1, 1000, 127)
|
event2 = Event.note(0, 60, 1, 1000, 127)
|
||||||
events = [event1, event2]
|
events = [event1, event2]
|
||||||
duration = 100
|
duration = 100
|
||||||
assert :ok = Player.generate_schedule(events, duration)
|
assert :ok = Player.generate_schedule(player, events, duration)
|
||||||
|
|
||||||
change_program = MIDISynth.Command.change_program(0, 40)
|
change_program = MIDISynth.Command.change_program(0, 40)
|
||||||
note_on = MIDISynth.Command.note_on(0, 60, 127)
|
note_on = MIDISynth.Command.note_on(0, 60, 127)
|
||||||
note_off = MIDISynth.Command.note_off(0, 60)
|
note_off = MIDISynth.Command.note_off(0, 60)
|
||||||
|
|
||||||
[command1, command2] = Player.get_schedule()
|
[command1, command2] = Player.get_schedule(player)
|
||||||
assert {1, <<^change_program::binary-size(2), ^note_on::binary-size(3)>>} = command1
|
assert {1, <<^change_program::binary-size(2), ^note_on::binary-size(3)>>} = command1
|
||||||
assert {1000, ^note_off} = command2
|
assert {1000, ^note_off} = command2
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue