mirror of
https://github.com/SleepingInsomniac/pixelfaucet
synced 2025-01-15 15:41:08 +01:00
Add new audio effects
This commit is contained in:
parent
44935fcbcc
commit
172478cffa
5 changed files with 133 additions and 18 deletions
|
@ -18,13 +18,14 @@ module PF
|
||||||
@white_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String)
|
@white_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String)
|
||||||
@black_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, Harmonica.new]
|
@instruments : Array(Instrument) = [RetroVoice.new, SineVoice.new, PianoVoice.new, Flute.new, KickDrum.new, SnareDrum.new, Harmonica.new]
|
||||||
|
|
||||||
def initialize(*args, **kwargs)
|
def initialize(*args, **kwargs)
|
||||||
super
|
super
|
||||||
|
|
||||||
@text_color = Pixel.new(127, 127, 127)
|
@text_color = Pixel.new(127, 127, 127)
|
||||||
@controller = PF::Controller(Keys).new({
|
@controller = PF::Controller(Keys).new({
|
||||||
|
Keys::E => "echo",
|
||||||
Keys::UP => "octave up",
|
Keys::UP => "octave up",
|
||||||
Keys::DOWN => "octave down",
|
Keys::DOWN => "octave down",
|
||||||
Keys::LEFT => "prev inst",
|
Keys::LEFT => "prev inst",
|
||||||
|
@ -52,19 +53,28 @@ module PF
|
||||||
|
|
||||||
@sounds = [] of Sound
|
@sounds = [] of Sound
|
||||||
@keysdown = {} of String => Tuple(Instrument, UInt32)
|
@keysdown = {} of String => Tuple(Instrument, UInt32)
|
||||||
|
@echo = false
|
||||||
|
|
||||||
|
echo_effect = EchoEffect.new(44100 // 3)
|
||||||
|
|
||||||
# Initialize an audio handler
|
# Initialize an audio handler
|
||||||
# - the given Proc will be called at the sample rate/freq param (44.1Khz is standard)
|
# - the given Proc will be called at the sample rate/freq param (44.1Khz is standard)
|
||||||
# - the channel variable describes which speaker the sample is for
|
# - the channel variable describes which speaker the sample is for
|
||||||
@audio = Audio.new(channels: 1) do |time, channel|
|
@audio = Audio.new(channels: 1) do |time, channel|
|
||||||
value = 0.0
|
value = 0.0
|
||||||
|
|
||||||
@instruments.each do |instrument|
|
@instruments.each do |instrument|
|
||||||
instrument.sounds.each do |sound|
|
instrument.sounds.each do |sound|
|
||||||
value += sound.sample(time)
|
value += sound.sample(time)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if @echo
|
||||||
|
echo_effect.apply(value)
|
||||||
|
else
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@key_size = height // 2 - 25
|
@key_size = height // 2 - 25
|
||||||
@key_width = width // 10
|
@key_width = width // 10
|
||||||
|
@ -115,6 +125,10 @@ module PF
|
||||||
@base_note += 12 if @controller.pressed?("octave up") && @base_note <= 112
|
@base_note += 12 if @controller.pressed?("octave up") && @base_note <= 112
|
||||||
@base_note -= 12 if @controller.pressed?("octave down") && @base_note >= 21 + 12
|
@base_note -= 12 if @controller.pressed?("octave down") && @base_note >= 21 + 12
|
||||||
|
|
||||||
|
if @controller.pressed?("echo")
|
||||||
|
@echo = !@echo
|
||||||
|
end
|
||||||
|
|
||||||
if @controller.pressed?("next inst")
|
if @controller.pressed?("next inst")
|
||||||
@instrument = (@instrument + 1) % @instruments.size
|
@instrument = (@instrument + 1) % @instruments.size
|
||||||
end
|
end
|
||||||
|
@ -134,12 +148,14 @@ module PF
|
||||||
end
|
end
|
||||||
|
|
||||||
if @controller.released?(name)
|
if @controller.released?(name)
|
||||||
instrument, note_id = @keysdown[name]
|
if tuple = @keysdown.[name]?
|
||||||
|
instrument, note_id = tuple
|
||||||
instrument.off(note_id, @audio.time)
|
instrument.off(note_id, @audio.time)
|
||||||
@keysdown.delete(name)
|
@keysdown.delete(name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
clear
|
clear
|
||||||
|
@ -147,7 +163,7 @@ module PF
|
||||||
draw_string(<<-TEXT, 5, 5, @text_color)
|
draw_string(<<-TEXT, 5, 5, @text_color)
|
||||||
Press up/down to change octave, Bottom row of keyboard plays notes
|
Press up/down to change octave, Bottom row of keyboard plays notes
|
||||||
#{@instruments.map(&.name).join(", ")}
|
#{@instruments.map(&.name).join(", ")}
|
||||||
Octave: #{@base_note // 12 - 1}, Voice : #{@instruments[@instrument].name}
|
Octave: #{@base_note // 12 - 1}, Voice: #{@instruments[@instrument].name}, Echo: #{@echo ? "on" : "off"}
|
||||||
#{@instruments[@instrument].sounds.map { |s| s.hertz.round(2) }}
|
#{@instruments[@instrument].sounds.map { |s| s.hertz.round(2) }}
|
||||||
TEXT
|
TEXT
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
module PF
|
module PF
|
||||||
class Audio
|
class Audio
|
||||||
|
alias Callback = Float64, UInt8 -> Float64
|
||||||
# stored as a class variable to avoid garbage collection, since it's passed to a C function
|
# stored as a class variable to avoid garbage collection, since it's passed to a C function
|
||||||
@@box : Pointer(Void)?
|
@@box : Pointer(Void)?
|
||||||
@spec : LibSDL::AudioSpec
|
@spec : LibSDL::AudioSpec
|
||||||
|
@ -12,7 +13,7 @@ module PF
|
||||||
getter time : Float64 = 0.0
|
getter time : Float64 = 0.0
|
||||||
@channel : UInt8 = 0u8
|
@channel : UInt8 = 0u8
|
||||||
|
|
||||||
def initialize(freq : Int32 = 44100, channels : UInt8 = 2, samples : UInt16 = 512, &callback : Float64, UInt8 -> Float64)
|
def initialize(freq : Int32 = 44100, channels : UInt8 = 2, samples : UInt16 = 512, &callback : Callback)
|
||||||
# Information to be passed to the audio callback
|
# Information to be passed to the audio callback
|
||||||
boxed_data = Box.box({
|
boxed_data = Box.box({
|
||||||
callback,
|
callback,
|
||||||
|
|
24
src/audio/echo_effect.cr
Normal file
24
src/audio/echo_effect.cr
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
struct EchoEffect
|
||||||
|
@cursor : Int32 = 0
|
||||||
|
|
||||||
|
def initialize(frames : Int32)
|
||||||
|
@buffer = Slice(Float64).new(frames, 0.0)
|
||||||
|
@filter = LowPassFilter.new(440.0, 0.7, 44100)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read
|
||||||
|
@buffer[@cursor]
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(sample : Float64)
|
||||||
|
@buffer[@cursor] = sample
|
||||||
|
@cursor += 1
|
||||||
|
@cursor = 0 if @cursor >= @buffer.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply(sample : Float64, strength = 0.5)
|
||||||
|
sample += @filter.apply(read * strength)
|
||||||
|
write(sample)
|
||||||
|
sample
|
||||||
|
end
|
||||||
|
end
|
|
@ -41,7 +41,20 @@ module PF
|
||||||
sustain: Envelope::Stage.new(Float64::INFINITY, 0.5, 0.5),
|
sustain: Envelope::Stage.new(Float64::INFINITY, 0.5, 0.5),
|
||||||
release: Envelope::Stage.new(0.5, 1.0, 0.0)
|
release: Envelope::Stage.new(0.5, 1.0, 0.0)
|
||||||
)
|
)
|
||||||
@wave = Sound.saw_wave(7.0, 0.001)
|
@wave = Sound.saw_wave(10.0, 0.005)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class SineVoice < Instrument
|
||||||
|
def initialize
|
||||||
|
@name = "Sine"
|
||||||
|
@envelope = Envelope.new(
|
||||||
|
attack: Envelope::Stage.new(0.01, 0.0, 1.0),
|
||||||
|
decay: Envelope::Stage.new(0.1, 1.0, 0.5),
|
||||||
|
sustain: Envelope::Stage.new(Float64::INFINITY, 0.5, 0.5),
|
||||||
|
release: Envelope::Stage.new(0.5, 1.0, 0.0)
|
||||||
|
)
|
||||||
|
@wave = Sound.sin_wave(5.0, 0.001)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -62,17 +75,25 @@ module PF
|
||||||
release: Envelope::Stage.new(0.3, 1.0, 0.0)
|
release: Envelope::Stage.new(0.3, 1.0, 0.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
harmonics = [
|
||||||
|
{1.0, 0.0},
|
||||||
|
{0.5, 0.0},
|
||||||
|
{0.25, 0.0},
|
||||||
|
]
|
||||||
|
|
||||||
@wave = ->(time : Float64, hertz : Float64) do
|
@wave = ->(time : Float64, hertz : Float64) do
|
||||||
# https://www.desmos.com/calculator/mnxargxllk
|
# https://www.desmos.com/calculator/mnxargxllk
|
||||||
av = 2 * Math::PI * hertz * time
|
y = 0.0
|
||||||
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)
|
harmonics.each do |amplitude, phase|
|
||||||
y += y / 2
|
av = 2 * Math::PI * (hertz / 2.0) * time + phase
|
||||||
y += y / 4
|
y += amplitude * (
|
||||||
y += y / 8
|
(Math.sin(Math::PI * (av / Math::PI)) ** 3) +
|
||||||
y += y / 16
|
(Math.sin(Math::PI * ((av / Math::PI) + (2 / 3))))
|
||||||
y += y / 32
|
)
|
||||||
y /= 5
|
end
|
||||||
|
|
||||||
|
y
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
53
src/audio/low_pass_filter.cr
Normal file
53
src/audio/low_pass_filter.cr
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# first-order low-pass filter
|
||||||
|
class LowPassFilter
|
||||||
|
@x1 : Float64
|
||||||
|
@x2 : Float64
|
||||||
|
@y1 : Float64
|
||||||
|
@y2 : Float64
|
||||||
|
@b0 : Float64
|
||||||
|
@b1 : Float64
|
||||||
|
@b2 : Float64
|
||||||
|
@a1 : Float64
|
||||||
|
@a2 : Float64
|
||||||
|
|
||||||
|
def initialize(cutoff_frequency : Float64, quality_factor : Float64, sample_rate : Float64)
|
||||||
|
# Compute the filter coefficients
|
||||||
|
omega = 2.0 * Math::PI * cutoff_frequency / sample_rate
|
||||||
|
sin_omega = Math.sin(omega)
|
||||||
|
cos_omega = Math.cos(omega)
|
||||||
|
alpha = sin_omega / (2.0 * quality_factor)
|
||||||
|
|
||||||
|
b0 = (1.0 - cos_omega) / 2.0
|
||||||
|
b1 = 1.0 - cos_omega
|
||||||
|
b2 = b0
|
||||||
|
a0 = 1.0 + alpha
|
||||||
|
a1 = -2.0 * cos_omega
|
||||||
|
a2 = 1.0 - alpha
|
||||||
|
|
||||||
|
# Initialize the filter state
|
||||||
|
@x1 = 0.0
|
||||||
|
@x2 = 0.0
|
||||||
|
@y1 = 0.0
|
||||||
|
@y2 = 0.0
|
||||||
|
|
||||||
|
# Store the filter coefficients
|
||||||
|
@b0 = b0 / a0
|
||||||
|
@b1 = b1 / a0
|
||||||
|
@b2 = b2 / a0
|
||||||
|
@a1 = a1 / a0
|
||||||
|
@a2 = a2 / a0
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply(sample : Float64) : Float64
|
||||||
|
# Apply the filter to the sample
|
||||||
|
output_sample = @b0 * sample + @b1 * @x1 + @b2 * @x2 - @a1 * @y1 - @a2 * @y2
|
||||||
|
|
||||||
|
# Update the filter state variables
|
||||||
|
@x2 = @x1
|
||||||
|
@x1 = sample
|
||||||
|
@y2 = @y1
|
||||||
|
@y1 = output_sample
|
||||||
|
|
||||||
|
output_sample
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue