Spawn the player server using the given options

Pass start_link arguments to MIDISynth
Simplify event creation
This commit is contained in:
Pim Kunis 2020-10-14 13:01:21 +02:00
parent 808370cf7f
commit 2b38e95436
6 changed files with 92 additions and 87 deletions

View file

@ -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:
```elixir
{:midi_player, "~> 0.1.0"}
{:midi_player, "~> 0.2.0"}
```
## Examples
@ -33,13 +33,13 @@ First, let's create some events.
This plays a piano sound for the C note for 1 second:
```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:
```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.)
@ -47,22 +47,22 @@ iex> change = MIDIPlayer.Event.ChangeProgram.new(0, 1000, 41)
Finally, play two notes on the violin at the same time:
```elixir
iex> violin1 = MIDIPlayer.Event.Note.new(0, 67, 1000, 3000, 127)
iex> violin2 = MIDIPlayer.Event.Note.new(0, 64, 1000, 3000, 127)
iex> violin1 = MIDIPlayer.Event.note(0, 67, 1000, 3000, 127)
iex> violin2 = MIDIPlayer.Event.note(0, 64, 1000, 3000, 127)
```
Now we are ready to play these events.
First start the player like so:
```elixir
iex> MIDIPlayer.start_link()
iex> {:ok, player} = MIDIPlayer.start_link([])
```
Then load the events, and play them!
```elixir
iex> MIDIPlayer.generate_schedule([piano, change, violin1, violin2], 3000)
iex> MIDIPlayer.play()
iex> MIDIPlayer.generate_schedule(player, [piano, change, violin1, violin2], 3000)
iex> MIDIPlayer.play(player)
```
## Thanks

View file

