Skip to content

Commit

Permalink
Merge pull request #3 from lucidstack/with-buffer-size
Browse files Browse the repository at this point in the history
5.0.0
  • Loading branch information
lucidstack committed Jun 1, 2016
2 parents 4c39c69 + f1e50b8 commit 91467ad
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 73 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 5.0.0
* `@ 147f569` - `PortMidi.Reader` now passes a `buffer_size` to the underlying nif, saving MIDI messages from being lost. This `buffer_size` is set to 256 by default, and can be configured at application level: `config :portmidi, buffer_size: 1024`
* `@ ed9e3bb` - `PortMidi.Reader` now emits messages as lists, no more as simple tuples. Sometimes there could be only one message, but a list is always returned. The tuples have also changed structure, to include timestamps, that were previously ignored: `[{{status, note1, note2}, timestamp}, ...]`
* `@ d202f7a` - `PortMidi.Writer` now accepts good old message tuples (`{status, note1, note2}`), event tuples, with timestamp (`{{status, note1, note2}, timestamp}`) or lists of event tuples (`[{{status, note1, note2}, timestamp}, ...]`). This is the preferred way for high throughput, and can be safely used as a pipe from an input device.

## 4.1.0
* `@ 614a27e` - Opening inputs and outputs now return `{:error, reason}` if Portmidi can't open the given device. Previously, the Portmidid NIFs would just throw a bad argument error, without context. `reason` is an atom representing an error from the C library. Have a look at `src/portmidi_shared.c#makePmErrorAtom` for all possible errors.

Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,24 @@ Add portmidi to your list of dependencies in `mix.exs`, and ensure
that `portmidi` is started before your application:
```
def deps do
[{:portmidi, "~> 3.1"}]
[{:portmidi, "~> 5.0"}]
end
def application do
[applications: [:portmidi]]
end
```

## Configuration

If needed, the input buffer size can be set, in your `config.exs`:

```
config :portmidi, buffer_size: 1024
```

By default, this value is 256.

## Usage

