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: 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

View file

@ -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,
%{ %{

View file

@ -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.
""" """

View file

@ -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(),

View file

@ -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

View file

@ -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