├── .gitignore ├── README.md ├── config └── config.exs ├── examples ├── beats.exs ├── binaural.exs ├── filter.exs ├── morse.exs ├── morse_decoder.exs ├── player.exs ├── sequencer.exs └── sine.exs ├── lib ├── synthex.ex └── synthex │ ├── adsr.ex │ ├── context.ex │ ├── file │ └── wav_header.ex │ ├── filter │ ├── biquad.ex │ ├── bitcrusher.ex │ ├── low_high_pass.ex │ └── moog.ex │ ├── generator │ ├── noise.ex │ └── oscillator.ex │ ├── input │ └── wav_reader.ex │ ├── math.ex │ ├── output │ ├── sox_player.ex │ ├── wav_writer.ex │ └── writer.ex │ ├── sequencer.ex │ └── sequencer │ ├── morse.ex │ └── simple_string_format.ex ├── mix.exs └── test ├── synthex_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | cover 3 | deps 4 | erl_crash.dump 5 | *.ez 6 | .idea/** 7 | *.iml 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Synthex 2 | 3 | A signal synthesis library. Currently implements basic oscillators and a few filters. Output to WAV file and direct audio output (requires SoX) are supported. 4 | 5 | ## Installation 6 | 7 | The package can be installed as: 8 | 9 | 1. Add synthex to your list of dependencies in `mix.exs`: 10 | ```elixir 11 | def deps do 12 | [{:synthex, "~> 0.0.1"}] 13 | end 14 | ``` 15 | 16 | 2. Ensure synthex is started before your application: 17 | ```elixir 18 | def application do 19 | [applications: [:synthex]] 20 | end 21 | ``` 22 | 23 | ## How to use 24 | 25 | Please take a look at the examples to see how to use it. Development is at a very early stage now, so things may change and break between releases 26 | 27 | ## TODO 28 | * More filters (FIR) - coming soon 29 | * Tests 30 | * Documentation 31 | -------------------------------------------------------------------------------- /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 | # You can configure for your application as: 12 | # 13 | # config :synthex, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:synthex, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /examples/beats.exs: -------------------------------------------------------------------------------- 1 | defmodule Beats do 2 | alias Synthex.Context 3 | alias Synthex.Output.SoxPlayer 4 | alias Synthex.Generator.Oscillator 5 | alias Synthex.Generator.Noise 6 | use Synthex.Math 7 | 8 | @rate 44100 9 | 10 | def run(duration) do 11 | {:ok, writer} = SoxPlayer.open(rate: @rate, channels: 1) 12 | 13 | context = 14 | %Context{output: writer, rate: @rate} 15 | |> Context.put_element(:main, :osc1, %Oscillator{algorithm: :sawtooth, frequency: 1, sync_phase: @pi}) 16 | |> Context.put_element(:main, :osc2, %Oscillator{algorithm: :pulse, frequency: 0.5, center: duty_cycle_to_radians(0.75)}) 17 | |> Context.put_element(:main, :osc3, %Oscillator{algorithm: :triangle, frequency: 0.02}) 18 | |> Context.put_element(:main, :noise, %Noise{type: :brown}) 19 | 20 | Synthex.synthesize(context, duration, fn (ctx) -> 21 | {ctx, osc3} = Context.get_sample(ctx, :main, :osc3) 22 | 23 | {ctx, osc1} = Context.get_sample(ctx, :main, :osc1, %{frequency: amplitude_to_frequency(osc3, 0.8, 4)}) 24 | {ctx, osc2} = Context.get_sample(ctx, :main, :osc2, %{frequency: amplitude_to_frequency(osc3, 0.4, 2)}) 25 | {ctx, noise} = Context.get_sample(ctx, :main, :noise) 26 | 27 | {ctx, osc1 * noise * shift_by(osc2, 1)} 28 | end) 29 | SoxPlayer.close(writer) 30 | end 31 | end 32 | 33 | Beats.run(20) -------------------------------------------------------------------------------- /examples/binaural.exs: -------------------------------------------------------------------------------- 1 | defmodule Binaural do 2 | alias Synthex.Context 3 | alias Synthex.Output.SoxPlayer 4 | alias Synthex.Generator.Oscillator 5 | alias Synthex.Generator.Noise 6 | use Synthex.Math 7 | 8 | @rate 44100 9 | @carrier 300 10 | @start_freq @carrier + 20 11 | @end_freq @carrier + 6 12 | @rampdown_duration 600 13 | @rampdown_freq 1/@rampdown_duration 14 | @sustain_duration 1200 15 | 16 | def run() do 17 | {:ok, writer} = SoxPlayer.open(rate: @rate, channels: 2) 18 | 19 | %Context{output: writer, rate: @rate} 20 | |> Context.put_element(:carrier, :osc, %Oscillator{algorithm: :sine, frequency: @carrier}) 21 | |> Context.put_element(:signal, :osc, %Oscillator{algorithm: :sine}) 22 | |> Context.put_element(:signal, :freq, %Oscillator{algorithm: :reverse_sawtooth, frequency: @rampdown_freq}) 23 | |> Context.put_element(:background, :noise, %Noise{type: :brown}) 24 | |> Context.put_element(:background, :lfo, %Oscillator{algorithm: :sawtooth, frequency: 0.2, phase: @pi}) 25 | |> rampdown() 26 | |> sustain() 27 | 28 | SoxPlayer.close(writer) 29 | end 30 | 31 | defp rampdown(context) do 32 | synth_phase(context, @rampdown_duration, fn (ctx) -> 33 | {ctx, freq} = Context.get_sample(ctx, :signal, :freq) 34 | Context.get_sample(ctx, :signal, :osc, %{frequency: amplitude_to_frequency(freq, @end_freq, @start_freq)}) 35 | end) 36 | end 37 | 38 | defp sustain(context) do 39 | synth_phase(context, @rampdown_duration, fn (ctx) -> Context.get_sample(ctx, :signal, :osc) end) 40 | end 41 | 42 | defp mix(front, back), do: (front * 0.90) + (back * 0.10) 43 | 44 | defp get_background(ctx) do 45 | {ctx, noise} = Context.get_sample(ctx, :background, :noise) 46 | {ctx, lfo} = Context.get_sample(ctx, :background, :lfo) 47 | {ctx, (noise * lfo)} 48 | end 49 | 50 | defp synth_phase(context, duration, signal_func) do 51 | Synthex.synthesize(context, duration, fn (ctx) -> 52 | {ctx, signal} = signal_func.(ctx) 53 | {ctx, carrier} = Context.get_sample(ctx, :carrier, :osc) 54 | {ctx, background} = get_background(ctx) 55 | 56 | {ctx, [mix(carrier, background), mix(signal, background)]} 57 | end) 58 | end 59 | end 60 | 61 | Binaural.run() -------------------------------------------------------------------------------- /examples/filter.exs: -------------------------------------------------------------------------------- 1 | defmodule Filter do 2 | alias Synthex.Context 3 | alias Synthex.Output.WavWriter 4 | alias Synthex.File.WavHeader 5 | alias Synthex.Generator.Oscillator 6 | alias Synthex.Filter.Moog 7 | use Synthex.Math 8 | 9 | def run(duration) do 10 | header = %WavHeader{channels: 1} 11 | {:ok, writer} = WavWriter.open(System.user_home() <> "/filter.wav", header) 12 | context = 13 | %Context{output: writer, rate: header.rate} 14 | |> Context.put_element(:main, :osc1, %Oscillator{algorithm: :sine}) 15 | |> Context.put_element(:main, :lfo, %Oscillator{algorithm: :triangle, frequency: 0.1}) 16 | |> Context.put_element(:main, :filter, %Moog{cutoff: 0.07, resonance: 3.2}) 17 | 18 | Synthex.synthesize(context, duration, fn (ctx) -> 19 | {ctx, lfo} = Context.get_sample(ctx, :main, :lfo) 20 | {ctx, osc1} = Context.get_sample(ctx, :main, :osc1, %{frequency: amplitude_to_frequency(lfo, 110, 1100)}) 21 | Context.get_sample(ctx, :main, :filter, %{sample: osc1}) 22 | end) 23 | 24 | WavWriter.close(writer) 25 | end 26 | end 27 | 28 | Filter.run(5) -------------------------------------------------------------------------------- /examples/morse.exs: -------------------------------------------------------------------------------- 1 | defmodule MorseEx do 2 | alias Synthex.Context 3 | alias Synthex.Output.WavWriter 4 | alias Synthex.File.WavHeader 5 | alias Synthex.Generator.Oscillator 6 | alias Synthex.Sequencer 7 | alias Synthex.Sequencer.Morse 8 | alias Synthex.ADSR 9 | 10 | use Synthex.Math 11 | 12 | def run() do 13 | header = %WavHeader{channels: 1, format: :float, sample_size: 32} 14 | {:ok, writer} = WavWriter.open(System.user_home() <> "/morse.wav", header) 15 | 16 | sequencer = Morse.from_text("Hello world", Morse.wpm_to_dot_duration(15)) 17 | duration = Sequencer.sequence_duration(sequencer) 18 | 19 | context = 20 | %Context{output: writer, rate: header.rate} 21 | |> Context.put_element(:main, :osc1, %Oscillator{algorithm: :sine}) 22 | |> Context.put_element(:main, :adsr, ADSR.adsr(header.rate, 1.0, 0.01, 0.000001, 0.01, 10, 10)) 23 | |> Context.put_element(:main, :sequencer, sequencer) 24 | 25 | Synthex.synthesize(context, duration, fn (ctx) -> 26 | {ctx, {freq, amp}} = Context.get_sample(ctx, :main, :sequencer) 27 | {ctx, osc1} = Context.get_sample(ctx, :main, :osc1, %{frequency: freq}) 28 | {ctx, adsr} = Context.get_sample(ctx, :main, :adsr, %{gate: ADSR.amplification_to_gate(amp)}) 29 | {ctx, osc1 * adsr} 30 | end) 31 | 32 | WavWriter.close(writer) 33 | :timer.sleep(100) 34 | end 35 | end 36 | 37 | MorseEx.run() -------------------------------------------------------------------------------- /examples/morse_decoder.exs: -------------------------------------------------------------------------------- 1 | defmodule MorseDecoder do 2 | alias Synthex.Input.WavReader 3 | alias Synthex.Sequencer.Morse 4 | 5 | use Synthex.Math 6 | 7 | @window_size_ms Morse.wpm_to_dot_duration(120) 8 | 9 | def run(path) do 10 | reader = WavReader.open(path, false) 11 | sample_count = WavReader.get_sample_count(reader) 12 | window_size = trunc(@window_size_ms * reader.header.rate) 13 | window_count = div(sample_count, window_size) 14 | 15 | {_reader, morse_string} = Enum.reduce(1..window_count, {reader, ""}, fn(_x, {reader, acc}) -> 16 | {reader, sum} = sum_window(reader, window_size) 17 | {reader, acc <> evaluate_window(sum, window_size)} 18 | end) 19 | 20 | morse_parts = morse_string |> String.strip(?.) |> split_at_transition() 21 | period_len = morse_parts |> Enum.reduce(0, fn(x, acc) -> 22 | case x do 23 | <> -> acc 24 | x -> max(byte_size(x), acc) 25 | end 26 | end) 27 | period_len = period_len / 3 28 | 29 | Enum.reduce(morse_parts, "", fn(s, acc) -> 30 | s_len = byte_size(s) 31 | char_s = binary_part(s, 0, 1) 32 | 33 | decoded = cond do 34 | s_len < (period_len * 0.5) -> "" 35 | s_len <= (period_len * 2) -> char_s 36 | s_len <= (period_len * 5) -> String.duplicate(char_s, 3) 37 | true -> String.duplicate(char_s, 7) 38 | end 39 | 40 | acc <> decoded 41 | end) 42 | |> Morse.morse_to_text() 43 | |> IO.puts 44 | end 45 | 46 | defp sum_window(reader, window_size) do 47 | Enum.reduce(1..window_size, {reader, 0.0}, fn(_x, {reader, sum}) -> 48 | {reader, [sample | _]} = WavReader.get_sample(nil, reader) 49 | {reader, sum + abs(sample)} 50 | end) 51 | end 52 | 53 | defp evaluate_window(sum, window_size) when sum >= (window_size * 0.45), do: "=" 54 | defp evaluate_window(_sum, _window_size), do: "." 55 | 56 | defp split_at_transition(string) do 57 | {_, strings} = Enum.reduce(to_char_list(string), {nil, []}, fn(c, {prev, list}) -> 58 | if prev == c do 59 | [h | rest] = list 60 | {c, [h <> <> | rest]} 61 | else 62 | {c, [<> | list]} 63 | end 64 | end) 65 | 66 | Enum.reverse(strings) 67 | end 68 | end 69 | 70 | MorseDecoder.run(hd(System.argv)) -------------------------------------------------------------------------------- /examples/player.exs: -------------------------------------------------------------------------------- 1 | defmodule Player do 2 | alias Synthex.Context 3 | alias Synthex.Input.WavReader 4 | alias Synthex.Output.SoxPlayer 5 | 6 | use Synthex.Math 7 | 8 | def run(path) do 9 | reader = WavReader.open(path, false) 10 | {:ok, writer} = SoxPlayer.open(rate: reader.header.rate, channels: reader.header.channels) 11 | context = 12 | %Context{output: writer, rate: reader.header.rate} 13 | |> Context.put_element(:main, :wav, reader) 14 | 15 | Synthex.synthesize(context, WavReader.get_duration(reader), fn (ctx) -> 16 | Context.get_sample(ctx, :main, :wav) 17 | end) 18 | 19 | SoxPlayer.close(writer) 20 | end 21 | end 22 | 23 | Player.run(hd(System.argv)) -------------------------------------------------------------------------------- /examples/sequencer.exs: -------------------------------------------------------------------------------- 1 | defmodule Sequencer do 2 | alias Synthex.Context 3 | alias Synthex.Output.SoxPlayer 4 | alias Synthex.Generator.Oscillator 5 | alias Synthex.Filter.Moog 6 | alias Synthex.Sequencer 7 | alias Synthex.ADSR 8 | 9 | use Synthex.Math 10 | 11 | @rate 44100 12 | 13 | @bpm 70 14 | @jingle_bells "|g4-e5-d5-c5-g4-|-g4-g4-e5-d5-c5-a4-|-a4-a4-f5-e5-d5-b4-|-f5-f5-e5-d5-e5-|g4-e5-d5-c5-g4-|-g4-g4-e5-d5-c5-a4-|-a4-a4-f5-e5-d5-b4-|-f5-g5-g5-g5-g5-a5-g5-f5-d5-c5-|e5-e5-e5-|-e5-e5-e5-|-e5-g5-c5-d5-e5-|-f5-f5-f5-f5-f5-|-e5-e5-e5-e5-|-d5-d5-e5-d5-g5|e5-e5-e5-|-e5-e5-e5-|-e5-g5-c5-d5-e5-|-f5-f5-f5-f5-f5-|-e5-e5-e5-e5-|-g5-f5-e5-d5-c5--|" 15 | @happy_birthday "|--a4--a4--b4--a4--d5--C5---a4--a4--b4--a4-e5--d5----a4--a4-a5--F5--d5--C5---b4--g5-g5--F5--d5--e5--d5--|" 16 | @v_lesu_rodilas_elochka "|--c4-a4-a4-g4-a4-f4-c4-c4-c4-a4-a4-A4-g4-c5>>---c5-d4-d4-b4-b4-a4-g4-f4-c4-a4-a4-g4-a4-f4>>---e4-d4-d4-b4-b4-a4-g4-f4-c4-a4-a4-g4-a4-f4>>---|" 17 | def run() do 18 | {:ok, writer} = SoxPlayer.open(rate: @rate, channels: 1) 19 | sequencer = Sequencer.from_simple_string(@v_lesu_rodilas_elochka, Sequencer.bpm_to_duration(@bpm, 4)) 20 | total_duration = Sequencer.sequence_duration(sequencer) 21 | 22 | context = 23 | %Context{output: writer, rate: @rate} 24 | |> Context.put_element(:main, :osc1, %Oscillator{algorithm: :pulse}) 25 | |> Context.put_element(:main, :osc1_1, %Oscillator{algorithm: :sawtooth}) 26 | |> Context.put_element(:main, :osc2, %Oscillator{algorithm: :pulse}) 27 | |> Context.put_element(:main, :osc2_1, %Oscillator{algorithm: :sawtooth}) 28 | |> Context.put_element(:main, :lfo, %Oscillator{algorithm: :triangle, frequency: 4}) 29 | |> Context.put_element(:main, :adsr, ADSR.adsr(@rate, 1.0, 0.4, 0.000001, 0.4, 10, 10)) 30 | |> Context.put_element(:main, :filter, %Moog{cutoff: 0.50, resonance: 1.1}) 31 | |> Context.put_element(:main, :sequencer, sequencer) 32 | 33 | Synthex.synthesize(context, total_duration, fn (ctx) -> 34 | {ctx, {freq, amp}} = Context.get_sample(ctx, :main, :sequencer) 35 | {ctx, lfo} = Context.get_sample(ctx, :main, :lfo) 36 | {ctx, osc1} = Context.get_sample(ctx, :main, :osc1, %{frequency: freq, center: @pi - (lfo * @pi/1.1)}) 37 | {ctx, osc1_1} = Context.get_sample(ctx, :main, :osc1_1, %{frequency: freq + (lfo * freq * 0.05)}) 38 | {ctx, osc2} = Context.get_sample(ctx, :main, :osc2, %{frequency: (freq + 3.5), center: @pi - (lfo * @pi/1.1)}) 39 | {ctx, osc2_1} = Context.get_sample(ctx, :main, :osc2_1, %{frequency: (freq + 3.5) + (lfo * freq * 0.05)}) 40 | {ctx, adsr} = Context.get_sample(ctx, :main, :adsr, %{gate: ADSR.amplification_to_gate(amp)}) 41 | mixed_sample = ((osc1 * 0.25) + (osc1_1 * 0.25) + (osc2 * 0.25) + (osc2_1 * 0.25)) * adsr 42 | Context.get_sample(ctx, :main, :filter, %{sample: mixed_sample}) 43 | end) 44 | SoxPlayer.close(writer) 45 | end 46 | end 47 | 48 | Sequencer.run() -------------------------------------------------------------------------------- /examples/sine.exs: -------------------------------------------------------------------------------- 1 | defmodule Sine do 2 | alias Synthex.Context 3 | alias Synthex.Output.SoxPlayer 4 | alias Synthex.Generator.Oscillator 5 | 6 | @rate 44100 7 | 8 | def run(duration, frequency) do 9 | {:ok, writer} = SoxPlayer.open(rate: @rate, channels: 2) 10 | 11 | context = 12 | %Context{output: writer, rate: @rate} 13 | |> Context.put_element(:main, :osc1, %Oscillator{algorithm: :sine, frequency: frequency}) 14 | 15 | Synthex.synthesize(context, duration, fn (ctx) -> 16 | Context.get_sample(ctx, :main, :osc1) 17 | end) 18 | 19 | SoxPlayer.close(writer) 20 | end 21 | end 22 | 23 | Sine.run(5, 440) 24 | -------------------------------------------------------------------------------- /lib/synthex.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex do 2 | use Synthex.Math 3 | alias Synthex.Context 4 | 5 | def synthesize(ctx = %Context{rate: rate, time: t}, duration, func) do 6 | sample_count = t + duration_in_secs_to_sample_count(duration, rate) 7 | do_synthesize(ctx, sample_count, func) 8 | end 9 | 10 | defp do_synthesize(ctx = %Context{time: sample_count}, sample_count, _func), do: ctx 11 | defp do_synthesize(ctx = %Context{output: writer, time: t}, sample_count, func) do 12 | {ctx, samples} = func.(ctx) 13 | clamped_samples = clamp_all(samples) 14 | Synthex.Output.Writer.write_samples(writer, clamped_samples) 15 | 16 | ctx |> Map.put(:time, t + 1) |> do_synthesize(sample_count, func) 17 | end 18 | 19 | defp clamp_all(samples) when is_list(samples), do: Enum.map(samples, fn(sample) -> clamp(sample) end) 20 | defp clamp_all(sample), do: clamp(sample) 21 | end 22 | -------------------------------------------------------------------------------- /lib/synthex/adsr.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.ADSR do 2 | alias Synthex.ADSR 3 | 4 | defstruct [gate: :off, state: :idle, out: 0.0, prev_gate: :off, sustain_level: 1.0, attack_base: 0.0, attack_coefficient: 0.0, decay_base: 0.0, decay_coefficient: 0.0, release_base: 0.0, release_coefficient: 0.0] 5 | 6 | def get_sample(_ctx, adsr = %ADSR{gate: gate, state: state, prev_gate: prev_gate}) do 7 | new_state = get_transition(prev_gate, gate, state) 8 | {next_state, out} = process(new_state, adsr) 9 | {%ADSR{adsr | state: next_state, out: out, prev_gate: gate}, out} 10 | end 11 | 12 | defp process(:idle, _adsr), do: {:idle, 0.0} 13 | defp process(:attack, %ADSR{attack_base: attack_base, attack_coefficient: attack_coefficient, out: out}) do 14 | new_out = attack_base + (out * attack_coefficient) 15 | process_attack(new_out) 16 | end 17 | defp process(:decay, %ADSR{decay_base: decay_base, decay_coefficient: decay_coefficient, out: out, sustain_level: sustain_level}) do 18 | new_out = decay_base + (out * decay_coefficient) 19 | process_decay(new_out, sustain_level) 20 | end 21 | defp process(:sustain, %ADSR{sustain_level: sustain_level}), do: {:sustain, sustain_level} 22 | defp process(:release, %ADSR{release_base: release_base, release_coefficient: release_coefficient, out: out}) do 23 | new_out = release_base + (out * release_coefficient) 24 | process_release(new_out) 25 | end 26 | 27 | defp process_attack(out) when out >= 1.0, do: {:decay, 1.0} 28 | defp process_attack(out), do: {:attack, out} 29 | 30 | defp process_decay(out, sustain_level) when out <= sustain_level, do: {:sustain, sustain_level} 31 | defp process_decay(out, _sustain_level), do: {:decay, out} 32 | 33 | defp process_release(out) when out <= 0.0, do: {:idle, 0.0} 34 | defp process_release(out), do: {:release, out} 35 | 36 | defp get_transition(:off, :on, _), do: :attack 37 | defp get_transition(:on, :off, _), do: :release 38 | defp get_transition(_prev, _gate, state), do: state 39 | 40 | def adsr(rate, sustain_level, attack_duration, decay_duration, release_duration, target_ratio_A \\ 0.3, target_ratio_DR \\ 0.0001) do 41 | {attack_base, attack_coefficient} = calculate_base_and_coefficient(rate, 1.0, attack_duration, target_ratio_A) 42 | {decay_base, decay_coefficient} = calculate_base_and_coefficient(rate, sustain_level, decay_duration, -target_ratio_DR) 43 | {release_base, release_coefficient} = calculate_base_and_coefficient(rate, 0.0, release_duration, -target_ratio_DR) 44 | 45 | %ADSR{sustain_level: sustain_level, attack_base: attack_base, attack_coefficient: attack_coefficient, decay_base: decay_base, decay_coefficient: decay_coefficient, release_base: release_base, release_coefficient: release_coefficient} 46 | end 47 | 48 | def amplification_to_gate(amp) when amp > 0.0, do: :on 49 | def amplification_to_gate(_amp), do: :off 50 | 51 | defp calculate_coefficient(rate, target_ratio), do: :math.exp(-:math.log((1.0 + target_ratio) / target_ratio) / rate) 52 | 53 | defp calculate_base_and_coefficient(rate, target_level, duration, target_ratio) do 54 | target_rate = duration * rate 55 | coefficient = calculate_coefficient(target_rate, abs(target_ratio)) 56 | base = (target_level + target_ratio) * (1.0 - coefficient) 57 | {base, coefficient} 58 | end 59 | end -------------------------------------------------------------------------------- /lib/synthex/context.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Context do 2 | alias Synthex.Context 3 | 4 | defstruct [rate: 44100, output: nil, blocks: %{}, time: 0] 5 | 6 | def put_element(ctx = %Context{blocks: blocks}, block_name, element_name, element) do 7 | block = Map.get(blocks, block_name, %{}) |> Map.put(element_name, element) 8 | blocks = Map.put(blocks, block_name, block) 9 | Map.put(ctx, :blocks, blocks) 10 | end 11 | 12 | def get_element(%Context{blocks: blocks}, block_name, element_name, def_val \\ nil) do 13 | Map.get(blocks, block_name, %{}) |> Map.get(element_name, def_val) 14 | end 15 | 16 | def get_sample(ctx, block_name, element_name, modifiers \\ %{}) do 17 | element = get_element(ctx, block_name, element_name) |> Map.merge(modifiers) 18 | {element, sample} = apply(element.__struct__, :get_sample, [ctx, element]) 19 | {put_element(ctx, block_name, element_name, element), sample} 20 | end 21 | end -------------------------------------------------------------------------------- /lib/synthex/file/wav_header.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.File.WavHeader do 2 | require Integer 3 | 4 | defstruct [format: :lpcm, channels: 2, rate: 44100, sample_size: 16, data_chunk_size: 0] 5 | @wave_chunk_size 16 6 | 7 | def write(%Synthex.File.WavHeader{format: format, channels: channels, rate: rate, sample_size: sample_size, data_chunk_size: data_chunk_size}, io) do 8 | sample_size_bytes = div(sample_size, 8) 9 | full_size = (4 + 24 + 8 + data_chunk_size) + padding_length(data_chunk_size) 10 | encoded_format = encoded_format(format) 11 | block_align = sample_size_bytes * channels 12 | avg_bytes_per_sec = rate * block_align 13 | 14 | :ok = :file.write(io, "RIFF") 15 | :ok = :file.write(io, <>) 16 | :ok = :file.write(io, "WAVEfmt ") 17 | :ok = :file.write(io, <<@wave_chunk_size::little-integer-size(32)>>) 18 | :ok = :file.write(io, <>) 19 | :ok = :file.write(io, <>) 20 | :ok = :file.write(io, <>) 21 | :ok = :file.write(io, <>) 22 | :ok = :file.write(io, <>) 23 | :ok = :file.write(io, <>) 24 | :ok = :file.write(io, "data") 25 | :ok = :file.write(io, <>) 26 | end 27 | 28 | def read(io) do 29 | {:ok, "RIFF"} = :file.read(io, 4) 30 | {:ok, _full_size} = :file.read(io, 4) 31 | {:ok, "WAVEfmt "} = :file.read(io, 8) 32 | {:ok, <>} = :file.read(io, 4) 33 | {:ok, <>} = :file.read(io, 2) 34 | {:ok, <>} = :file.read(io, 2) 35 | {:ok, <>} = :file.read(io, 4) 36 | {:ok, _avg_bytes_per_sec} = :file.read(io, 4) 37 | {:ok, _block_align} = :file.read(io, 2) 38 | {:ok, <>} = :file.read(io, 2) 39 | {:ok, _} = :file.position(io, {:cur, wave_chunk_size - @wave_chunk_size}) 40 | 41 | data_chunk_size = find_data_chunk(io) 42 | format = decoded_format(encoded_format) 43 | %Synthex.File.WavHeader{format: format, channels: channels, rate: rate, sample_size: sample_size, data_chunk_size: data_chunk_size} 44 | end 45 | 46 | defp find_data_chunk(io) do 47 | {:ok, chunk_name} = :file.read(io, 4) 48 | {:ok, <>} = :file.read(io, 4) 49 | 50 | if chunk_name == "data" do 51 | chunk_size 52 | else 53 | :file.position(io, {:cur, chunk_size}) 54 | find_data_chunk(io) 55 | end 56 | end 57 | 58 | defp padding_length(data_chunk_size) when Integer.is_even(data_chunk_size), do: 1 59 | defp padding_length(_data_chunk_size), do: 0 60 | 61 | defp encoded_format(:lpcm), do: 0x0001 62 | defp encoded_format(:float), do: 0x0003 63 | 64 | defp decoded_format(0x0001), do: :lpcm 65 | defp decoded_format(0x0003), do: :float 66 | 67 | end -------------------------------------------------------------------------------- /lib/synthex/filter/biquad.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Filter.Biquad do 2 | use Synthex.Math 3 | 4 | alias Synthex.Filter.Biquad 5 | 6 | defstruct [coefficients: {1.0, 0.0, 0.0, 1.0, 0.0, 0.0}, sample: 0.0, in: {0.0, 0.0}, out: {0.0, 0.0}] 7 | 8 | def get_sample(_ctx, state = %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}, sample: sample, in: {i1, i2}, out: {o1, o2}}) do 9 | output = ((b0/a0) * sample) + ((b1/a0) * i1) + ((b2/a0) * i2) - ((a1/a0) * o1) - ((a2/a0) * o2) 10 | state = state |> Map.put(:in, {sample, i1}) |> Map.put(:out, {output, o1}) 11 | 12 | {state, output} 13 | end 14 | 15 | defp get_a(db_gain), do: :math.pow(10, (db_gain/40)) 16 | 17 | defp get_w0(rate, freq) do 18 | w0 = @tau * (freq/rate) 19 | cos_w0 = :math.cos(w0) 20 | sin_w0 = :math.sin(w0) 21 | 22 | {w0, cos_w0, sin_w0} 23 | end 24 | 25 | defp get_alpha(:q, _w0, sin_w0, q, _), do: sin_w0 / (2 * q) 26 | defp get_alpha(:bandwidth, w0, sin_w0, bw, _), do: sin_w0 * :math.sinh((:math.log(2) / 2) * bw * (w0 / sin_w0)) 27 | defp get_alpha(:slope, _w0, sin_w0, s, a), do: (sin_w0 / 2) * :math.sqrt((a + 1 / a) * (1 / s - 1) + 2) 28 | 29 | def lowpass(rate, freq, q) do 30 | {w0, cos_w0, sin_w0} = get_w0(rate, freq) 31 | alpha = get_alpha(:q, w0, sin_w0, q, :none) 32 | 33 | a0 = 1 + alpha 34 | a1 = -2 * cos_w0 35 | a2 = 1 - alpha 36 | b1 = 1 - cos_w0 37 | b0 = b2 = b1 / 2 38 | 39 | %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}} 40 | end 41 | 42 | def highpass(rate, freq, q) do 43 | {w0, cos_w0, sin_w0} = get_w0(rate, freq) 44 | alpha = get_alpha(:q, w0, sin_w0, q, :none) 45 | one_plus_cos_w0 = (1 + cos_w0) 46 | 47 | a0 = 1 + alpha 48 | a1 = -2 * cos_w0 49 | a2 = 1 - alpha 50 | b0 = b2 = one_plus_cos_w0 / 2 51 | b1 = -one_plus_cos_w0 52 | 53 | %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}} 54 | end 55 | 56 | def bandpass_skirt(rate, freq, {type, q_or_bw}) do 57 | {w0, cos_w0, sin_w0} = get_w0(rate, freq) 58 | alpha = get_alpha(type, w0, sin_w0, q_or_bw, :none) 59 | half_sin_w0 = sin_w0 / 2 60 | 61 | a0 = 1 + alpha 62 | a1 = -2 * cos_w0 63 | a2 = 1 - alpha 64 | b0 = half_sin_w0 65 | b1 = 0.0 66 | b2 = -half_sin_w0 67 | 68 | %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}} 69 | end 70 | 71 | def bandpass_peak(rate, freq, {type, q_or_bw}) do 72 | {w0, cos_w0, sin_w0} = get_w0(rate, freq) 73 | alpha = get_alpha(type, w0, sin_w0, q_or_bw, :none) 74 | 75 | a0 = 1 + alpha 76 | a1 = -2 * cos_w0 77 | a2 = 1 - alpha 78 | b0 = alpha 79 | b1 = 0.0 80 | b2 = -alpha 81 | 82 | %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}} 83 | end 84 | 85 | def notch(rate, freq, {type, q_or_bw}) do 86 | {w0, cos_w0, sin_w0} = get_w0(rate, freq) 87 | alpha = get_alpha(type, w0, sin_w0, q_or_bw, :none) 88 | 89 | a0 = 1 + alpha 90 | a1 = b1 = -2 * cos_w0 91 | a2 = 1 - alpha 92 | b0 = b2 = 1.0 93 | 94 | %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}} 95 | end 96 | 97 | def allpass(rate, freq, q) do 98 | {w0, cos_w0, sin_w0} = get_w0(rate, freq) 99 | alpha = get_alpha(:q, w0, sin_w0, q, :none) 100 | 101 | a0 = b2 = 1 + alpha 102 | a1 = b1 = -2 * cos_w0 103 | a2 = b0 = 1 - alpha 104 | 105 | %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}} 106 | end 107 | 108 | def peaking_eq(rate, freq, db_gain, {type, q_or_bw}) do 109 | a = get_a(db_gain) 110 | {w0, cos_w0, sin_w0} = get_w0(rate, freq) 111 | alpha = get_alpha(type, w0, sin_w0, q_or_bw, :none) 112 | alpha_on_a = alpha/a 113 | a_times_alpha = alpha * a 114 | 115 | a0 = 1 + alpha_on_a 116 | a1 = b1 = -2 * cos_w0 117 | a2 = 1 - alpha_on_a 118 | b0 = 1 + a_times_alpha 119 | b2 = 1 - a_times_alpha 120 | 121 | %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}} 122 | end 123 | 124 | def lowshelf(rate, freq, db_gain, {type, q_or_slope}) do 125 | a = get_a(db_gain) 126 | {w0, cos_w0, sin_w0} = get_w0(rate, freq) 127 | alpha = get_alpha(type, w0, sin_w0, q_or_slope, a) 128 | ap1 = a + 1 129 | am1 = a - 1 130 | ap1_cos_w0 = ap1 * cos_w0 131 | am1_cos_w0 = am1 * cos_w0 132 | beta = 2 * :math.sqrt(a) * alpha 133 | 134 | a0 = ap1 + am1_cos_w0 + beta 135 | a1 = -2 * (am1 + ap1_cos_w0) 136 | a2 = ap1 + am1_cos_w0 - beta 137 | b0 = a * (ap1 - am1_cos_w0 + beta) 138 | b1 = 2 * a * (am1 - ap1_cos_w0) 139 | b2 = a * (ap1 - am1_cos_w0 - beta) 140 | 141 | %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}} 142 | end 143 | 144 | def highshelf(rate, freq, db_gain, {type, q_or_slope}) do 145 | a = get_a(db_gain) 146 | {w0, cos_w0, sin_w0} = get_w0(rate, freq) 147 | alpha = get_alpha(type, w0, sin_w0, q_or_slope, a) 148 | ap1 = a + 1 149 | am1 = a - 1 150 | ap1_cos_w0 = ap1 * cos_w0 151 | am1_cos_w0 = am1 * cos_w0 152 | beta = 2 * :math.sqrt(a) * alpha 153 | 154 | a0 = ap1 - am1_cos_w0 + beta 155 | a1 = 2 * (am1 - ap1_cos_w0) 156 | a2 = ap1 - am1_cos_w0 - beta 157 | b0 = a * (ap1 + am1_cos_w0 + beta) 158 | b1 = -2 * a * (am1 + ap1_cos_w0) 159 | b2 = a * (ap1 + am1_cos_w0 - beta) 160 | 161 | %Biquad{coefficients: {a0, a1, a2, b0, b1, b2}} 162 | end 163 | 164 | end -------------------------------------------------------------------------------- /lib/synthex/filter/bitcrusher.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Filter.Bitcrusher do 2 | @moduledoc """ 3 | Quantizer / Decimator with smooth control. 4 | 5 | bits must be between 1 and 16 6 | normalized_frequency (frequency / rate) must be between 0 and 1 7 | """ 8 | 9 | use Synthex.Math 10 | 11 | alias Synthex.Filter.Bitcrusher 12 | 13 | defstruct [bits: 16, normalized_frequency: 1.0, sample: 0.0, last: 0.0, phaser: 0.0] 14 | 15 | def get_sample(_ctx, state = %Bitcrusher{bits: bits, normalized_frequency: normalized_frequency, sample: sample, last: last, phaser: phaser}) do 16 | step = :math.pow(1/2, bits) 17 | {phaser, last} = updated_state(phaser + normalized_frequency, last, sample, step) 18 | 19 | state = state |> Map.put(:phaser, phaser) |> Map.put(:last, last) 20 | {state, last} 21 | end 22 | 23 | defp updated_state(phaser, _last, sample, step) when phaser >= 1.0, do: {phaser - 1.0, step * Float.floor(sample / step + 0.5)} 24 | defp updated_state(phaser, last, _sample, _step), do: {phaser, last} 25 | end -------------------------------------------------------------------------------- /lib/synthex/filter/low_high_pass.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Filter.LowHighPass do 2 | use Synthex.Math 3 | 4 | alias Synthex.Context 5 | alias Synthex.Filter.LowHighPass 6 | 7 | defstruct [type: :lowpass, cutoff: 220, sample: 0.0, history: nil] 8 | 9 | def get_sample(%Context{rate: rate}, state = %LowHighPass{type: type, cutoff: cutoff, sample: sample, history: history}) do 10 | rc = 1.0 / (cutoff * @tau) 11 | dt = 1.0 / rate 12 | alpha = calculate_alpha(type, dt, rc) 13 | filtered_sample = do_get_sample(type, alpha, sample, history) 14 | 15 | {Map.put(state, :history, {filtered_sample, sample}), filtered_sample} 16 | end 17 | 18 | defp calculate_alpha(:lowpass, dt, rc), do: dt / (rc + dt) 19 | defp calculate_alpha(:highpass, dt, rc), do: rc / (rc + dt) 20 | 21 | defp do_get_sample(_type, _alpha, sample, nil), do: sample 22 | defp do_get_sample(:lowpass, alpha, sample, {prev_filtered, _}), do: prev_filtered + alpha * (sample - prev_filtered) 23 | defp do_get_sample(:highpass, alpha, sample, {prev_filtered, prev}), do: alpha * (prev_filtered + sample - prev) 24 | 25 | end -------------------------------------------------------------------------------- /lib/synthex/filter/moog.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Filter.Moog do 2 | @moduledoc """ 3 | Emulates the Moog VCF. 4 | 5 | cutoff must be between 0 and 1 6 | resonance must be between 0 and 4 7 | """ 8 | use Synthex.Math 9 | 10 | alias Synthex.Filter.Moog 11 | 12 | defstruct [cutoff: 1, resonance: 0, sample: 0.0, in: {0.0, 0.0, 0.0, 0.0}, out: {0.0, 0.0, 0.0, 0.0}] 13 | 14 | def get_sample(_ctx, state = %Moog{cutoff: cutoff, resonance: resonance, sample: sample, in: {i1, i2, i3, i4}, out: {o1, o2, o3, o4}}) do 15 | f = cutoff * 1.16 16 | f_squared = f * f 17 | fb = resonance * (1.0 - 0.15 * f_squared) 18 | sample = sample - o4 * fb 19 | sample = sample * 0.35013 * f_squared * f_squared 20 | o1 = sample + 0.3 * i1 + (1 - f) * o1 21 | o2 = o1 + 0.3 * i2 + (1 - f) * o2 22 | o3 = o2 + 0.3 * i3 + (1 - f) * o3 23 | o4 = o3 + 0.3 * i4 + (1 - f) * o4 24 | 25 | state = 26 | state 27 | |> Map.put(:in, {sample, o1, o2, o3}) 28 | |> Map.put(:out, {o1, o2, o3, o4}) 29 | 30 | {state, o4} 31 | end 32 | 33 | 34 | end -------------------------------------------------------------------------------- /lib/synthex/generator/noise.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Generator.Noise do 2 | alias Synthex.Generator.Noise 3 | 4 | defstruct [type: :white, history: {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}] 5 | 6 | def get_sample(_ctx, state = %Noise{type: :white}), do: {state, get_random_sample()} 7 | def get_sample(_ctx, state = %Noise{type: :pink, history: {b0, b1, b2, b3, b4, b5, b6}}) do 8 | white = get_random_sample() 9 | 10 | b0 = 0.99886 * b0 + white * 0.0555179 11 | b1 = 0.99332 * b1 + white * 0.0750759 12 | b2 = 0.96900 * b2 + white * 0.1538520 13 | b3 = 0.86650 * b3 + white * 0.3104856 14 | b4 = 0.55000 * b4 + white * 0.5329522 15 | b5 = -0.7616 * b5 - white * 0.0168980 16 | 17 | pink = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362 18 | 19 | b6 = white * 0.115926 20 | 21 | {Map.put(state, :history, {b0, b1, b2, b3, b4, b5, b6}), pink * 0.11} 22 | end 23 | def get_sample(_ctx, state = %Noise{type: :brown, history: {b0, _, _, _, _, _, _}}) do 24 | white = get_random_sample() 25 | brown = (b0 + (0.02 * white)) / 1.02 26 | 27 | {Map.put(state, :history, {brown, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}), brown * 3.5} 28 | end 29 | 30 | defp get_random_sample do 31 | :rand.uniform() * 2 - 1 32 | end 33 | end -------------------------------------------------------------------------------- /lib/synthex/generator/oscillator.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Generator.Oscillator do 2 | use Synthex.Math 3 | 4 | alias Synthex.Context 5 | alias Synthex.Generator.Oscillator 6 | 7 | defstruct [algorithm: :sine, frequency: 220, sync_phase: @tau, phase: 0.0, center: @pi] 8 | 9 | def get_sample(%Context{rate: rate}, state = %Oscillator{algorithm: algorithm, frequency: frequency, sync_phase: sync_phase, phase: phase}) do 10 | sample = do_get_sample(algorithm, state) 11 | phase_delta = (@tau * frequency) / rate 12 | phase = calculate_phase(phase + phase_delta, sync_phase) 13 | {Map.put(state, :phase, phase), sample} 14 | end 15 | 16 | defp calculate_phase(phase, sync_phase) when phase >= sync_phase, do: phase - sync_phase 17 | defp calculate_phase(phase, _sync_phase), do: phase 18 | 19 | defp do_get_sample(:sine, %{phase: phase}), do: :math.sin(phase) 20 | defp do_get_sample(alg, %{phase: phase, center: center}) when alg in [:pulse, :square] and phase < center, do: 1.0 21 | defp do_get_sample(alg, _state) when alg in [:pulse, :square], do: -1.0 22 | defp do_get_sample(:sawtooth, %{phase: phase}), do: (@one_on_pi * phase) - 1.0 23 | defp do_get_sample(:reverse_sawtooth, %{phase: phase}), do: 1.0 - (@one_on_pi * phase) 24 | defp do_get_sample(:triangle, %{phase: phase, center: center}) when phase < center, do: -1.0 + (@two_on_pi * phase) 25 | defp do_get_sample(:triangle, %{phase: phase}), do: 3.0 - (@two_on_pi * phase) 26 | defp do_get_sample(func, state) when is_function(func), do: func.(state) 27 | defp do_get_sample({module, function}, state), do: apply(module, function, [state]) 28 | end -------------------------------------------------------------------------------- /lib/synthex/input/wav_reader.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Input.WavReader do 2 | alias Synthex.Input.WavReader 3 | alias Synthex.File.WavHeader 4 | 5 | defstruct [file: nil, header: nil, loop: true, data_offset: 0, pos: 0] 6 | 7 | def get_sample(_ctx, state = %WavReader{file: file, header: header, loop: loop, data_offset: data_offset, pos: pos}) do 8 | eof = (pos == :none) || (pos >= (header.data_chunk_size + data_offset)) 9 | pos = read_position(pos, eof, loop, data_offset) 10 | {new_pos, samples} = read_samples(file, pos, header) 11 | {Map.put(state, :pos, new_pos), samples} 12 | end 13 | 14 | def open(path, loop \\ true) do 15 | {:ok, file} = :file.open(path, [:read, :binary, :read_ahead]) 16 | header = WavHeader.read(file) 17 | {:ok, data_offset} = :file.position(file, {:cur, 0}) 18 | %WavReader{file: file, header: header, loop: loop, data_offset: data_offset, pos: data_offset} 19 | end 20 | 21 | def get_duration(reader = %WavReader{header: %WavHeader{rate: rate}}) do 22 | get_sample_count(reader) / rate 23 | end 24 | 25 | def get_sample_count(%WavReader{header: %WavHeader{data_chunk_size: data_chunk_size, channels: channels, sample_size: sample_size}}) do 26 | div(data_chunk_size, (div(sample_size, 8) * channels)) 27 | end 28 | 29 | defp read_position(pos, false, _, _), do: pos 30 | defp read_position(_, true, true, data_offset), do: data_offset 31 | defp read_position(_, true, false, _), do: :none 32 | 33 | defp read_samples(_file, :none, %WavHeader{channels: channels}), do: {:none, Enum.map(1..channels, fn(_) -> 0.0 end)} 34 | defp read_samples(file, pos, %WavHeader{sample_size: sample_size, format: format, channels: channels}) do 35 | to_read = div(sample_size, 8) * channels 36 | {:ok, data} = :file.read(file, to_read) 37 | {pos + to_read, decode_samples(data, sample_size, format, [])} 38 | end 39 | 40 | defp decode_samples("", _sample_size, _format, decoded_samples), do: Enum.reverse(decoded_samples) 41 | defp decode_samples(data, sample_size, :lpcm, decoded_samples) do 42 | <> = data 43 | decode_samples(rest, sample_size, :lpcm, [int_to_float_sample(sample, sample_size) | decoded_samples]) 44 | end 45 | defp decode_samples(<>, 32, :float, decoded_samples) do 46 | decode_samples(rest, 32, :float, [sample | decoded_samples]) 47 | end 48 | 49 | defp int_to_float_sample(sample, 8) when sample < 0, do: sample / 128 50 | defp int_to_float_sample(sample, 8), do: sample / 127 51 | defp int_to_float_sample(sample, 16) when sample < 0, do: sample / 32768 52 | defp int_to_float_sample(sample, 16), do: sample / 32767 53 | defp int_to_float_sample(sample, 24) when sample < 0, do: sample / 8388608 54 | defp int_to_float_sample(sample, 24), do: sample / 8388607 55 | defp int_to_float_sample(sample, 32) when sample < 0, do: sample / 2147483648 56 | defp int_to_float_sample(sample, 32), do: sample / 2147483647 57 | end -------------------------------------------------------------------------------- /lib/synthex/math.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Math do 2 | defmacro __using__(_) do 3 | quote do 4 | import Synthex.Math 5 | @pi :math.pi() 6 | @tau @pi * 2 7 | @one_on_pi 1/@pi 8 | @two_on_pi 2/@pi 9 | end 10 | end 11 | 12 | defmacro fmod(val, divider) do 13 | quote do 14 | unquote(val) - (Float.floor(unquote(val)/unquote(divider)) * unquote(divider)) 15 | end 16 | end 17 | 18 | defmacro amplitude_to_frequency(magnitude, min, max) do 19 | quote do 20 | (unquote(magnitude) + 1.0) * ((unquote(max) - unquote(min))/2.0) + unquote(min) 21 | end 22 | end 23 | 24 | defmacro amplitude_to_rounded_frequency(magnitude, min, max) do 25 | quote do 26 | round(unquote(__MODULE__).amplitude_to_frequency(unquote(magnitude), unquote(min), unquote(max))) 27 | end 28 | end 29 | 30 | defmacro duration_in_secs_to_sample_count(duration, rate) do 31 | quote do 32 | if is_float(unquote(duration)) do 33 | Float.ceil(unquote(duration) * unquote(rate)) |> trunc 34 | else 35 | unquote(duration) * unquote(rate) 36 | end 37 | end 38 | end 39 | 40 | defmacro duty_cycle_to_radians(duty_cycle) do 41 | quote do 42 | @tau * unquote(duty_cycle) 43 | end 44 | end 45 | 46 | defmacro shift_by(sample, amount) do 47 | quote do 48 | clamp(unquote(sample) + unquote(amount)) 49 | end 50 | end 51 | 52 | def clamp(sample) when sample <= -1.0, do: -1.0 53 | def clamp(sample) when sample >= 1.0, do: 1.0 54 | def clamp(sample), do: sample 55 | end -------------------------------------------------------------------------------- /lib/synthex/output/sox_player.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Output.SoxPlayer do 2 | @moduledoc """ 3 | Outputs to SoX player. SoX must be installed on the system beforehand and must be in the PATH environment variable. 4 | """ 5 | 6 | use GenServer 7 | 8 | def open(opts) do 9 | rate = Keyword.get(opts, :rate, 44100) 10 | channels = Keyword.get(opts, :channels, 2) 11 | 12 | GenServer.start_link(__MODULE__, %{rate: rate, channels: channels}) 13 | end 14 | 15 | def close(player) do 16 | GenServer.cast(player, :close) 17 | end 18 | 19 | def init(%{rate: rate, channels: channels}) do 20 | args = ['-q', '-t', 'raw', '-L', '-b', '32', '-e', 'floating-point', '-r', Integer.to_char_list(rate), '-c', Integer.to_char_list(channels), '-'] 21 | play = :os.find_executable('play') 22 | port = Port.open({:spawn_executable, play}, [{:args, args}, :stderr_to_stdout, :binary, :stream]) 23 | {:ok, %{port: port}} 24 | end 25 | 26 | def handle_call({:write_samples, samples}, _from, state = %{port: port}) do 27 | encoded_samples = encode_samples(samples) 28 | Port.command(port, encoded_samples) 29 | {:reply, :ok, state} 30 | end 31 | 32 | def handle_cast(:close, state) do 33 | {:stop, :normal, state} 34 | end 35 | 36 | defp encode_samples(samples) when is_list(samples) do 37 | Enum.reduce(samples, <<>>, fn(sample, acc) -> acc <> encode_samples(sample) end) 38 | end 39 | defp encode_samples(sample) do 40 | <> 41 | end 42 | end -------------------------------------------------------------------------------- /lib/synthex/output/wav_writer.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Output.WavWriter do 2 | use GenServer 3 | 4 | @header_length 44 5 | 6 | def open(path, header \\ %Synthex.File.WavHeader{}) do 7 | GenServer.start_link(__MODULE__, %{path: path, header: header}) 8 | end 9 | 10 | def close(writer) do 11 | GenServer.cast(writer, :close) 12 | end 13 | 14 | def init(%{path: path, header: header}) do 15 | {:ok, file} = :file.open(path, [:write, :raw, :binary, :delayed_write]) 16 | {:ok, _} = :file.position(file, @header_length) 17 | 18 | Process.flag(:trap_exit, true) 19 | 20 | {:ok, %{header: header, file: file, data_chunk_size: 0}} 21 | end 22 | 23 | def handle_call({:write_samples, samples}, _from, state = %{header: header, file: file, data_chunk_size: data_chunk_size}) do 24 | encoded_samples = encode_samples(samples, header) 25 | :ok = :file.write(file, encoded_samples) 26 | {:reply, :ok, %{state | data_chunk_size: data_chunk_size + byte_size(encoded_samples)}} 27 | end 28 | 29 | def handle_cast(:close, state) do 30 | {:stop, :normal, state} 31 | end 32 | 33 | def terminate(_signal, %{file: file, header: header, data_chunk_size: data_chunk_size}) do 34 | {:ok, _} = :file.position(file, :bof) 35 | :ok = Synthex.File.WavHeader.write(%Synthex.File.WavHeader{header | data_chunk_size: data_chunk_size}, file) 36 | :ok = :file.close(file) 37 | end 38 | 39 | defp encode_samples(samples, header) when is_list(samples) do 40 | Enum.reduce(samples, <<>>, fn(sample, acc) -> acc <> encode_samples(sample, header) end) 41 | end 42 | defp encode_samples(sample, header = %Synthex.File.WavHeader{format: :lpcm, sample_size: sample_size}) when is_float(sample) do 43 | sample |> float_to_int_sample(sample_size) |> encode_samples(header) 44 | end 45 | defp encode_samples(sample, %Synthex.File.WavHeader{format: :lpcm, sample_size: sample_size}) when is_integer(sample) do 46 | <> 47 | end 48 | defp encode_samples(sample, %Synthex.File.WavHeader{format: :float, sample_size: 32}) when is_float(sample) do 49 | <> 50 | end 51 | defp encode_samples(_, _) do 52 | raise "Supported input formats are integers and float. Supported encoding formats are integer 8/16/24/32 bit and float 32 bit" 53 | end 54 | 55 | defp float_to_int_sample(sample, 8) when sample < 0, do: round(sample * 128) 56 | defp float_to_int_sample(sample, 8), do: round(sample * 127) 57 | defp float_to_int_sample(sample, 16) when sample < 0, do: round(sample * 32768) 58 | defp float_to_int_sample(sample, 16), do: round(sample * 32767) 59 | defp float_to_int_sample(sample, 24) when sample < 0, do: round(sample * 8388608) 60 | defp float_to_int_sample(sample, 24), do: round(sample * 8388607) 61 | defp float_to_int_sample(sample, 32) when sample < 0, do: round(sample * 2147483648) 62 | defp float_to_int_sample(sample, 32), do: round(sample * 2147483647) 63 | end -------------------------------------------------------------------------------- /lib/synthex/output/writer.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Output.Writer do 2 | def write_samples(writer, samples) do 3 | GenServer.call(writer, {:write_samples, samples}) 4 | end 5 | end -------------------------------------------------------------------------------- /lib/synthex/sequencer.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Sequencer do 2 | alias Synthex.Context 3 | alias Synthex.Sequencer 4 | 5 | defstruct [sequence: nil, note_duration: 0.15, loop: true, playing_sequence: nil, step: 0] 6 | 7 | @silence {0.000000001, 0.0} 8 | 9 | def get_sample(_ctx, state = %Sequencer{playing_sequence: [], loop: false}), do: {state, @silence} 10 | def get_sample(ctx, state = %Sequencer{sequence: sequence, playing_sequence: ps, loop: l}) when (ps == nil) or (ps == [] and l == true) do 11 | get_sample(ctx, %Sequencer{state | playing_sequence: sequence}) 12 | end 13 | def get_sample(%Context{rate: rate}, state = %Sequencer{playing_sequence: [note | _] = sequence, note_duration: note_duration, step: step}) do 14 | step_delta = 1 / rate 15 | {new_sequence, new_step} = get_next_step(step + step_delta, note_duration, sequence) 16 | {%Sequencer{state | playing_sequence: new_sequence, step: new_step}, note} 17 | end 18 | 19 | defp get_next_step(step, note_duration, sequence) when step < note_duration, do: {sequence, step} 20 | defp get_next_step(step, note_duration, [_note | rest]), do: {rest, step - note_duration} 21 | 22 | def sequence_duration(%Sequencer{sequence: sequence, note_duration: note_duration}), do: length(sequence) * note_duration 23 | 24 | def bpm_to_duration(bpm, notes_per_beat), do: 60 / bpm / notes_per_beat 25 | 26 | defdelegate from_simple_string(string, note_duration), to: Synthex.Sequencer.SimpleStringFormat 27 | end -------------------------------------------------------------------------------- /lib/synthex/sequencer/morse.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Sequencer.Morse do 2 | alias Synthex.Sequencer 3 | 4 | def from_text(text, dot_duration \\ 0.048, freq \\ 880.0) do 5 | morse_string = text_to_morse(text) 6 | from_morse(morse_string, dot_duration, freq) 7 | end 8 | 9 | def from_morse(morse_string, dot_duration, freq) do 10 | sequence = 11 | morse_string 12 | |> to_char_list() 13 | |> Enum.reduce([], fn(c, acc) -> [morse_char_to_freq(c, freq) | acc] end) 14 | |> Enum.reverse() 15 | 16 | %Sequencer{sequence: sequence, note_duration: dot_duration} 17 | end 18 | 19 | def wpm_to_dot_duration(wpm), do: 1.2 / wpm 20 | 21 | @text_to_morse_map %{ 22 | ?A => "=.===", 23 | ?B => "===.=.=.=", 24 | ?C => "===.=.===.=", 25 | ?D => "===.=.=", 26 | ?E => "=", 27 | ?F => "=.=.===.=", 28 | ?G => "===.===.=", 29 | ?H => "=.=.=.=", 30 | ?I => "=.=", 31 | ?J => "=.===.===.===", 32 | ?K => "===.=.===", 33 | ?L => "=.===.=.=", 34 | ?M => "===.===", 35 | ?N => "===.=", 36 | ?O => "===.===.===", 37 | ?P => "=.===.===.=", 38 | ?Q => "===.===.=.===", 39 | ?R => "=.===.=", 40 | ?S => "=.=.=", 41 | ?T => "===", 42 | ?U => "=.=.===", 43 | ?V => "=.=.=.===", 44 | ?W => "=.===.===", 45 | ?X => "===.=.=.===", 46 | ?Y => "===.=.===.===", 47 | ?Z => "===.===.=.=", 48 | ?1 => "=.===.===.===.===", 49 | ?2 => "=.=.===.===.===", 50 | ?3 => "=.=.=.===.===", 51 | ?4 => "=.=.=.=.===", 52 | ?5 => "=.=.=.=.=", 53 | ?6 => "===.=.=.=.=", 54 | ?7 => "===.===.=.=.=", 55 | ?8 => "===.===.===.=.=", 56 | ?9 => "===.===.===.===.=", 57 | ?0 => "===.===.===.===.===", 58 | ?\s => "." 59 | } 60 | 61 | @morse_to_text_map Enum.map(@text_to_morse_map, fn({k, v}) -> {v, k} end) |> Enum.into(%{}) |> Map.put("+", ?\s) 62 | 63 | def text_to_morse(text) do 64 | text 65 | |> String.upcase 66 | |> String.replace(~r/[^A-Z0-9]+/, " ") 67 | |> to_char_list() 68 | |> Enum.reduce("", fn(c, acc) -> acc <> "..." <> @text_to_morse_map[c] end) 69 | end 70 | 71 | def morse_to_text(morse_string) do 72 | morse_string 73 | |> String.replace(".......", "...+...") 74 | |> String.split(~r/[\.]{3,7}/, trim: true) 75 | |> Enum.map(fn(s) -> replace_nil(@morse_to_text_map[s]) end) 76 | |> to_string() 77 | |> String.strip() 78 | end 79 | 80 | defp morse_char_to_freq(?=, freq), do: {freq, 1.0} 81 | defp morse_char_to_freq(?., freq), do: {freq, 0.0} 82 | 83 | defp replace_nil(nil), do: ?? 84 | defp replace_nil(c), do: c 85 | 86 | end -------------------------------------------------------------------------------- /lib/synthex/sequencer/simple_string_format.ex: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Sequencer.SimpleStringFormat do 2 | alias Synthex.Sequencer 3 | 4 | @notes %{ 5 | "c8" => 4186.01, 6 | "b7" => 3951.07, 7 | "A7" => 3729.31, 8 | "a7" => 3520.00, 9 | "G7" => 3322.44, 10 | "g7" => 3135.96, 11 | "F7" => 2959.96, 12 | "f7" => 2793.83, 13 | "e7" => 2637.02, 14 | "D7" => 2489.02, 15 | "d7" => 2349.32, 16 | "C7" => 2217.46, 17 | "c7" => 2093.00, 18 | "b6" => 1975.53, 19 | "A6" => 1864.66, 20 | "a6" => 1760.00, 21 | "G6" => 1661.22, 22 | "g6" => 1567.98, 23 | "F6" => 1479.98, 24 | "f6" => 1396.91, 25 | "e6" => 1318.51, 26 | "D6" => 1244.51, 27 | "d6" => 1174.66, 28 | "C6" => 1108.73, 29 | "c6" => 1046.50, 30 | "b5" => 987.767, 31 | "A5" => 932.328, 32 | "a5" => 880.000, 33 | "G5" => 830.609, 34 | "g5" => 783.991, 35 | "F5" => 739.989, 36 | "f5" => 698.456, 37 | "e5" => 659.255, 38 | "D5" => 622.254, 39 | "d5" => 587.330, 40 | "C5" => 554.365, 41 | "c5" => 523.251, 42 | "b4" => 493.883, 43 | "A4" => 466.164, 44 | "a4" => 440.000, 45 | "G4" => 415.305, 46 | "g4" => 391.995, 47 | "F4" => 369.994, 48 | "f4" => 349.228, 49 | "e4" => 329.628, 50 | "D4" => 311.127, 51 | "d4" => 293.665, 52 | "C4" => 277.183, 53 | "c4" => 261.626, 54 | "b3" => 246.942, 55 | "A3" => 233.082, 56 | "a3" => 220.000, 57 | "G3" => 207.652, 58 | "g3" => 195.998, 59 | "F3" => 184.997, 60 | "f3" => 174.614, 61 | "e3" => 164.814, 62 | "D3" => 155.563, 63 | "d3" => 146.832, 64 | "C3" => 138.591, 65 | "c3" => 130.813, 66 | "b2" => 123.471, 67 | "A2" => 116.541, 68 | "a2" => 110.000, 69 | "g2" => 97.9989, 70 | "F2" => 92.4987, 71 | "f2" => 87.3071, 72 | "e2" => 82.4069, 73 | "D2" => 77.7817, 74 | "d2" => 73.4162, 75 | "C2" => 69.2957, 76 | "c2" => 65.4064, 77 | "b1" => 61.7354, 78 | "A1" => 58.2705, 79 | "a1" => 55.0000, 80 | "G1" => 51.9131, 81 | "g1" => 48.9994, 82 | "F1" => 46.2493, 83 | "f1" => 43.6535, 84 | "e1" => 41.2034, 85 | "D1" => 38.8909, 86 | "d1" => 36.7081, 87 | "C1" => 34.6478, 88 | "c1" => 32.7032, 89 | "b0" => 30.8677, 90 | "A0" => 29.1352, 91 | "a0" => 27.5000 92 | } 93 | 94 | def from_simple_string(string, note_duration) do 95 | sequence = string 96 | |> String.replace(~r/[|\n\t\r ]/, "") 97 | |> String.replace("-", "--") 98 | |> String.replace(">", ">>") 99 | |> sequence_simple_string([]) 100 | 101 | %Sequencer{sequence: sequence, note_duration: note_duration} 102 | end 103 | 104 | defp sequence_simple_string("", sequence), do: Enum.reverse(sequence) 105 | defp sequence_simple_string(song, sequence) do 106 | {note, rest} = String.split_at(song, 2) 107 | decoded_note = decode_note(sequence, note) 108 | sequence_simple_string(rest, [decoded_note | sequence]) 109 | end 110 | 111 | defp decode_note([{freq, _} | _], "--"), do: {freq, 0.0} 112 | defp decode_note([], "--"), do: {0.000000001, 0.0} 113 | defp decode_note([{freq, amp} | _], ">>"), do: {freq, amp} 114 | defp decode_note(_prev, note), do: {Map.fetch!(@notes, note), 1.0} 115 | end -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Synthex.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :synthex, 6 | version: "0.1.0", 7 | elixir: "~> 1.1", 8 | package: package, 9 | description: description, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | deps: deps] 13 | end 14 | 15 | # Configuration for the OTP application 16 | # 17 | # Type "mix help compile.app" for more information 18 | def application do 19 | [applications: [:logger]] 20 | end 21 | 22 | defp deps do 23 | [] 24 | end 25 | 26 | defp description do 27 | "A signal synthesis library" 28 | end 29 | 30 | defp package do 31 | [ 32 | maintainers: ["Michele Balistreri"], 33 | licenses: ["ISC"], 34 | links: %{"GitHub" => "https://github.com/bitgamma/synthex"} 35 | ] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/synthex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SynthexTest do 2 | use ExUnit.Case 3 | doctest Synthex 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------