To send MIDI events to a MIDI device:
Expand All @@ -29,7 +39,16 @@ iex(1)> {:ok, output} = PortMidi.open(:output, "Launchpad Mini")
iex(2)> PortMidi.write(output, {176, 0, 127})
:ok
iex(3)> PortMidi.close(:output, output)
iex(3)> PortMidi.write(output, {{176, 0, 127}, 123}) # with timestamp
:ok
iex(4)> PortMidi.write(output, [
iex(5)> {{176, 0, 127}, 123},
iex(6)> {{178, 0, 127}, 128}
iex(7)> ]) # as a sequence of events (more efficient)
:ok
iex(8)> PortMidi.close(:output, output)
:ok
```

Expand Down
21 changes: 9 additions & 12 deletions lib/portmidi.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,19 @@ defmodule PortMidi do
Input.listen(input, pid)

@doc """
Writes a MIDI event to the given `output` device. `message` must be a tuple
`{status, note, velocity}`. Returns `:ok` on write.
Writes a MIDI event to the given `output` device. `message` can be a tuple
`{status, note, velocity}`, a tuple `{{status, note, velocity}, timestamp}`
or a list `[{{status, note, velocity}, timestamp}, ...]`. Returns `:ok` on write.
"""
@spec write(pid(), {byte(), byte(), byte()}) :: :ok
@type message :: {byte(), byte(), byte()}
@type timestamp :: byte()

@spec write(pid(), message) :: :ok
@spec write(pid(), {message, timestamp}) :: :ok
@spec write(pid(), [{message, timestamp}, ...]) :: :ok
def write(output, message), do:
Output.write(output, message)

@doc """
Writes a MIDI event to the given `output` device, with given `timestamp`.
`message` must be a tuple `{status, note, velocity}`. Returns `:ok` on
write.
"""
@spec write(pid(), {byte(), byte(), byte()}, non_neg_integer()) :: :ok
def write(output, message, timestamp), do:
Output.write(output, message, timestamp)

@doc """
Returns a map with input and output devices, in the form of
`PortMidi.Device` structs
Expand Down
21 changes: 12 additions & 9 deletions lib/portmidi/input/reader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ defmodule PortMidi.Input.Reader do
import PortMidi.Nifs.Input
alias PortMidi.Input.Server

@buffer_size Application.get_env(:portmidi, :buffer_size, 256)

def start_link(server, device_name) do
Agent.start_link fn -> do_start(server, device_name) end
Agent.start_link fn -> start(server, device_name) end
end

# Client implementation
Expand All @@ -19,25 +21,26 @@ defmodule PortMidi.Input.Reader do

# Agent implementation
######################

defp do_start(server, device_name) do
defp start(server, device_name) do
case device_name |> String.to_char_list |> do_open do
{:ok, stream} -> {server, stream}
{:error, reason} -> exit(reason)
end
end

defp do_listen({server, stream}) do
task = Task.async fn -> do_loop(server, stream) end
task = Task.async fn -> loop(server, stream) end
{:ok, {server, stream, task}}
end

defp do_loop(server, stream) do
if do_poll(stream) == :read do
Server.new_message(server, stream |> do_read)
end
defp loop(server, stream) do
if do_poll(stream) == :read, do: read_and_send(server,stream)
loop(server, stream)
end

do_loop(server, stream)
defp read_and_send(server, stream) do
messages = do_read(stream, @buffer_size)
Server.new_messages(server, messages)
end

defp do_stop({_server, stream, task}) do
Expand Down
8 changes: 4 additions & 4 deletions lib/portmidi/input/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ defmodule PortMidi.Input.Server do
# Client implementation
#######################

def new_message(server, message), do:
GenServer.cast(server, {:new_message, message})
def new_messages(server, messages), do:
GenServer.cast(server, {:new_messages, messages})

def stop(server), do:
GenServer.stop(server)
Expand All @@ -30,10 +30,10 @@ defmodule PortMidi.Input.Server do
end
end

def handle_cast({:new_message, message}, reader) do
def handle_cast({:new_messages, messages}, reader) do
self
|> Listeners.list
|> Enum.each(&(send(&1, {self, message})))
|> Enum.each(&(send(&1, {self, messages})))

{:noreply, reader}
end
Expand Down
2 changes: 1 addition & 1 deletion lib/portmidi/nifs/input.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule PortMidi.Nifs.Input do
def do_poll(_stream), do:
raise "NIF library not loaded"

def do_read(_stream), do:
def do_read(_stream, _buffer_size), do:
raise "NIF library not loaded"

def do_open(_device_name), do:
Expand Down
2 changes: 1 addition & 1 deletion lib/portmidi/nifs/output.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule PortMidi.Nifs.Output do
def do_open(_device_name), do:
raise "NIF library not loaded"

def do_write(_stream, _message, _timestamp), do:
def do_write(_stream, _message), do:
raise "NIF library not loaded"

def do_close(_stream), do:
Expand Down
33 changes: 19 additions & 14 deletions lib/portmidi/output.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,18 @@ defmodule PortMidi.Output do
GenServer.start_link(__MODULE__, device_name)
end


# Client implementation
#######################

@default_timestamp 0
def write(server, message, timestamp \\ @default_timestamp)

def write(_, message, _) when length(message) != 3, do:
raise "message must be [status, note, velocity]"

def write(server, message, timestamp), do:
GenServer.call(server, {:write, message, timestamp})
def write(server, message), do:
GenServer.call(server, {:write, message})

def stop(server), do:
GenServer.stop(server)


# Server implementation
#######################

def init(device_name) do
Process.flag(:trap_exit, true)

Expand All @@ -32,12 +26,23 @@ defmodule PortMidi.Output do
end
end

def handle_call({:write, message, timestamp}, _from, stream) do
response = do_write(stream, message, timestamp)
def handle_call({:write, messages}, _from, stream) when is_list(messages) do
response = do_write(stream, messages)
{:reply, response, stream}
end

def terminate(_reason, stream), do:
stream |> do_close
@default_timestamp 0
def handle_call({:write, {_, _, _} = message}, _from, stream) do
response = do_write(stream, [{message, @default_timestamp}])
{:reply, response, stream}
end

def handle_call({:write, message}, _from, stream) do
response = do_write(stream, [message])
{:reply, response, stream}
end

def terminate(_reason, stream) do
stream |> do_close
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule PortMidi.Mixfile do
use Mix.Project
@version "4.2.0"
@version "5.0.0"

def project do
[app: :portmidi,
Expand Down
25 changes: 18 additions & 7 deletions src/portmidi_in.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,31 @@ static ERL_NIF_TERM do_poll(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])

static ERL_NIF_TERM do_read(ErlNifEnv* env, int arc, const ERL_NIF_TERM argv[]) {
PmEvent buffer[MAXBUFLEN];
int status, data1, data2;
int status, data1, data2, timestamp;
static PortMidiStream ** stream;

ErlNifResourceType* streamType = (ErlNifResourceType*)enif_priv_data(env);
if(!enif_get_resource(env, argv[0], streamType, (PortMidiStream **) &stream)) {
return enif_make_badarg(env);
}

int numEvents = Pm_Read(*stream, buffer, 1);
status = enif_make_int(env, Pm_MessageStatus(buffer[0].message));
data1 = enif_make_int(env, Pm_MessageData1(buffer[0].message));
data2 = enif_make_int(env, Pm_MessageData2(buffer[0].message));
int bufferSize = enif_make_int(env, argv[2]);
int numEvents = Pm_Read(*stream, buffer, bufferSize);

ERL_NIF_TERM events[numEvents];
for(int i = 0; i < numEvents; i++) {
status = enif_make_int(env, Pm_MessageStatus(buffer[i].message));
data1 = enif_make_int(env, Pm_MessageData1(buffer[i].message));
data2 = enif_make_int(env, Pm_MessageData2(buffer[i].message));
timestamp = enif_make_long(env, buffer[i].timestamp);
events[i] = enif_make_tuple2(
env,
enif_make_tuple3(env, status, data1, data2),
timestamp
);
}

return enif_make_tuple3(env, status, data1, data2);
return enif_make_list_from_array(env, events, numEvents);
}

static ERL_NIF_TERM do_close(ErlNifEnv* env, int arc, const ERL_NIF_TERM argv[]) {
Expand All @@ -95,7 +106,7 @@ static ERL_NIF_TERM do_close(ErlNifEnv* env, int arc, const ERL_NIF_TERM argv[])
static ErlNifFunc nif_funcs[] = {
{"do_open", 1, do_open},
{"do_poll", 1, do_poll},
{"do_read", 1, do_read},
{"do_read", 2, do_read},
{"do_close", 1, do_close}
};

Expand Down
50 changes: 31 additions & 19 deletions src/portmidi_out.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,26 +49,38 @@ static ERL_NIF_TERM do_write(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]
return enif_make_badarg(env);
}

int numOfErlValues;
ERL_NIF_TERM erlMessage = argv[1];
const ERL_NIF_TERM * erlValues;
enif_get_tuple(env, erlMessage, &numOfErlValues, &erlValues);

long int status, note, velocity, timestamp = 0;
enif_get_long(env, erlValues[0], &status);
enif_get_long(env, erlValues[1], &note);
enif_get_long(env, erlValues[2], &velocity);

if(argv[2]) {
enif_get_long(env, argv[2], &timestamp);
}
ERL_NIF_TERM erlMessages = argv[1];
const ERL_NIF_TERM * erlEvent;
const ERL_NIF_TERM * erlMessage;
ERL_NIF_TERM erlTuple;

unsigned int numOfMessages;
int tupleSize;
enif_get_list_length(env, erlMessages, &numOfMessages);

PmEvent events[numOfMessages];
long int status, note, velocity, timestamp;

for(int i = 0; i < numOfMessages; i++) {
enif_get_list_cell(env, erlMessages, &erlTuple, &erlMessages);
enif_get_tuple(env, erlTuple, &tupleSize, &erlEvent);

PmEvent event;
event.message = Pm_Message(status, note, velocity);
event.timestamp = timestamp;
enif_get_tuple(env, erlEvent[0], &tupleSize, &erlMessage);
enif_get_long(env, erlMessage[0], &status);
enif_get_long(env, erlMessage[1], &note);
enif_get_long(env, erlMessage[2], &velocity);

enif_get_long(env, erlEvent[1], &timestamp);

PmEvent event;
event.message = Pm_Message(status, note, velocity);
event.timestamp = timestamp;

events[i] = event;
}

PmError writeError;
writeError = Pm_Write(*stream, &event, 1);
writeError = Pm_Write(*stream, events, numOfMessages);

if (writeError == pmNoError) {
return enif_make_atom(env, "ok");
Expand Down Expand Up @@ -98,8 +110,8 @@ static ERL_NIF_TERM do_close(ErlNifEnv* env, int arc, const ERL_NIF_TERM argv[])
}

static ErlNifFunc nif_funcs[] = {
{"do_open", 1, do_open},
{"do_write", 3, do_write},
{"do_open", 1, do_open},
{"do_write", 2, do_write},
{"do_close", 1, do_close}
};

Expand Down
6 changes: 3 additions & 3 deletions test/portmidi/input/server_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ defmodule PortMidiInputServerTest do
use ExUnit.Case, async: false
import Mock

test "new_message/2 broadcasts to processes in Listeners" do
test "new_messages/2 broadcasts to processes in Listeners" do
{:ok, input} = Agent.start(fn -> [] end)
Listeners.register(input, self)

Agent.get input, fn(_) ->
handle_cast({:new_message, {176, 0, 127}}, nil)
handle_cast({:new_messages, %{message: {176, 0, 127}, timestamp: 0}}, nil)
end

assert_received {input, {176, 0, 127}}
assert_received {input, %{message: {176, 0, 127}, timestamp: 0}}
end

test "terminating the server calls close on the reader" do
Expand Down

0 comments on commit 91467ad

Please sign in to comment.