├── .gitignore ├── LICENSE ├── README.md ├── Song.xrns ├── ex-portmidi.png ├── mix.exs ├── mix.lock ├── music.exs ├── portmidi.png ├── presentation.md ├── reloading.exs └── renoise.png /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thibaut Barrère 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The code in this repository demonstrates how to use Elixir "hot-reloading" 2 | feature, together with MIDI events generation. 3 | 4 | Youtube link: 5 | 6 | * https://www.youtube.com/watch?v=_VgcUatTilU&feature=youtu.be&t=2m2s 7 | 8 | Slides: 9 | 10 | * https://speakerdeck.com/thbar/elixir-hot-reloading-and-midi-events-generation 11 | 12 | In short: 13 | 14 | * `reloading.exs` monitors the file system and hot-reloads `music.exs` 15 | * `music.exs`: 16 | * relies on `GenServer` to ensure the "midi player" will keep state between code reloads 17 | * creates a "tick" every 50 milliseconds 18 | * uses `portmidi` to send MIDI events to [Renoise](https://www.renoise.com) 19 | 20 | ### How to use? 21 | 22 | * Install [Renoise](https://www.renoise.com) demo and load `Song.xrns` 23 | * Run the code: 24 | 25 | ``` 26 | brew install portmidi 27 | mix deps.get 28 | mix run --no-halt reloading.exs 29 | ``` 30 | 31 | * Edit `music.exs` to play around 32 | -------------------------------------------------------------------------------- /Song.xrns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thbar/demo-elixir-reloading-music/b8bb1853623bea62bca288f3e4f064f20511bb66/Song.xrns -------------------------------------------------------------------------------- /ex-portmidi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thbar/demo-elixir-reloading-music/b8bb1853623bea62bca288f3e4f064f20511bb66/ex-portmidi.png -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Reloading.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :reloading, 6 | version: "1.0.0", 7 | elixir: "~> 1.4", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | def application do 14 | [applications: [:logger, :exfswatch, :portmidi]] 15 | end 16 | 17 | defp deps do 18 | [ 19 | # File-system watching 20 | {:exfswatch, "~> 0.4.1"}, 21 | # MIDI communication 22 | {:portmidi, "~> 5.1.1"}, 23 | # Pretty-printing 24 | {:apex, "~> 1.0.0"} 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"apex": {:hex, :apex, "1.0.0", "abf230314d35ca4c48a902f693247f190ad42fc14862b9c4f7dbb7077b21c20a", [:mix], []}, 2 | "exfswatch": {:hex, :exfswatch, "0.4.1", "008db817f6096eec69d17c7ca562c572fb4409728a09f465dea7e31190fb983c", [:mix], []}, 3 | "portmidi": {:hex, :portmidi, "5.1.1", "7b41e69c5d6364d143dc3663080750127fc0497d2bbe2d1690b7703d7e639482", [:make, :mix], []}} 4 | -------------------------------------------------------------------------------- /music.exs: -------------------------------------------------------------------------------- 1 | defmodule Midi do 2 | use GenServer 3 | require Logger 4 | 5 | def init(args) do 6 | {:ok, args} 7 | end 8 | 9 | def start_link do 10 | # Logger.info "Available devices: #{inspect PortMidi.devices}" 11 | {:ok, device} = PortMidi.open(:output, "Renoise MIDI-In") 12 | tick_period = 50 13 | Process.send_after(:midi, {:tick}, tick_period) 14 | GenServer.start_link(__MODULE__, %{ 15 | current_tick: -1, 16 | # We could write down :os.system_time(:milli_seconds) later 17 | # to dynamically recompute ticks based on elapsed time 18 | # and correct for drift 19 | device: device, 20 | tick_period: tick_period 21 | }, name: :midi) 22 | end 23 | 24 | def handle_info({:tick}, state) do 25 | # Immediately reschedule the next tick to reduce drift 26 | Process.send_after(:midi, {:tick}, state.tick_period) 27 | current_tick = Map.fetch!(state, :current_tick) + 1 28 | 29 | show_visual_feedback(current_tick) 30 | 31 | play_notes(state.device, current_tick) 32 | 33 | {:noreply, %{state | current_tick: current_tick}} 34 | end 35 | 36 | def handle_info({:note_off, note}, state) do 37 | PortMidi.write(state.device, {0x90, note, 0}) 38 | {:noreply, state} 39 | end 40 | 41 | def show_visual_feedback(current_tick) do 42 | if rem(current_tick, 64) == 0 do 43 | IO.write IO.ANSI.clear <> IO.ANSI.home 44 | end 45 | if rem(current_tick, 8) == 0 do 46 | IO.write IO.ANSI.yellow <> to_string(1 + round(rem(current_tick, 64) / 8)) <> IO.ANSI.default_color 47 | end 48 | end 49 | 50 | def play_notes(device, current_tick) do 51 | notes = [0x54] 52 | delay = 16 53 | # notes = [0x54, 0x57, 0x5B, 0x60] 54 | # delay = 4 55 | volume = 00 56 | increase = 0 57 | 58 | notes = notes ++ Enum.reverse(notes) 59 | if rem(current_tick, delay) == 0 do 60 | index = rem(div(current_tick, delay), Enum.count(notes)) 61 | note = Enum.at(notes, index) + increase 62 | PortMidi.write(device, {0x90, note, volume}) 63 | Process.send_after(:midi, {:note_off, note}, 50 * 2) 64 | end 65 | end 66 | end -------------------------------------------------------------------------------- /portmidi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thbar/demo-elixir-reloading-music/b8bb1853623bea62bca288f3e4f064f20511bb66/portmidi.png -------------------------------------------------------------------------------- /presentation.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ## Elixir Hot-Reloading & MIDI notes generation 4 | 5 | https://github.com/thbar/demo-elixir-reloading-music 6 | 7 | --- 8 | 9 | ### How to implement a very simple Elixir code hot-reloading example? 10 | 11 | --- 12 | 13 | ### How to make it interesting? 14 | 15 | --- 16 | 17 | ### Live sound events generation :smile: 18 | 19 | --- 20 | 21 | ### How to generate one note? 22 | 23 | --- 24 | 25 | # Renoise (music production system) 26 | 27 | ![inline](renoise.png) 28 | 29 | --- 30 | 31 | # PortMidi (C library) 32 | 33 | ![inline](portmidi.png) 34 | 35 | --- 36 | 37 | # Elixir bindings for PortMidi 38 | 39 | ![inline](ex-portmidi.png) 40 | 41 | --- 42 | 43 | ```elixir 44 | # Start a process for MIDI event queue 45 | {:ok, pid} = PortMidi.open(:output, "Renoise MIDI-In") 46 | 47 | note = 48 # C-4 48 | velocity = 127 49 | 50 | # Send "NOTE ON" 51 | PortMidi.write(pid, {0x90, note, velocity}) 52 | 53 | # Send "NOTE OFF" 54 | PortMidi.write(pid, {0x80, note}) 55 | ``` 56 | 57 | --- 58 | 59 | ### How to build a music loop? 60 | 61 | --- 62 | 63 | * `GenServer` 64 | * `Process.send_after(xxx)` 65 | 66 | --- 67 | 68 | ```elixir 69 | defmodule MidiPlayer do 70 | use GenServer 71 | 72 | def start_link do 73 | {:ok, device} = PortMidi.open(:output, "Renoise MIDI-In") 74 | tick_period = 50 75 | Process.send_after(:midi, {:tick}, tick_period) 76 | GenServer.start_link(__MODULE__, %{ 77 | current_tick: -1, 78 | device: device, 79 | tick_period: tick_period 80 | }, name: :midi) 81 | end 82 | # SNIP 83 | end 84 | ``` 85 | 86 | --- 87 | 88 | ```elixir 89 | defmodule MidiPlayer do 90 | def handle_info({:tick}, state) do 91 | Process.send_after(:midi, {:tick}, state.tick_period) 92 | current_tick = Map.fetch!(state, :current_tick) + 1 93 | 94 | show_visual_feedback(current_tick) 95 | play_notes(state.device, current_tick) 96 | 97 | {:noreply, %{state | current_tick: current_tick}} 98 | end 99 | end 100 | ``` 101 | 102 | --- 103 | 104 | ```elixir 105 | def play_notes(device, current_tick) do 106 | notes = [0x54, 0x57, 0x5B, 0x60] 107 | delay = 4 108 | 109 | if rem(current_tick, delay) == 0 do 110 | index = rem(div(current_tick, delay), Enum.count(notes)) 111 | note = Enum.at(notes, index) 112 | PortMidi.write(device, {0x90, note, volume}) 113 | Process.send_after(:midi, {:note_off, note}, 50 * 2) 114 | end 115 | end 116 | ``` 117 | 118 | --- 119 | 120 | ### I CAN HAZ RELOADING? 121 | 122 | ```elixir 123 | Code.eval_file("music.exs") 124 | ``` 125 | 126 | --- 127 | 128 | ### How to react to file change? 129 | 130 | ```elixir 131 | defmodule Monitor do 132 | use ExFSWatch, 133 | dirs: ["music.exs"], 134 | listener_extra_args: "--latency=0.0" 135 | 136 | def callback(_file_path, _events) do 137 | Code.eval_file("music.exs") 138 | end 139 | end 140 | 141 | Monitor.start 142 | ``` 143 | 144 | --- 145 | 146 | ### AHA MOMENT 147 | 148 | GenServer reloading **keeps the state across reloads**. 149 | 150 | => We can keep the "current music tick" between reloads. 151 | 152 | --- 153 | 154 | ``` 155 | +-----------------+ +------------------------+ 156 | | (reloable) code | + | preserved state (tick) | 157 | +-----------------+ +------------------------+ 158 | 159 | | | 160 | \ / 161 | 162 | +---------------------+ +----------+ +---------+ 163 | | ex-portmidi process | -> | portmidi | -> | renoise | 164 | +---------------------+ +----------+ +---------+ 165 | ``` 166 | 167 | --- 168 | 169 | ### DEMO -------------------------------------------------------------------------------- /reloading.exs: -------------------------------------------------------------------------------- 1 | # This avoids "warning: redefining module Midi" which occurs on reload 2 | Code.compiler_options(ignore_module_conflict: true) 3 | 4 | defmodule Monitor do 5 | use ExFSWatch, dirs: ["music.exs"], listener_extra_args: "--latency=0.0" 6 | 7 | def callback(_file_path, _events) do 8 | reload() 9 | end 10 | 11 | def reload do 12 | Code.eval_file("music.exs") 13 | end 14 | end 15 | 16 | # Force a first load at startup 17 | Monitor.reload 18 | 19 | # Make monitor watch the filesystem 20 | Monitor.start 21 | 22 | Midi.start_link -------------------------------------------------------------------------------- /renoise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thbar/demo-elixir-reloading-music/b8bb1853623bea62bca288f3e4f064f20511bb66/renoise.png --------------------------------------------------------------------------------