@ -14,10 +14,12 @@ defmodule MIDIPlayer do
@doc """
Start the MIDI player.
Arguments are the same as `MIDISynth.start_link/2`.
"""
@spec start_link() :: GenServer.on_start()
def start_link do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
@spec start_link(keyword(), GenServer.options()) :: GenServer.on_start()
def start_link(args, opts \\ []) do
GenServer.start_link(__MODULE__, args, opts)
end
@doc """
@ -31,49 +33,51 @@ defmodule MIDIPlayer do
See `MIDIPlayer.Event` to create events.
"""
@spec generate_schedule([MIDIPlayer.Event.t()], non_neg_integer()) :: :ok
def generate_schedule(events, duration) when duration > 0 do
GenServer.call(__MODULE__, {:generate_schedule, events, duration})
@spec generate_schedule(GenServer.server(), [MIDIPlayer.Event.t()], non_neg_integer()) :: :ok
def generate_schedule(player, events, duration) when duration > 0 do
GenServer.call(player, {:generate_schedule, events, duration})
end
@doc """
Play the current MIDI schedule from the start.
"""
@spec play() :: :ok
def play do
GenServer.call(__MODULE__, :play)
@spec play(GenServer.server()) :: :ok
def play(player) do
GenServer.call(player, :play)
end
@doc """
Set the player on repeat or not.
"""
@spec set_repeat(boolean()) :: :ok
def set_repeat(repeat) when is_boolean(repeat) do
GenServer.call(__MODULE__, {:set_repeat, repeat})
@spec set_repeat(GenServer.server(), boolean()) :: :ok
def set_repeat(player, repeat) when is_boolean(repeat) do
GenServer.call(player, {:set_repeat, repeat})
end
@doc """
Stop the player and cancel the pause.
"""
@spec stop_playing() :: :ok
def stop_playing do
GenServer.call(__MODULE__, :stop_playing)
@spec stop_playing(GenServer.server()) :: :ok
def stop_playing(player) do
GenServer.call(player, :stop_playing)
end
@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}
def pause do
GenServer.call(__MODULE__, :pause)
@spec pause(GenServer.server()) :: :ok | {:error, :already_paused | :not_started}
def pause(player) do
GenServer.call(player, :pause)
end
@doc """
Resume playback on the player after it has been paused.
"""
@spec resume() :: :ok | {:error, :not_paused}
def resume do
GenServer.call(__MODULE__, :resume)
@spec resume(GenServer.server()) :: :ok | {:error, :not_paused}
def resume(player) do
GenServer.call(player, :resume)
end
@doc """
@ -83,16 +87,16 @@ defmodule MIDIPlayer do
corresponding bitstream of MIDI commands to be played at that time.
The list is guaranteed to be ascending in time.
"""
@spec get_schedule() :: schedule()
def get_schedule do
GenServer.call(__MODULE__, :get_schedule)
@spec get_schedule(GenServer.server()) :: schedule()
def get_schedule(player) do
GenServer.call(player, :get_schedule)
end
# Server callbacks
@impl GenServer
def init(_arg) do
{:ok, synth} = MIDISynth.start_link([])
def init(args) do
{:ok, synth} = MIDISynth.start_link(args)
{:ok,
%{

View file

@ -11,24 +11,6 @@ defmodule MIDIPlayer.Event do
"""
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
defmodule ChangeProgram do
@ -37,12 +19,6 @@ defmodule MIDIPlayer.Event do
"""
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
@typedoc """
@ -50,6 +26,30 @@ defmodule MIDIPlayer.Event do
"""
@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 """
Converts the event to a list of MIDI commands.
"""

View file

@ -4,7 +4,7 @@ defmodule MIDIPlayer.MixProject do
def project do
[
app: :midi_player,
version: "0.1.0",
version: "0.2.0",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps(),

View file

@ -6,23 +6,23 @@ defmodule MIDIPlayer.EventTest do
test "note event" do
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
test "change program event" do
assert %Event.ChangeProgram{channel: 0, time: 1, program: 40} =
Event.ChangeProgram.new(0, 1, 40)
Event.change_program(0, 1, 40)
end
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)
assert {1, <<0x90, 60, 127>>} = note_on
assert {1000, <<0x80, 60, 64>>} = note_off
end
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)
end
end

View file

@ -3,55 +3,56 @@ defmodule MIDIPlayerTest do
doctest MIDIPlayer
alias MIDIPlayer, as: Player
alias MIDIPlayer.Event
setup do
{:ok, _pid} = Player.start_link()
:ok
{:ok, player} = Player.start_link([])
[player: player]
end
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
[events: events, duration: duration]
end
test "play", %{events: events, duration: duration} do
assert :ok = Player.generate_schedule(events, duration)
assert :ok = Player.play()
test "play", %{player: player, events: events, duration: duration} do
assert :ok = Player.generate_schedule(player, events, duration)
assert :ok = Player.play(player)
Process.sleep(2500)
end
test "pause & resume", %{events: events, duration: duration} do
Player.generate_schedule(events, duration)
Player.play()
test "pause & resume", %{player: player, events: events, duration: duration} do
Player.generate_schedule(player, events, duration)
Player.play(player)
Process.sleep(1100)
assert :ok = Player.pause()
assert :ok = Player.pause(player)
Process.sleep(500)
assert :ok = Player.resume()
assert :ok = Player.resume(player)
Process.sleep(1400)
end
test "pause & resume edge cases", %{events: events, duration: duration} do
Player.generate_schedule(events, duration)
assert {:error, :not_started} = Player.pause()
assert {:error, :not_paused} = Player.resume()
Player.play()
Player.pause()
assert {:error, :already_paused} = Player.pause()
test "pause & resume edge cases", %{player: player, events: events, duration: duration} do
Player.generate_schedule(player, events, duration)
assert {:error, :not_started} = Player.pause(player)
assert {:error, :not_paused} = Player.resume(player)
Player.play(player)
Player.pause(player)
assert {:error, :already_paused} = Player.pause(player)
end
test "event conversion" do
event1 = MIDIPlayer.Event.ChangeProgram.new(0, 1, 40)
event2 = MIDIPlayer.Event.Note.new(0, 60, 1, 1000, 127)
test "event conversion", %{player: player} do
event1 = Event.change_program(0, 1, 40)
event2 = Event.note(0, 60, 1, 1000, 127)
events = [event1, event2]
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)
note_on = MIDISynth.Command.note_on(0, 60, 127)
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 {1000, ^note_off} = command2
end