diff --git a/README.md b/README.md index 947abe2..4a2f185 100644 --- a/README.md +++ b/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: ```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 diff --git a/lib/midi_player.ex b/lib/midi_player.ex index 9e4bcc4..d8ab784 100644 --- a/lib/midi_player.ex +++ b/lib/midi_player.ex @@ -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, %{ diff --git a/lib/midi_player/event.ex b/lib/midi_player/event.ex index aaa765d..18350d8 100644 --- a/lib/midi_player/event.ex +++ b/lib/midi_player/event.ex @@ -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. """ diff --git a/mix.exs b/mix.exs index d1774ef..427270f 100644 --- a/mix.exs +++ b/mix.exs @@ -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(), diff --git a/test/event_test.exs b/test/event_test.exs index 6ab709b..2d55932 100644 --- a/test/event_test.exs +++ b/test/event_test.exs @@ -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 diff --git a/test/player_test.exs b/test/player_test.exs index 7a00326..9fb71bf 100644 --- a/test/player_test.exs +++ b/test/player_test.exs @@ -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