From 2c22abf2ea25cdaa1b3adb07d1ca2dde488b4e0a Mon Sep 17 00:00:00 2001 From: Alex Clink Date: Sat, 19 Feb 2022 23:01:22 -0500 Subject: [PATCH] Improve piano, add harmonica --- examples/piano.cr | 2 +- src/audio.cr | 2 +- src/audio/instrument.cr | 49 ++++++++++++++++++++++++++++++++++++----- src/audio/note.cr | 5 +++-- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/examples/piano.cr b/examples/piano.cr index c86434a..3f4c4db 100644 --- a/examples/piano.cr +++ b/examples/piano.cr @@ -18,7 +18,7 @@ module PF @white_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String) @black_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String) - @instruments : Array(Instrument) = [RetroVoice.new, PianoVoice.new, Flute.new, KickDrum.new, SnareDrum.new] + @instruments : Array(Instrument) = [RetroVoice.new, PianoVoice.new, Flute.new, KickDrum.new, SnareDrum.new, Harmonica.new] def initialize(*args, **kwargs) super diff --git a/src/audio.cr b/src/audio.cr index 005fc36..b23be68 100644 --- a/src/audio.cr +++ b/src/audio.cr @@ -6,7 +6,7 @@ module PF @device_id : LibSDL::AudioDeviceID property volume = 0.5 # https://dsp.stackexchange.com/questions/3581/algorithms-to-mix-audio-signals-without-clipping - property headroom = 0.4 + property headroom = 0.3 delegate :freq, to: @spec @playing : Bool = false getter time : Float64 = 0.0 diff --git a/src/audio/instrument.cr b/src/audio/instrument.cr index 37d1ddc..e8d4aa9 100644 --- a/src/audio/instrument.cr +++ b/src/audio/instrument.cr @@ -3,6 +3,7 @@ module PF property name : String = "Unnamed Instrument" property envelope : Envelope property wave : Sound::Wave + property volume : Float64 = 1.0 getter sounds : Array(Sound) = [] of Sound @notes : Hash(UInt32, Sound) = {} of UInt32 => Sound @@ -13,7 +14,7 @@ module PF def on(hertz : Float64, time : Float64) @note_id += 1_u32 - sound = Sound.new(hertz, @envelope, time, @wave) + sound = Sound.new(hertz, @envelope, time, @volume, @wave) @notes[@note_id] = sound @sounds << sound @note_id @@ -47,13 +48,32 @@ module PF class PianoVoice < Instrument def initialize @name = "Piano" + + exp_interpolation = ->(time : Float64, duration : Float64, initial : Float64, level : Float64) do + # https://www.desmos.com/calculator/r2jn9wurwv + curve = 1000 + (initial - level) * ((curve ** -(time / duration)) * (1 + (1 / curve)) - (1 / curve)) + level + end + @envelope = Envelope.new( - attack: Envelope::Stage.new(0.001, 0.0, 1.0), - decay: Envelope::Stage.new(0.7, 1.0, 0.0), + attack: Envelope::Stage.new(0.001, 0.0, 1.0, exp_interpolation), + decay: Envelope::Stage.new(3.0, 1.0, 0.0, exp_interpolation), sustain: Envelope::Stage.new(0.0, 0.0, 0.0), - release: Envelope::Stage.new(0.5, 1.0, 0.0) + release: Envelope::Stage.new(0.3, 1.0, 0.0) ) - @wave = Sound.triangle_wave(6.0, 0.0005) + + @wave = ->(time : Float64, hertz : Float64) do + # https://www.desmos.com/calculator/mnxargxllk + av = 2 * Math::PI * hertz * time + y = (Math.sin(Math::PI * (av / Math::PI)) ** 3) + Math.sin(Math::PI * ((av / Math::PI) + (2 / 3))) + y = (Math.sin(av) ** 3) + Math.sin(av + 0.6666) + y += y / 2 + y += y / 4 + y += y / 8 + y += y / 16 + y += y / 32 + y /= 5 + end end end @@ -106,4 +126,23 @@ module PF end end end + + class Harmonica < Instrument + def initialize + @name = "Harmonica" + @envelope = Envelope.new( + attack: Envelope::Stage.new(0.1, 0.0, 1.0), + decay: Envelope::Stage.new(0.3, 1.0, 0.8), + sustain: Envelope::Stage.new(Float64::INFINITY, 0.8, 0.8), + release: Envelope::Stage.new(0.3, 1.0, 0.0) + ) + @volume = 0.5 + wave = Sound.square_wave + @wave = ->(time : Float64, hertz : Float64) do + 0.5 * wave.call(time + 0.1, hertz * 2) + + wave.call(time, hertz) + + rand(-0.05..0.05) + end + end + end end diff --git a/src/audio/note.cr b/src/audio/note.cr index 5149703..fb6d2df 100644 --- a/src/audio/note.cr +++ b/src/audio/note.cr @@ -1,7 +1,8 @@ module PF struct Note - NAMES = %w[C C#/Db D D#/Eb E F F#/Gb G G#/Ab A A#/Bb B] - ACCIDENTALS = StaticArray[1u8, 3u8, 6u8, 8u8, 10u8] + TWELFTH_ROOT = 2 ** (1 / 12) + NAMES = %w[C C#/Db D D#/Eb E F F#/Gb G G#/Ab A A#/Bb B] + ACCIDENTALS = StaticArray[1u8, 3u8, 6u8, 8u8, 10u8] getter tuning : Float64 = 440.0 getter number : Float64