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:
|
||||
|
||||
```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
|
||||
|
|
|
@ -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,
|
||||
%{
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
2
mix.exs
2
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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue