├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── config └── config.exs ├── lib ├── mix │ └── tasks │ │ └── portmidi.devices.ex ├── portmidi.ex └── portmidi │ ├── device.ex │ ├── devices.ex │ ├── input.ex │ ├── input │ ├── reader.ex │ └── server.ex │ ├── listeners.ex │ ├── nifs │ ├── devices.ex │ ├── input.ex │ └── output.ex │ └── output.ex ├── mix.exs ├── mix.lock ├── priv └── .gitkeep ├── src ├── erl_comm.c ├── portmidi_devices.c ├── portmidi_in.c ├── portmidi_out.c └── portmidi_shared.c └── test ├── port_midi_test.exs ├── portmidi ├── devices_test.exs ├── input │ └── server_test.exs └── listeners_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /priv 5 | erl_crash.dump 6 | *.ez 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.1.2 4 | 5 | * `@ 6f2b0aa` - Fix Elixir 1.7 warnings 6 | 7 | ## 5.1.1 8 | 9 | * `@ ea79492` - Replace NULL with 0 when calling findDevice in C code 10 | 11 | ## 5.1.0 12 | 13 | * `@ 1cf967b` - Add an optional `latency` argument to `PortMidi.open/2,3`. When opening an output device, a `latency` value greater than `0` has to be set, if you need to use timestamps; otherwise, these will be ignored. Kudos to [@thbar](https://github.com/thbar) for spotting this issue! 👏 14 | 15 | ## 5.0.1 16 | 17 | * `@ 8f7c308` - Add `-std=c99` and remove unneded flags for NIFs compilation in Makefile 18 | 19 | ## 5.0.0 20 | * `@ 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` 21 | * `@ 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}, ...]` 22 | * `@ 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. 23 | 24 | ## 4.1.0 25 | * `@ 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. 26 | 27 | ## 4.0.0 28 | * `@ 19ff9a8` - MIDI events from PortMidi.Input are now sent as a tuple of three values, instead of an array. This makes the API consistent with PortMidi.Output, which accepts a tuple of three elements. 29 | * `@ 59efd17` - MIDI events are now sent with the server PID, which is returned when an input is opened (e.g. `{:ok, input} = PortMidi.open(:input, "Launchpad")`). This makes it easy to differentiate received messages, so that a process can listen on multiple MIDI devices, and be able to handle the messages differently, pattern matching on the input. The MIDI event is sent as a second tuple, e.g. `{^input, {status, note, velocity}}`. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andrea Rossi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS = -g -std=c99 -O3 -pedantic -Wextra -Wno-unused-parameter -Wno-missing-field-initializers 2 | 3 | ERLANG_PATH = $(shell erl -eval 'io:format("~s", [lists:concat([code:root_dir(), "/erts-", erlang:system_info(version), "/include"])])' -s init stop -noshell) 4 | CFLAGS += -I$(ERLANG_PATH) 5 | 6 | ifneq ($(OS),Windows_NT) 7 | CFLAGS += -fPIC 8 | 9 | ifeq ($(shell uname),Darwin) 10 | LDFLAGS += -dynamiclib -undefined dynamic_lookup 11 | endif 12 | endif 13 | 14 | all: priv/portmidi_in.so priv/portmidi_out.so priv/portmidi_devices.so 15 | 16 | priv/portmidi_in.so: src/portmidi_in.c src/portmidi_shared.c 17 | $(CC) $(CFLAGS) -shared $(LDFLAGS) -o $@ -lportmidi src/portmidi_in.c src/portmidi_shared.c 18 | 19 | priv/portmidi_out.so: src/portmidi_out.c src/portmidi_shared.c 20 | $(CC) $(CFLAGS) -shared $(LDFLAGS) -o $@ -lportmidi src/portmidi_out.c src/portmidi_shared.c 21 | 22 | priv/portmidi_devices.so: src/portmidi_devices.c src/portmidi_shared.c 23 | $(CC) $(CFLAGS) -shared $(LDFLAGS) -o $@ -lportmidi src/portmidi_devices.c src/portmidi_shared.c 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ex-portmidi 2 | 3 | **Need to catch up?** Have a look at the [changelog](/CHANGELOG.md)! 🚀 4 | 5 | ex-portmidi is a wrapper for the [PortMidi C library](http://portmedia.sourceforge.net/portmidi/), 6 | that provides some nice abstractions to (write to|listen on) MIDI devices. 7 | 8 | ## Installation 9 | 10 | Add portmidi to your list of dependencies in `mix.exs`, and ensure 11 | that `portmidi` is started before your application: 12 | ``` 13 | def deps do 14 | [{:portmidi, "~> 5.0"}] 15 | end 16 | 17 | def application do 18 | [applications: [:portmidi]] 19 | end 20 | ``` 21 | 22 | ## Configuration 23 | 24 | If needed, the input buffer size can be set, in your `config.exs`: 25 | 26 | ``` 27 | config :portmidi, buffer_size: 1024 28 | ``` 29 | 30 | By default, this value is 256. 31 | 32 | ## Usage 33 | 34 | To send MIDI events to a MIDI device: 35 | ``` 36 | iex(1)> {:ok, output} = PortMidi.open(:output, "Launchpad Mini") 37 | {:ok, #PID<0.172.0>} 38 | 39 | iex(2)> PortMidi.write(output, {176, 0, 127}) 40 | :ok 41 | 42 | iex(3)> PortMidi.write(output, {{176, 0, 127}, 123}) # with timestamp 43 | :ok 44 | 45 | iex(4)> PortMidi.write(output, [ 46 | iex(5)> {{176, 0, 127}, 123}, 47 | iex(6)> {{178, 0, 127}, 128} 48 | iex(7)> ]) # as a sequence of events (more efficient) 49 | :ok 50 | 51 | iex(8)> PortMidi.close(:output, output) 52 | :ok 53 | ``` 54 | 55 | To listen for MIDI events from a MIDI device: 56 | ``` 57 | iex(1)> {:ok, input} = PortMidi.open(:input, "Launchpad Mini") 58 | {:ok, #PID<0.103.0>} 59 | 60 | ex(2)> PortMidi.listen(input, self) 61 | :ok 62 | 63 | iex(3)> receive do 64 | ...(3)> {^input, event} -> IO.inspect(event) 65 | ...(3)> end 66 | {144, 112, 127} 67 | 68 | iex(4)> PortMidi.close(:input, input) 69 | :ok 70 | ``` 71 | 72 | To list all connected devices: 73 | ``` 74 | ex(1)> PortMidi.devices 75 | %{input: [%PortMidi.Device{input: 1, interf: "CoreMIDI", name: "Launchpad Mini", 76 | opened: 0, output: 0}], 77 | output: [%PortMidi.Device{input: 0, interf: "CoreMIDI", 78 | name: "Launchpad Mini", opened: 0, output: 1}]} 79 | ``` 80 | 81 | For more details, [check out the Hexdocs](https://hexdocs.pm/portmidi/PortMidi.html). 82 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # It is also possible to import configuration files, relative to this 12 | # directory. For example, you can emulate configuration per environment 13 | # by uncommenting the line below and defining dev.exs, test.exs and such. 14 | # Configuration from the imported file will override the ones defined 15 | # here (which is why it is important to import them last). 16 | # 17 | # import_config "#{Mix.env}.exs" 18 | -------------------------------------------------------------------------------- /lib/mix/tasks/portmidi.devices.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Portmidi.Devices do 2 | use Mix.Task 3 | @shortdoc "Shows the connected devices" 4 | 5 | def run(_args) do 6 | IO.puts "Input:" 7 | list_devices(:input) 8 | 9 | IO.puts "Output:" 10 | list_devices(:output) 11 | end 12 | 13 | defp list_devices(type) do 14 | PortMidi.devices[type] 15 | |> Enum.each(&print_device/1) 16 | end 17 | 18 | defp print_device(device) do 19 | IO.puts " - #{device.name}" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/portmidi.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi do 2 | @moduledoc """ 3 | The entry module of portmidi. Through this module you can open and close 4 | devices, listen on input devices, or write to output devices. 5 | """ 6 | 7 | alias PortMidi.Input 8 | alias PortMidi.Output 9 | alias PortMidi.Listeners 10 | alias PortMidi.Devices 11 | 12 | use Application 13 | 14 | @doc """ 15 | Starts the `:portmidi` application. Under the hood, starts the 16 | `Portmidi.Listeners` GenServer, that holds all the listeners to 17 | input devices. 18 | """ 19 | def start(_type, _args) do 20 | import Supervisor.Spec, warn: false 21 | 22 | children = [ 23 | worker(Listeners, []) 24 | ] 25 | 26 | opts = [strategy: :one_for_one, name: PortMidi.Supervisor] 27 | Supervisor.start_link(children, opts) 28 | end 29 | 30 | @doc """ 31 | Opens a connection to the input device with name `device_name`. 32 | 33 | Returns the `pid` to the corresponding GenServer. Use this `pid` to call 34 | `listen/2`. 35 | 36 | If Portmidi can't open the device, a tuple `{:error, reason}` is returned. 37 | Check `src/portmidi_shared.c#makePmErrorAtom` for all possible errors. 38 | """ 39 | @spec open(:input, <<>>) :: {:ok, pid()} | {:error, atom()} 40 | def open(:input, device_name) do 41 | Input.start_link device_name 42 | end 43 | 44 | @doc """ 45 | Opens a connection to the output device with name `device_name`. 46 | 47 | Returns the `pid` to the corresponding GenServer. Use this `pid` to call 48 | `write/2`. 49 | 50 | If Portmidi can't open the device, a tuple `{:error, reason}` is returned. 51 | Check `src/portmidi_shared.c#makePmErrorAtom` for all possible errors. 52 | """ 53 | @spec open(:output, <<>>, non_neg_integer()) :: {:ok, pid()} | {:error, atom()} 54 | def open(:output, device_name, latency \\ 0) do 55 | Output.start_link(device_name, latency) 56 | end 57 | 58 | @doc """ 59 | Terminates the GenServer held by the `device` argument, and closes the 60 | PortMidi stream. If the type is an input, and `listen/2` was called on it, 61 | it also shuts down the listening process. Using the given `device` after 62 | calling this method will raise an error. 63 | """ 64 | @spec close(atom, pid()) :: :ok 65 | def close(device_type, device) 66 | def close(:input, input), do: Input.stop(input) 67 | def close(:output, output), do: Input.stop(output) 68 | 69 | @doc """ 70 | Starts a listening process on the given `input`, and returns `:ok`. After 71 | calling this method, the process with the given `pid` will receive MIDI 72 | events in its mailbox as soon as they are emitted from the device. 73 | """ 74 | @spec listen(pid(), pid()) :: :ok 75 | def listen(input, pid), do: 76 | Input.listen(input, pid) 77 | 78 | @doc """ 79 | Writes a MIDI event to the given `output` device. `message` can be a tuple 80 | `{status, note, velocity}`, a tuple `{{status, note, velocity}, timestamp}` 81 | or a list `[{{status, note, velocity}, timestamp}, ...]`. Returns `:ok` on write. 82 | """ 83 | @type message :: {byte(), byte(), byte()} 84 | @type timestamp :: byte() 85 | 86 | @spec write(pid(), message) :: :ok 87 | @spec write(pid(), {message, timestamp}) :: :ok 88 | @spec write(pid(), [{message, timestamp}, ...]) :: :ok 89 | def write(output, message), do: 90 | Output.write(output, message) 91 | 92 | @doc """ 93 | Returns a map with input and output devices, in the form of 94 | `PortMidi.Device` structs 95 | """ 96 | @spec devices() :: %{input: [%PortMidi.Device{}, ...], output: [%PortMidi.Device{}, ...]} 97 | def devices, do: Devices.list 98 | end 99 | -------------------------------------------------------------------------------- /lib/portmidi/device.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Device do 2 | defstruct [:name, :interf, :input, :output, :opened] 3 | 4 | def build(map) do 5 | map 6 | |> Map.update(:name, nil, &to_string/1) 7 | |> Map.update(:interf, nil, &to_string/1) 8 | |> make_struct() 9 | end 10 | 11 | defp make_struct(map), do: 12 | struct(__MODULE__, map) 13 | end 14 | 15 | -------------------------------------------------------------------------------- /lib/portmidi/devices.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Devices do 2 | import PortMidi.Nifs.Devices 3 | alias PortMidi.Device 4 | 5 | def list do 6 | do_list() 7 | |> Map.update(:input, [], &do_build_devices/1) 8 | |> Map.update(:output, [], &do_build_devices/1) 9 | end 10 | 11 | defp do_build_devices(devices), do: 12 | Enum.map(devices, &Device.build/1) 13 | end 14 | -------------------------------------------------------------------------------- /lib/portmidi/input.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Input do 2 | alias PortMidi.Input.Server 3 | alias PortMidi.Listeners 4 | 5 | def start_link(device_name) do 6 | Server.start_link device_name 7 | end 8 | 9 | def listen(input, pid) do 10 | Listeners.register(input, pid) 11 | end 12 | 13 | def stop(input) do 14 | Server.stop(input) 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /lib/portmidi/input/reader.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Input.Reader do 2 | import PortMidi.Nifs.Input 3 | alias PortMidi.Input.Server 4 | 5 | @buffer_size Application.get_env(:portmidi, :buffer_size, 256) 6 | 7 | def start_link(server, device_name) do 8 | Agent.start_link fn -> start(server, device_name) end 9 | end 10 | 11 | # Client implementation 12 | ####################### 13 | 14 | def listen(agent), do: 15 | Agent.get_and_update agent, &do_listen/1 16 | 17 | def stop(agent) do 18 | Agent.get agent, &do_stop/1 19 | Agent.stop(agent) 20 | end 21 | 22 | # Agent implementation 23 | ###################### 24 | defp start(server, device_name) do 25 | case device_name |> String.to_char_list |> do_open do 26 | {:ok, stream} -> {server, stream} 27 | {:error, reason} -> exit(reason) 28 | end 29 | end 30 | 31 | defp do_listen({server, stream}) do 32 | task = Task.async fn -> loop(server, stream) end 33 | {:ok, {server, stream, task}} 34 | end 35 | 36 | defp loop(server, stream) do 37 | if do_poll(stream) == :read, do: read_and_send(server,stream) 38 | loop(server, stream) 39 | end 40 | 41 | defp read_and_send(server, stream) do 42 | messages = do_read(stream, @buffer_size) 43 | Server.new_messages(server, messages) 44 | end 45 | 46 | defp do_stop({_server, stream, task}) do 47 | task |> Task.shutdown 48 | stream |> do_close 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/portmidi/input/server.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Input.Server do 2 | alias PortMidi.Listeners 3 | alias PortMidi.Input.Reader 4 | 5 | def start_link(device_name) do 6 | GenServer.start_link(__MODULE__, device_name) 7 | end 8 | 9 | # Client implementation 10 | ####################### 11 | 12 | def new_messages(server, messages), do: 13 | GenServer.cast(server, {:new_messages, messages}) 14 | 15 | def stop(server), do: 16 | GenServer.stop(server) 17 | 18 | # Server implementation 19 | ####################### 20 | 21 | def init(device_name) do 22 | Process.flag(:trap_exit, true) 23 | 24 | case Reader.start_link(self(), device_name) do 25 | {:ok, reader} -> 26 | Reader.listen(reader) 27 | {:ok, reader} 28 | {:error, reason} -> 29 | {:stop, reason} 30 | end 31 | end 32 | 33 | def handle_cast({:new_messages, messages}, reader) do 34 | self() 35 | |> Listeners.list 36 | |> Enum.each(&(send(&1, {self(), messages}))) 37 | 38 | {:noreply, reader} 39 | end 40 | 41 | def terminate(_reason, reader), do: 42 | reader |> Reader.stop 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lib/portmidi/listeners.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Listeners do 2 | def start_link do 3 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 4 | end 5 | 6 | # Client implementation 7 | ####################### 8 | 9 | def register(input, pid), do: 10 | GenServer.cast(__MODULE__, {:register, input, pid}) 11 | 12 | def list(input), do: 13 | GenServer.call(__MODULE__, {:list, input}) 14 | 15 | # Server implementation 16 | ####################### 17 | 18 | def init(:ok), do: 19 | {:ok, {%{}, %{}}} 20 | 21 | def handle_call({:list, input}, _from, {listeners, _} = state) do 22 | if Map.has_key?(listeners, input) do 23 | {:reply, Map.get(listeners, input), state} 24 | else 25 | {:reply, {:error, :input_not_found}, state} 26 | end 27 | end 28 | 29 | def handle_cast({:register, input, pid}, {listeners, refs}) do 30 | ref = Process.monitor(pid) 31 | refs = refs |> Map.put(ref, pid) 32 | 33 | input_listeners = [pid | Map.get(listeners, input, [])] 34 | listeners = listeners |> Map.put(input, input_listeners) 35 | 36 | {:noreply, {listeners, refs}} 37 | end 38 | 39 | def handle_info({:DOWN, ref, :process, _, _}, {listeners, refs}) do 40 | {pid, refs} = Map.pop(refs, ref) 41 | listeners = do_update_listeners(listeners, pid) 42 | 43 | {:noreply, {listeners, refs}} 44 | end 45 | 46 | def handle_info(_msg, state), do: 47 | {:noreply, state} 48 | 49 | require Logger 50 | def terminate(reason, _), do: 51 | Logger.error(reason) 52 | 53 | # Private implementation 54 | ######################## 55 | 56 | def do_update_listeners(listeners, pid) do 57 | Enum.reduce(listeners, %{}, fn({input, listeners}, acc) -> 58 | Map.put acc, input, do_find_new_listeners(listeners, pid) 59 | end) 60 | end 61 | 62 | def do_find_new_listeners(listeners, pid) do 63 | listeners |> Enum.reject(&(&1 == pid)) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/portmidi/nifs/devices.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Nifs.Devices do 2 | @on_load :init 3 | def init do 4 | :portmidi 5 | |> :code.priv_dir 6 | |> :filename.join("portmidi_devices") 7 | |> :erlang.load_nif(0) 8 | end 9 | 10 | def do_list, do: 11 | raise "NIF library not loaded" 12 | end 13 | -------------------------------------------------------------------------------- /lib/portmidi/nifs/input.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Nifs.Input do 2 | @on_load {:init, 0} 3 | def init do 4 | :ok = :portmidi 5 | |> :code.priv_dir 6 | |> :filename.join("portmidi_in") 7 | |> :erlang.load_nif(0) 8 | end 9 | 10 | def do_poll(_stream), do: 11 | raise "NIF library not loaded" 12 | 13 | def do_read(_stream, _buffer_size), do: 14 | raise "NIF library not loaded" 15 | 16 | def do_open(_device_name), do: 17 | raise "NIF library not loaded" 18 | 19 | def do_close(_stream), do: 20 | raise "NIF library not loaded" 21 | end 22 | -------------------------------------------------------------------------------- /lib/portmidi/nifs/output.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Nifs.Output do 2 | @on_load {:init, 0} 3 | 4 | def init do 5 | :ok = :portmidi 6 | |> :code.priv_dir 7 | |> :filename.join("portmidi_out") 8 | |> :erlang.load_nif(0) 9 | end 10 | 11 | def do_open(_device_name, _latency), do: 12 | raise "NIF library not loaded" 13 | 14 | def do_write(_stream, _message), do: 15 | raise "NIF library not loaded" 16 | 17 | def do_close(_stream), do: 18 | raise "NIF library not loaded" 19 | end 20 | -------------------------------------------------------------------------------- /lib/portmidi/output.ex: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Output do 2 | import PortMidi.Nifs.Output 3 | 4 | def start_link(device_name, latency) do 5 | GenServer.start_link(__MODULE__, {device_name, latency}) 6 | end 7 | 8 | 9 | # Client implementation 10 | ####################### 11 | def write(server, message), do: 12 | GenServer.call(server, {:write, message}) 13 | 14 | def stop(server), do: 15 | GenServer.stop(server) 16 | 17 | 18 | # Server implementation 19 | ####################### 20 | def init({device_name, latency}) do 21 | Process.flag(:trap_exit, true) 22 | 23 | case do_open(to_charlist(device_name), latency) do 24 | {:ok, stream} -> {:ok, stream} 25 | {:error, reason} -> {:stop, reason} 26 | end 27 | end 28 | 29 | def handle_call({:write, messages}, _from, stream) when is_list(messages) do 30 | response = do_write(stream, messages) 31 | {:reply, response, stream} 32 | end 33 | 34 | @default_timestamp 0 35 | def handle_call({:write, {_, _, _} = message}, _from, stream) do 36 | response = do_write(stream, [{message, @default_timestamp}]) 37 | {:reply, response, stream} 38 | end 39 | 40 | def handle_call({:write, message}, _from, stream) do 41 | response = do_write(stream, [message]) 42 | {:reply, response, stream} 43 | end 44 | 45 | def terminate(_reason, stream) do 46 | stream |> do_close 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PortMidi.Mixfile do 2 | use Mix.Project 3 | @version "5.1.2" 4 | 5 | def project do 6 | [app: :portmidi, 7 | version: @version, 8 | elixir: "~> 1.2", 9 | description: "Elixir bindings to the portmidi C library", 10 | package: package(), 11 | compilers: [:port_midi, :elixir, :app], 12 | build_embedded: Mix.env == :prod, 13 | start_permanent: Mix.env == :prod, 14 | deps: deps(), 15 | 16 | # Docs 17 | name: "PortMidi", 18 | docs: [source_ref: "v#{@version}", main: "PortMidi", 19 | source_url: "https://github.com/lucidstack/ex-portmidi"] 20 | ] 21 | end 22 | 23 | def application do 24 | [applications: [:logger], 25 | mod: {PortMidi, []}] 26 | end 27 | 28 | defp deps do 29 | [{:credo, "~> 0.5.3", only: [:dev, :test]}, 30 | {:mock, "~> 0.1.1", only: :test}, 31 | {:ex_doc, github: "elixir-lang/ex_doc", only: :dev}, 32 | {:earmark, ">= 0.0.0", only: :dev}] 33 | end 34 | 35 | defp package do 36 | [maintainers: ["Andrea Rossi"], 37 | files: ["priv", "lib", "src", "Makefile", "mix.exs", "README.md", "LICENSE"], 38 | licenses: ["MIT"], 39 | links: %{"Github" => "https://github.com/lucidstack/ex-portmidi"}] 40 | end 41 | end 42 | 43 | defmodule Mix.Tasks.Compile.PortMidi do 44 | @moduledoc "Compiles portmidi bindings" 45 | def run(_) do 46 | if Mix.shell.cmd("make") != 0 do 47 | raise Mix.Error, message: "could not run `make`. Do you have make, gcc and libportmidi installed?" 48 | end 49 | 50 | :ok 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, 2 | "credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, 3 | "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, 4 | "ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "8da0eda6b40e1913bb299c8c634f0672036bbd33", []}, 5 | "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:make, :rebar], []}, 6 | "mock": {:hex, :mock, "0.1.3", "657937b03f88fce89b3f7d6becc9f1ec1ac19c71081aeb32117db9bc4d9b3980", [:mix], [{:meck, "~> 0.8.2", [hex: :meck, optional: false]}]}} 7 | -------------------------------------------------------------------------------- /priv/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidstack/ex-portmidi/6f0c842d499b4a37071ad0157e10cb0abd44b174/priv/.gitkeep -------------------------------------------------------------------------------- /src/erl_comm.c: -------------------------------------------------------------------------------- 1 | /*** 2 | * Excerpted from "Programming Erlang, Second Edition", 3 | * published by The Pragmatic Bookshelf. 4 | * Copyrights apply to this code. It may not be used to create training material, 5 | * courses, books, articles, and the like. Contact us if you are in doubt. 6 | * We make no guarantees that this code is fit for any purpose. 7 | * Visit http://www.pragmaticprogrammer.com/titles/jaerlang2 for more book information. 8 | ***/ 9 | /* erl_comm.c */ 10 | #include 11 | typedef unsigned char byte; 12 | 13 | int read_cmd(byte *buf); 14 | int write_cmd(byte *buf, int len); 15 | int read_exact(byte *buf, int len); 16 | int write_exact(byte *buf, int len); 17 | 18 | int read_cmd(byte *buf) 19 | { 20 | int len; 21 | if (read_exact(buf, 2) != 2) 22 | return(-1); 23 | len = (buf[0] << 8) | buf[1]; 24 | return read_exact(buf, len); 25 | } 26 | 27 | int write_cmd(byte *buf, int len) 28 | { 29 | byte li; 30 | li = (len >> 8) & 0xff; 31 | write_exact(&li, 1); 32 | li = len & 0xff; 33 | write_exact(&li, 1); 34 | return write_exact(buf, len); 35 | } 36 | 37 | int read_exact(byte *buf, int len) 38 | { 39 | int i, got=0; 40 | do { 41 | if ((i = read(0, buf+got, len-got)) <= 0) 42 | return(i); 43 | got += i; 44 | } while (got 3 | #include 4 | #include 5 | #include 6 | 7 | #define MAXBUFLEN 1024 8 | 9 | typedef unsigned char byte; 10 | 11 | const PmDeviceInfo ** listDevices(int); 12 | 13 | static ERL_NIF_TERM do_list(ErlNifEnv* env, int arc, const ERL_NIF_TERM argv[]) { 14 | int i = 0; 15 | int numOfDevices = Pm_CountDevices(); 16 | int numOfInputs = 0, numOfOutputs = 0; 17 | const PmDeviceInfo ** devices = listDevices(numOfDevices); 18 | 19 | ERL_NIF_TERM allDevices = enif_make_new_map(env); 20 | ERL_NIF_TERM inputDevices[numOfDevices]; 21 | ERL_NIF_TERM outputDevices[numOfDevices]; 22 | 23 | for(i = 0; i < numOfDevices; i++) { 24 | ERL_NIF_TERM device = enif_make_new_map(env); 25 | enif_make_map_put(env, device, enif_make_atom(env, "name"), enif_make_string(env, devices[i]->name, ERL_NIF_LATIN1), &device); 26 | enif_make_map_put(env, device, enif_make_atom(env, "interf"), enif_make_string(env, devices[i]->interf, ERL_NIF_LATIN1), &device); 27 | enif_make_map_put(env, device, enif_make_atom(env, "input"), enif_make_int(env, devices[i]->input), &device); 28 | enif_make_map_put(env, device, enif_make_atom(env, "output"), enif_make_int(env, devices[i]->output), &device); 29 | enif_make_map_put(env, device, enif_make_atom(env, "opened"), enif_make_int(env, devices[i]->opened), &device); 30 | 31 | 32 | if(devices[i]->input) { 33 | inputDevices[numOfInputs] = device; 34 | numOfInputs++; 35 | } else { 36 | outputDevices[numOfOutputs] = device; 37 | numOfOutputs++; 38 | } 39 | } 40 | 41 | enif_make_map_put(env, allDevices, enif_make_atom(env, "input"), enif_make_list_from_array(env, inputDevices, numOfInputs), &allDevices); 42 | enif_make_map_put(env, allDevices, enif_make_atom(env, "output"), enif_make_list_from_array(env, outputDevices, numOfOutputs), &allDevices); 43 | 44 | return allDevices; 45 | } 46 | 47 | static ErlNifFunc nif_funcs[] = { 48 | {"do_list", 0, do_list} 49 | }; 50 | 51 | ERL_NIF_INIT(Elixir.PortMidi.Nifs.Devices,nif_funcs,NULL,NULL,NULL,NULL) 52 | -------------------------------------------------------------------------------- /src/portmidi_in.c: -------------------------------------------------------------------------------- 1 | #define MAXBUFLEN 1024 2 | #include "erl_nif.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define MAXBUFLEN 1024 9 | 10 | typedef enum {INPUT, OUTPUT} DeviceType; 11 | 12 | PmError findDevice(PmStream **stream, char *deviceName, DeviceType type, long latency); 13 | char* makePmErrorAtom(PmError); 14 | const PmDeviceInfo ** listDevices(int); 15 | void debug(char *str); 16 | 17 | int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) { 18 | ErlNifResourceFlags flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER; 19 | *priv_data = enif_open_resource_type(env, NULL, "stream_resource", NULL, ERL_NIF_RT_CREATE, NULL); 20 | 21 | return 0; 22 | } 23 | 24 | static ERL_NIF_TERM do_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { 25 | char deviceName[MAXBUFLEN]; 26 | ERL_NIF_TERM streamTerm; 27 | PortMidiStream **streamAlloc; 28 | PmError result; 29 | 30 | ErlNifResourceType* streamType = (ErlNifResourceType*)enif_priv_data(env); 31 | streamAlloc = (PortMidiStream**)enif_alloc_resource(streamType, sizeof(PortMidiStream*)); 32 | 33 | enif_get_string(env, argv[0], deviceName, MAXBUFLEN, ERL_NIF_LATIN1); 34 | if((result = findDevice(streamAlloc, deviceName, INPUT, 0)) != pmNoError) { 35 | ERL_NIF_TERM reason = enif_make_atom(env, makePmErrorAtom(result)); 36 | return enif_make_tuple2(env, enif_make_atom(env, "error"), reason); 37 | } 38 | 39 | streamTerm = enif_make_resource(env, streamAlloc); 40 | enif_keep_resource(streamAlloc); 41 | 42 | return enif_make_tuple2( 43 | env, 44 | enif_make_atom(env, "ok"), 45 | streamTerm 46 | ); 47 | } 48 | 49 | static ERL_NIF_TERM do_poll(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { 50 | static PortMidiStream ** stream; 51 | 52 | ErlNifResourceType* streamType = (ErlNifResourceType*)enif_priv_data(env); 53 | if(!enif_get_resource(env, argv[0], streamType, (PortMidiStream **) &stream)) { 54 | return enif_make_badarg(env); 55 | } 56 | 57 | if(Pm_Poll(*stream)) { 58 | return enif_make_atom(env, "read"); 59 | } else { 60 | return enif_make_atom(env, "retry"); 61 | } 62 | } 63 | 64 | static ERL_NIF_TERM do_read(ErlNifEnv* env, int arc, const ERL_NIF_TERM argv[]) { 65 | PmEvent buffer[MAXBUFLEN]; 66 | int status, data1, data2, timestamp; 67 | static PortMidiStream ** stream; 68 | 69 | ErlNifResourceType* streamType = (ErlNifResourceType*)enif_priv_data(env); 70 | if(!enif_get_resource(env, argv[0], streamType, (PortMidiStream **) &stream)) { 71 | return enif_make_badarg(env); 72 | } 73 | 74 | int bufferSize = enif_make_int(env, argv[2]); 75 | int numEvents = Pm_Read(*stream, buffer, bufferSize); 76 | 77 | ERL_NIF_TERM events[numEvents]; 78 | for(int i = 0; i < numEvents; i++) { 79 | status = enif_make_int(env, Pm_MessageStatus(buffer[i].message)); 80 | data1 = enif_make_int(env, Pm_MessageData1(buffer[i].message)); 81 | data2 = enif_make_int(env, Pm_MessageData2(buffer[i].message)); 82 | timestamp = enif_make_long(env, buffer[i].timestamp); 83 | events[i] = enif_make_tuple2( 84 | env, 85 | enif_make_tuple3(env, status, data1, data2), 86 | timestamp 87 | ); 88 | } 89 | 90 | return enif_make_list_from_array(env, events, numEvents); 91 | } 92 | 93 | static ERL_NIF_TERM do_close(ErlNifEnv* env, int arc, const ERL_NIF_TERM argv[]) { 94 | static PortMidiStream ** stream; 95 | 96 | ErlNifResourceType* streamType = (ErlNifResourceType*)enif_priv_data(env); 97 | if(!enif_get_resource(env, argv[0], streamType, (PortMidiStream **) &stream)) { 98 | return enif_make_badarg(env); 99 | } 100 | 101 | Pm_Close(*stream); 102 | 103 | return enif_make_atom(env, "ok"); 104 | } 105 | 106 | static ErlNifFunc nif_funcs[] = { 107 | {"do_open", 1, do_open}, 108 | {"do_poll", 1, do_poll}, 109 | {"do_read", 2, do_read}, 110 | {"do_close", 1, do_close} 111 | }; 112 | 113 | ERL_NIF_INIT(Elixir.PortMidi.Nifs.Input,nif_funcs,load,NULL,NULL,NULL) 114 | -------------------------------------------------------------------------------- /src/portmidi_out.c: -------------------------------------------------------------------------------- 1 | #include "erl_nif.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define MAXBUFLEN 1024 8 | 9 | typedef enum {INPUT, OUTPUT} DeviceType; 10 | 11 | PmError findDevice(PmStream **stream, char *deviceName, DeviceType type, long latency); 12 | char* makePmErrorAtom(PmError); 13 | const PmDeviceInfo ** listDevices(int); 14 | void debug(char *str); 15 | 16 | int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) { 17 | ErlNifResourceFlags flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER; 18 | *priv_data = enif_open_resource_type(env, NULL, "stream_resource", NULL, ERL_NIF_RT_CREATE, NULL); 19 | 20 | return 0; 21 | } 22 | 23 | static ERL_NIF_TERM do_open(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { 24 | char deviceName[MAXBUFLEN]; 25 | long latency; 26 | ERL_NIF_TERM streamTerm; 27 | PortMidiStream **streamAlloc; 28 | PmError result; 29 | 30 | ErlNifResourceType* streamType = (ErlNifResourceType*)enif_priv_data(env); 31 | streamAlloc = (PortMidiStream**)enif_alloc_resource(streamType, sizeof(PortMidiStream*)); 32 | 33 | enif_get_string(env, argv[0], deviceName, MAXBUFLEN, ERL_NIF_LATIN1); 34 | enif_get_long(env, argv[1], &latency); 35 | 36 | if((result = findDevice(streamAlloc, deviceName, OUTPUT, latency)) != pmNoError) { 37 | ERL_NIF_TERM reason = enif_make_atom(env, makePmErrorAtom(result)); 38 | return enif_make_tuple2(env, enif_make_atom(env, "error"), reason); 39 | } 40 | 41 | streamTerm = enif_make_resource(env, streamAlloc); 42 | enif_keep_resource(streamAlloc); 43 | 44 | return enif_make_tuple2(env, enif_make_atom(env, "ok"), streamTerm); 45 | } 46 | 47 | static ERL_NIF_TERM do_write(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { 48 | static PortMidiStream ** stream; 49 | 50 | ErlNifResourceType* streamType = (ErlNifResourceType*)enif_priv_data(env); 51 | if(!enif_get_resource(env, argv[0], streamType, (PortMidiStream **) &stream)) { 52 | return enif_make_badarg(env); 53 | } 54 | 55 | ERL_NIF_TERM erlMessages = argv[1]; 56 | const ERL_NIF_TERM * erlEvent; 57 | const ERL_NIF_TERM * erlMessage; 58 | ERL_NIF_TERM erlTuple; 59 | 60 | unsigned int numOfMessages; 61 | int tupleSize; 62 | enif_get_list_length(env, erlMessages, &numOfMessages); 63 | 64 | PmEvent events[numOfMessages]; 65 | long int status, note, velocity, timestamp; 66 | 67 | for(unsigned int i = 0; i < numOfMessages; i++) { 68 | enif_get_list_cell(env, erlMessages, &erlTuple, &erlMessages); 69 | enif_get_tuple(env, erlTuple, &tupleSize, &erlEvent); 70 | 71 | enif_get_tuple(env, erlEvent[0], &tupleSize, &erlMessage); 72 | enif_get_long(env, erlMessage[0], &status); 73 | enif_get_long(env, erlMessage[1], ¬e); 74 | enif_get_long(env, erlMessage[2], &velocity); 75 | 76 | enif_get_long(env, erlEvent[1], ×tamp); 77 | 78 | PmEvent event; 79 | event.message = Pm_Message(status, note, velocity); 80 | event.timestamp = timestamp; 81 | 82 | events[i] = event; 83 | } 84 | 85 | PmError writeError; 86 | writeError = Pm_Write(*stream, events, numOfMessages); 87 | 88 | if (writeError == pmNoError) { 89 | return enif_make_atom(env, "ok"); 90 | } 91 | 92 | const char * writeErrorMsg; 93 | writeErrorMsg = Pm_GetErrorText(writeError); 94 | 95 | return enif_make_tuple2( 96 | env, 97 | enif_make_atom(env, "error"), 98 | enif_make_string(env, writeErrorMsg, ERL_NIF_LATIN1) 99 | ); 100 | } 101 | 102 | static ERL_NIF_TERM do_close(ErlNifEnv* env, int arc, const ERL_NIF_TERM argv[]) { 103 | static PortMidiStream ** stream; 104 | 105 | ErlNifResourceType* streamType = (ErlNifResourceType*)enif_priv_data(env); 106 | if(!enif_get_resource(env, argv[0], streamType, (PortMidiStream **) &stream)) { 107 | return enif_make_badarg(env); 108 | } 109 | 110 | Pm_Close(*stream); 111 | 112 | return enif_make_atom(env, "ok"); 113 | } 114 | 115 | static ErlNifFunc nif_funcs[] = { 116 | {"do_open", 2, do_open}, 117 | {"do_write", 2, do_write}, 118 | {"do_close", 1, do_close} 119 | }; 120 | 121 | ERL_NIF_INIT(Elixir.PortMidi.Nifs.Output,nif_funcs,load,NULL,NULL,NULL) 122 | -------------------------------------------------------------------------------- /src/portmidi_shared.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define MAXBUFLEN 1024 7 | 8 | typedef enum {INPUT, OUTPUT} DeviceType; 9 | 10 | char* makePmErrorAtom(PmError errnum); 11 | PmError findDevice(PortMidiStream **stream, char *deviceName, DeviceType type, long latency); 12 | const PmDeviceInfo ** listDevices(int); 13 | void debug(char *str); 14 | 15 | PmError findDevice(PortMidiStream **stream, char *deviceName, DeviceType type, long latency) { 16 | PmError result = pmInvalidDeviceId; 17 | const PmDeviceInfo *deviceInfo; 18 | 19 | int i = 0; 20 | while((deviceInfo = Pm_GetDeviceInfo(i)) != NULL) { 21 | int nameCompare = strcmp(deviceInfo->name, deviceName); 22 | 23 | if(nameCompare == 0 ) { 24 | if(type == INPUT && deviceInfo->input == 1) { 25 | result = Pm_OpenInput(stream, i, NULL, 0, NULL, NULL); 26 | break; 27 | } 28 | if(type == OUTPUT && deviceInfo->output == 1) { 29 | result = Pm_OpenOutput(stream, i, NULL, 0, NULL, NULL, latency); 30 | break; 31 | } 32 | } 33 | 34 | i++; 35 | } 36 | 37 | return result; 38 | } 39 | 40 | const PmDeviceInfo ** listDevices(int numOfDevices) { 41 | int i = 0; 42 | static const PmDeviceInfo * devices[MAXBUFLEN]; 43 | 44 | for(i = 0; i < numOfDevices; i++) { devices[i] = Pm_GetDeviceInfo(i); } 45 | return devices; 46 | } 47 | 48 | char* makePmErrorAtom(PmError errnum) { 49 | char* atom; 50 | switch(errnum) { 51 | case pmNoError: 52 | atom = ""; 53 | break; 54 | case pmHostError: 55 | atom = "host_error"; 56 | break; 57 | case pmInvalidDeviceId: 58 | atom = "invalid_device_id"; 59 | break; 60 | case pmInsufficientMemory: 61 | atom = "out_of_memory"; 62 | break; 63 | case pmBufferTooSmall: 64 | atom = "buffer_too_small"; 65 | break; 66 | case pmBadPtr: 67 | atom = "bad_pointer"; 68 | break; 69 | case pmInternalError: 70 | atom = "internal_portmidi_error"; 71 | break; 72 | case pmBufferOverflow: 73 | atom = "buffer_overflow"; 74 | break; 75 | case pmBadData: 76 | atom = "invalid_midi_message"; 77 | break; 78 | case pmBufferMaxSize: 79 | atom = "buffer_max_size"; 80 | break; 81 | default: 82 | atom = "illegal_error_number"; 83 | break; 84 | } 85 | return atom; 86 | } 87 | 88 | void debug(char* str) { 89 | fprintf(stderr, "%s\n", str); 90 | } 91 | -------------------------------------------------------------------------------- /test/port_midi_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PortMidiTest do 2 | use ExUnit.Case 3 | doctest PortMidi 4 | end 5 | -------------------------------------------------------------------------------- /test/portmidi/devices_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PortMidiDevicesTest do 2 | import PortMidi.Devices, only: [list: 0] 3 | alias PortMidi.Device 4 | 5 | use ExUnit.Case, async: false 6 | import Mock 7 | 8 | @mock_nif_devices %{ 9 | input: [%{name: 'Launchpad Mini', interf: 'CoreMIDI', input: 1, output: 0, opened: 0}], 10 | output: [%{name: 'Launchpad Mini', interf: 'CoreMIDI', input: 0, output: 1, opened: 0}] 11 | } 12 | 13 | test_with_mock "list returns a map of devices", PortMidi.Nifs.Devices, [do_list: fn -> @mock_nif_devices end] do 14 | assert list == %{ 15 | input: [%Device{name: "Launchpad Mini", interf: "CoreMIDI", input: 1, output: 0, opened: 0}], 16 | output: [%Device{name: "Launchpad Mini", interf: "CoreMIDI", input: 0, output: 1, opened: 0}] 17 | } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/portmidi/input/server_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PortMidiInputServerTest do 2 | alias PortMidi.Input.Reader 3 | alias PortMidi.Listeners 4 | import PortMidi.Input.Server 5 | 6 | use ExUnit.Case, async: false 7 | import Mock 8 | 9 | test "new_messages/2 broadcasts to processes in Listeners" do 10 | {:ok, input} = Agent.start(fn -> [] end) 11 | Listeners.register(input, self) 12 | 13 | Agent.get input, fn(_) -> 14 | handle_cast({:new_messages, %{message: {176, 0, 127}, timestamp: 0}}, nil) 15 | end 16 | 17 | assert_received {input, %{message: {176, 0, 127}, timestamp: 0}} 18 | end 19 | 20 | test "terminating the server calls close on the reader" do 21 | test_pid = self 22 | reader_mock = [start_link: fn(_pid, _device_name) -> {:ok, test_pid} end, 23 | listen: fn(_reader) -> :ok end, 24 | stop: fn(_reader) -> :ok end] 25 | 26 | with_mock Reader, reader_mock do 27 | {:ok, server} = start_link("Launchpad Mini") 28 | stop(server) 29 | 30 | assert called Reader.stop(test_pid) 31 | end 32 | end 33 | 34 | test "an error from Reader on open stops the server" do 35 | reader_mock = [start_link: fn(_pid, _device_name) -> 36 | {:error, :invalid_device_id} 37 | end] 38 | 39 | with_mock Reader, reader_mock do 40 | assert {:stop, :invalid_device_id} = init("Launchpad Mini") 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/portmidi/listeners_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PortMidiListenersTest do 2 | alias PortMidi.Listeners 3 | use ExUnit.Case, async: true 4 | 5 | setup do 6 | {:ok, input} = Agent.start(fn -> nil end) 7 | {:ok, input: input} 8 | end 9 | 10 | test "creates an empty map of listeners", %{input: input} do 11 | assert Listeners.list(input) == {:error, :input_not_found} 12 | end 13 | 14 | test "adds pids when registering a process", %{input: input} do 15 | {:ok, listener} = Agent.start(fn -> nil end) 16 | 17 | Listeners.register(input, listener) 18 | assert listener in Listeners.list(input) 19 | end 20 | 21 | test "removes listeners when down", %{input: input} do 22 | {:ok, listener} = Agent.start(fn -> nil end) 23 | Listeners.register(input, listener) 24 | 25 | assert listener in Listeners.list(input) 26 | 27 | Agent.stop(listener) 28 | refute listener in Listeners.list(input) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------