From 39f5f592a7f08a8d2da94475dcb5a5884ff5683a Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Sat, 10 Oct 2020 19:36:07 +0200 Subject: [PATCH] Init repository. --- .formatter.exs | 4 +++ .gitignore | 24 +++++++++++++ README.md | 21 ++++++++++++ lib/midi_tools.ex | 18 ++++++++++ lib/midi_tools/player.ex | 73 ++++++++++++++++++++++++++++++++++++++++ mix.exs | 26 ++++++++++++++ mix.lock | 16 +++++++++ test/midi_tools_test.exs | 8 +++++ test/test_helper.exs | 1 + 9 files changed, 191 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/midi_tools.ex create mode 100644 lib/midi_tools/player.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/midi_tools_test.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ccf3631 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +midi_tools-*.tar + diff --git a/README.md b/README.md new file mode 100644 index 0000000..87a55f5 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# MIDITools + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `midi_tools` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:midi_tools, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/midi_tools](https://hexdocs.pm/midi_tools). + diff --git a/lib/midi_tools.ex b/lib/midi_tools.ex new file mode 100644 index 0000000..19cf4a0 --- /dev/null +++ b/lib/midi_tools.ex @@ -0,0 +1,18 @@ +defmodule MIDITools do + @moduledoc """ + Documentation for `MIDITools`. + """ + + @doc """ + Hello world. + + ## Examples + + iex> MIDITools.hello() + :world + + """ + def hello do + :world + end +end diff --git a/lib/midi_tools/player.ex b/lib/midi_tools/player.ex new file mode 100644 index 0000000..366ce77 --- /dev/null +++ b/lib/midi_tools/player.ex @@ -0,0 +1,73 @@ +defmodule Player do + use GenServer + + # Client API + + def start_link(arg \\ nil) do + GenServer.start_link(__MODULE__, arg, name: __MODULE__) + end + + def set_schedule(schedule) do + GenServer.call(__MODULE__, {:set_schedule, schedule}) + end + + def play do + GenServer.call(__MODULE__, :play) + end + + # Server callbacks + + @impl true + def init(_arg) do + {:ok, synth} = MIDISynth.start_link([]) + epoch = Timex.epoch() |> Timex.to_datetime() + {:ok, %{timer: nil, schedule: [], schedule_left: [], start_time: epoch, synth: synth}} + end + + @impl true + def handle_call({:set_schedule, schedule}, _from, state) do + {:reply, :ok, %{state | schedule: schedule, schedule_left: schedule}} + end + + def handle_call(:play, _from, %{schedule_left: schedule_left} = state) do + start_time = Timex.now() + state = %{state | start_time: start_time} + timer = start_timer(schedule_left, start_time) + {:reply, :ok, %{state | timer: timer}} + end + + @impl true + def handle_info( + :play, + %{schedule_left: schedule_left, start_time: start_time, synth: synth} = state + ) do + schedule_left = play_next_midi(schedule_left, start_time, synth) + {:noreply, %{state | schedule_left: schedule_left}} + end + + defp play_next_midi([], _start_time, _synth), do: [] + + defp play_next_midi([{offset, command} | next_schedule] = schedule_left, start_time, synth) do + next_time = DateTime.add(start_time, offset, :millisecond) + micro_diff = Timex.diff(next_time, Timex.now(), :microsecond) + + if micro_diff < 500 do + # Play command, and try to play next command too. + MIDISynth.midi(synth, command) + play_next_midi(next_schedule, start_time, synth) + else + # Command is too far in the future, schedule next timer. + delay = ceil(micro_diff / 1000) + Process.send_after(self(), :play, delay) + schedule_left + end + end + + defp start_timer([], _start_time), do: nil + + defp start_timer([{offset, _command} | _], start_time) do + next_time = DateTime.add(start_time, offset, :millisecond) + delay = Timex.diff(next_time, Timex.now(), :millisecond) + Process.send_after(self(), :play, delay) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..deeb6a6 --- /dev/null +++ b/mix.exs @@ -0,0 +1,26 @@ +defmodule MIDITools.MixProject do + use Mix.Project + + def project do + [ + app: :midi_tools, + version: "0.1.0", + elixir: "~> 1.10", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:midi_synth, "~> 0.4.0"}, + {:timex, "~> 3.6"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..303c960 --- /dev/null +++ b/mix.lock @@ -0,0 +1,16 @@ +%{ + "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, + "elixir_make": {:hex, :elixir_make, "0.6.1", "8faa29a5597faba999aeeb72bbb9c91694ef8068f0131192fb199f98d32994ef", [:mix], [], "hexpm", "35d33270680f8d839a4003c3e9f43afb595310a592405a00afc12de4c7f55a18"}, + "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"}, + "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, + "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "midi_synth": {:hex, :midi_synth, "0.4.0", "58e88aee3fdaf4f60e99847fdbf8921576dce5820966f0d58a4c0f4a96198740", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "789152ba66603e5c358c7f6971ceac6f014e7605e2367cc6be7a9af36fc2cc70"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"}, + "tzdata": {:hex, :tzdata, "1.0.4", "a3baa4709ea8dba552dca165af6ae97c624a2d6ac14bd265165eaa8e8af94af6", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "b02637db3df1fd66dd2d3c4f194a81633d0e4b44308d36c1b2fdfd1e4e6f169b"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, +} diff --git a/test/midi_tools_test.exs b/test/midi_tools_test.exs new file mode 100644 index 0000000..3687866 --- /dev/null +++ b/test/midi_tools_test.exs @@ -0,0 +1,8 @@ +defmodule MIDIToolsTest do + use ExUnit.Case + doctest MIDITools + + test "greets the world" do + assert MIDITools.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()