mirror of
https://github.com/SleepingInsomniac/pixelfaucet
synced 2025-01-15 15:41:08 +01:00
Refactor audio synth
This commit is contained in:
parent
22e6f007c4
commit
2cf7bf3df9
11 changed files with 488 additions and 290 deletions
|
@ -4,138 +4,104 @@ require "../src/audio"
|
||||||
require "../src/audio/*"
|
require "../src/audio/*"
|
||||||
|
|
||||||
module PF
|
module PF
|
||||||
enum Instrument : UInt8
|
|
||||||
RetroVoice
|
|
||||||
PianoVoice
|
|
||||||
DrumVoice
|
|
||||||
end
|
|
||||||
|
|
||||||
class RetroVoice < Voice
|
|
||||||
def initialize(@note, time)
|
|
||||||
@envelope = Envelope.new(time,
|
|
||||||
attack_time: 0.01,
|
|
||||||
decay_time: 0.1,
|
|
||||||
sustain_level: 0.5,
|
|
||||||
release_time: 0.5
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def hertz(time)
|
|
||||||
envelope.amplitude(time) * (
|
|
||||||
Oscilator.square(note.hertz, time, 7.0, 0.001)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class PianoVoice < Voice
|
|
||||||
def initialize(@note, time)
|
|
||||||
@envelope = Envelope.new(time,
|
|
||||||
attack_time: 0.001,
|
|
||||||
decay_time: 0.7,
|
|
||||||
sustain_level: 0.0,
|
|
||||||
release_time: 1.0
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def hertz(time)
|
|
||||||
envelope.amplitude(time) * (
|
|
||||||
Oscilator.triangle(note.hertz - 1.5, time - 0.33)
|
|
||||||
Oscilator.saw(note.hertz, time, 5, 3.0, 0.000001) +
|
|
||||||
Oscilator.triangle(note.hertz + 1.5, time + 0.33)
|
|
||||||
) / 3.0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class DrumVoice < Voice
|
|
||||||
def initialize(@note, time)
|
|
||||||
@envelope = Envelope.new(time,
|
|
||||||
attack_time: 0.01,
|
|
||||||
decay_time: 1.0,
|
|
||||||
sustain_level: 0.0,
|
|
||||||
release_time: 0.1
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def hertz(time)
|
|
||||||
envelope.amplitude(time) * (
|
|
||||||
Oscilator.noise(note.hertz + Math.sin(time / 10), time)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Piano < Game
|
class Piano < Game
|
||||||
|
@instrument : UInt8 = 0
|
||||||
|
@base_note : UInt8 = 69 # (in MIDI) - A4 / 440.0Hz
|
||||||
|
|
||||||
|
# Variables for drawing the piano keys
|
||||||
|
@highlight : Pixel = Pixel.new(120, 120, 120)
|
||||||
|
@text_hl : Pixel = Pixel.new(0, 200, 255)
|
||||||
@key_size : Int32
|
@key_size : Int32
|
||||||
@key_width : Int32
|
@key_width : Int32
|
||||||
@middle : Int32
|
@middle : Int32
|
||||||
@keys : UInt32 = 15
|
@keys : UInt32 = 16
|
||||||
@base_octave : Int8 = 4
|
@white_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String)
|
||||||
@accidentals : StaticArray(UInt8, 12) = StaticArray[0u8, 1u8, 0u8, 0u8, 1u8, 0u8, 1u8, 0u8, 0u8, 1u8, 0u8, 1u8]
|
@black_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String)
|
||||||
@highlight : Pixel = Pixel.new(120, 120, 120)
|
|
||||||
@text_hl : Pixel = Pixel.new(0, 200, 255)
|
@instruments : Array(Instrument) = [RetroVoice.new, PianoVoice.new, Flute.new, KickDrum.new, SnareDrum.new]
|
||||||
@instrument : Instrument = Instrument::RetroVoice
|
|
||||||
|
|
||||||
def initialize(*args, **kwargs)
|
def initialize(*args, **kwargs)
|
||||||
super
|
super
|
||||||
|
|
||||||
|
@text_color = Pixel.new(127, 127, 127)
|
||||||
|
@controller = PF::Controller(Keys).new({
|
||||||
|
Keys::UP => "octave up",
|
||||||
|
Keys::DOWN => "octave down",
|
||||||
|
Keys::LEFT => "prev inst",
|
||||||
|
Keys::RIGHT => "next inst",
|
||||||
|
|
||||||
|
Keys::Z => "A",
|
||||||
|
Keys::S => "A#/Bb",
|
||||||
|
Keys::X => "B",
|
||||||
|
Keys::C => "C",
|
||||||
|
Keys::F => "C#/Db",
|
||||||
|
Keys::V => "D",
|
||||||
|
Keys::G => "D#/Eb",
|
||||||
|
Keys::B => "E",
|
||||||
|
Keys::N => "F",
|
||||||
|
Keys::J => "F#/Gb",
|
||||||
|
Keys::M => "G",
|
||||||
|
Keys::K => "G#/Ab",
|
||||||
|
Keys::COMMA => "A+",
|
||||||
|
Keys::L => "A#/Bb+",
|
||||||
|
Keys::PERIOD => "B+",
|
||||||
|
Keys::SLASH => "C+",
|
||||||
|
Keys::APOSTROPHE => "C#/Db+",
|
||||||
|
})
|
||||||
|
|
||||||
|
@sounds = [] of Sound
|
||||||
|
@keysdown = {} of String => Tuple(Instrument, UInt32)
|
||||||
|
|
||||||
|
# Initialize an audio handler
|
||||||
|
# - 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
|
||||||
|
@audio = Audio.new(channels: 1) do |time, channel|
|
||||||
|
value = 0.0
|
||||||
|
@instruments.each do |instrument|
|
||||||
|
instrument.sounds.each do |sound|
|
||||||
|
value += sound.sample(time)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
value
|
||||||
|
end
|
||||||
|
|
||||||
@key_size = height // 2 - 25
|
@key_size = height // 2 - 25
|
||||||
@key_width = width // 10
|
@key_width = width // 10
|
||||||
@middle = (height // 2) + 25
|
@middle = (height // 2) + 25
|
||||||
|
|
||||||
@text_color = Pixel.new(127, 127, 127)
|
calculate_keys
|
||||||
@controller = PF::Controller(Keys).new({
|
|
||||||
Keys::UP => "up",
|
|
||||||
Keys::DOWN => "down",
|
|
||||||
Keys::KEY_1 => "1",
|
|
||||||
Keys::KEY_2 => "2",
|
|
||||||
Keys::KEY_3 => "3",
|
|
||||||
Keys::KEY_4 => "4",
|
|
||||||
Keys::KEY_5 => "5",
|
|
||||||
Keys::KEY_6 => "6",
|
|
||||||
Keys::KEY_7 => "7",
|
|
||||||
Keys::KEY_8 => "8",
|
|
||||||
Keys::KEY_9 => "9",
|
|
||||||
Keys::KEY_0 => "0",
|
|
||||||
Keys::Z => "A",
|
|
||||||
Keys::S => "AS",
|
|
||||||
Keys::X => "B",
|
|
||||||
Keys::C => "C",
|
|
||||||
Keys::F => "CS",
|
|
||||||
Keys::V => "D",
|
|
||||||
Keys::G => "DS",
|
|
||||||
Keys::B => "E",
|
|
||||||
Keys::N => "F",
|
|
||||||
Keys::J => "FS",
|
|
||||||
Keys::M => "G",
|
|
||||||
Keys::K => "GS",
|
|
||||||
Keys::COMMA => "A+",
|
|
||||||
Keys::L => "AS+",
|
|
||||||
Keys::PERIOD => "B+",
|
|
||||||
Keys::SLASH => "C+",
|
|
||||||
})
|
|
||||||
|
|
||||||
@sounds = [] of Voice
|
# Without this, the audio will not make noise
|
||||||
@keysdown = {} of String => Voice
|
@audio.play
|
||||||
|
end
|
||||||
|
|
||||||
@audio = Audio.new(channels: 1) do |time, channel|
|
def calculate_keys(base : UInt8 = @base_note)
|
||||||
@sounds.reduce(0.0) do |total, sound|
|
pos = 0
|
||||||
total + sound.hertz(time)
|
|
||||||
end
|
while @white_keys.size > 0
|
||||||
|
@white_keys.pop
|
||||||
end
|
end
|
||||||
|
|
||||||
@audio.play
|
while @black_keys.size > 0
|
||||||
|
@black_keys.pop
|
||||||
|
end
|
||||||
|
|
||||||
@white_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String)
|
0.upto(@keys) do |n|
|
||||||
@black_keys = [] of Tuple(Vector2(Int32), Vector2(Int32), String)
|
note = Note.new(@base_note + n)
|
||||||
|
name = n > 11 ? note.name + "+" : note.name
|
||||||
|
|
||||||
pos = 0
|
unless note.accidental?
|
||||||
(Note::NOTES + %w[A+ AS+ B+ C+]).map_with_index do |name, i|
|
# Calculate the position of a white key
|
||||||
if @accidentals[i % 12] == 0
|
|
||||||
top_left = Vector[@key_width * pos, @middle - @key_size]
|
top_left = Vector[@key_width * pos, @middle - @key_size]
|
||||||
bottom_right = Vector[(@key_width * pos) + @key_width, @middle + @key_size]
|
bottom_right = Vector[(@key_width * pos) + @key_width, @middle + @key_size]
|
||||||
@white_keys << {top_left, bottom_right, name}
|
@white_keys << {top_left, bottom_right, name}
|
||||||
|
# position from the left is increased by 1 for every white key
|
||||||
pos += 1
|
pos += 1
|
||||||
else
|
else
|
||||||
|
# Calculate the position of a black key
|
||||||
|
# Black keys are thinner than white keys (space in between the black keys)
|
||||||
shrinkage = (@key_width // 8)
|
shrinkage = (@key_width // 8)
|
||||||
|
# black keys are at the same position as the last, but half as tall and offset by half the width.
|
||||||
left = (@key_width * pos) - (@key_width // 2) + shrinkage
|
left = (@key_width * pos) - (@key_width // 2) + shrinkage
|
||||||
top_left = Vector[left, @middle - @key_size]
|
top_left = Vector[left, @middle - @key_size]
|
||||||
bottom_right = Vector[left + @key_width - (shrinkage * 2), @middle]
|
bottom_right = Vector[left + @key_width - (shrinkage * 2), @middle]
|
||||||
|
@ -147,46 +113,44 @@ module PF
|
||||||
def update(dt, event)
|
def update(dt, event)
|
||||||
@controller.map_event(event)
|
@controller.map_event(event)
|
||||||
|
|
||||||
@base_octave = ((@base_octave + 1) % 8) if @controller.pressed?("up")
|
@base_note += 12 if @controller.pressed?("octave up") && @base_note <= 112
|
||||||
@base_octave = ((@base_octave - 1) % 8) if @controller.pressed?("down")
|
@base_note -= 12 if @controller.pressed?("octave down") && @base_note >= 21 + 12
|
||||||
@instrument = Instrument::RetroVoice if @controller.pressed?("1")
|
|
||||||
@instrument = Instrument::PianoVoice if @controller.pressed?("2")
|
|
||||||
@instrument = Instrument::DrumVoice if @controller.pressed?("3")
|
|
||||||
|
|
||||||
{% for name, n in Note::NOTES + %w[A+ AS+ B+ C+] %}
|
if @controller.pressed?("next inst")
|
||||||
if @controller.pressed?({{name}})
|
@instrument = (@instrument + 1) % @instruments.size
|
||||||
voice = case @instrument
|
end
|
||||||
when Instrument::RetroVoice
|
|
||||||
RetroVoice.new(Note.new({{n}}_i8, @base_octave), @audio.time)
|
if @controller.pressed?("prev inst")
|
||||||
when Instrument::PianoVoice
|
@instrument = @instruments.size.to_u8 if @instrument == 0
|
||||||
PianoVoice.new(Note.new({{n}}_i8, @base_octave), @audio.time)
|
@instrument -= 1
|
||||||
when Instrument::DrumVoice
|
end
|
||||||
DrumVoice.new(Note.new({{n}}_i8, @base_octave), @audio.time)
|
|
||||||
else
|
0.upto(@keys) do |n|
|
||||||
PianoVoice.new(Note.new({{n}}_i8, @base_octave), @audio.time)
|
note = Note.new(n + @base_note)
|
||||||
end
|
name = n > 11 ? note.name + "+" : note.name
|
||||||
@keysdown[{{name}}] = voice
|
|
||||||
@sounds << voice
|
if @controller.pressed?(name)
|
||||||
|
note_id = @instruments[@instrument].on(note.hertz, @audio.time)
|
||||||
|
@keysdown[name] = {@instruments[@instrument], note_id}
|
||||||
end
|
end
|
||||||
|
|
||||||
if @controller.released?({{name}})
|
if @controller.released?(name)
|
||||||
@keysdown[{{name}}].release(@audio.time)
|
instrument, note_id = @keysdown[name]
|
||||||
@keysdown.delete({{name}})
|
instrument.off(note_id, @audio.time)
|
||||||
|
@keysdown.delete(name)
|
||||||
end
|
end
|
||||||
{% end %}
|
end
|
||||||
|
|
||||||
@sounds.reject!(&.finished?)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
clear
|
clear
|
||||||
|
|
||||||
text = <<-TEXT
|
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
|
||||||
1 : RetroVoice, 2 : PianoVoice
|
#{@instruments.map(&.name).join(", ")}
|
||||||
Octave: #{@base_octave}, Voice : #{@instrument}
|
Octave: #{@base_note // 12 - 1}, Voice : #{@instruments[@instrument].name}
|
||||||
TEXT
|
#{@instruments[@instrument].sounds.map { |s| s.hertz.round(2) }}
|
||||||
draw_string(text, 5, 5, @text_color)
|
TEXT
|
||||||
|
|
||||||
@white_keys.each do |key|
|
@white_keys.each do |key|
|
||||||
top_left, bottom_right, name = key
|
top_left, bottom_right, name = key
|
||||||
|
|
51
spec/audio/envelope_spec.cr
Normal file
51
spec/audio/envelope_spec.cr
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
require "../spec_helper"
|
||||||
|
require "../../src/audio/envelope"
|
||||||
|
|
||||||
|
include PF
|
||||||
|
|
||||||
|
describe Envelope do
|
||||||
|
describe "#stage" do
|
||||||
|
it "returns the correct current stage" do
|
||||||
|
attack = Envelope::Stage.new(0.5, 0.0, 1.0)
|
||||||
|
decay = Envelope::Stage.new(0.1, 1.0, 0.8)
|
||||||
|
sustain = Envelope::Stage.new(Float64::INFINITY, 0.8, 0.8)
|
||||||
|
release = Envelope::Stage.new(0.5, 1.0, 0.0)
|
||||||
|
|
||||||
|
env = Envelope.new(attack, decay, sustain, release)
|
||||||
|
|
||||||
|
stage, time = env.stage(0.4)
|
||||||
|
stage.should eq(attack)
|
||||||
|
time.round(2).should eq(0.4)
|
||||||
|
|
||||||
|
stage, time = env.stage(0.51)
|
||||||
|
stage.should eq(decay)
|
||||||
|
time.round(2).should eq(0.01)
|
||||||
|
|
||||||
|
stage, time = env.stage(0.61)
|
||||||
|
stage.should eq(sustain)
|
||||||
|
time.round(2).should eq(0.01)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#amplitude" do
|
||||||
|
it "returns a known amplitude" do
|
||||||
|
attack = Envelope::Stage.new(1.0, 0.0, 1.0)
|
||||||
|
decay = Envelope::Stage.new(1.0, 1.0, 0.8)
|
||||||
|
sustain = Envelope::Stage.new(Float64::INFINITY, 0.8, 0.8)
|
||||||
|
release = Envelope::Stage.new(1.0, 1.0, 0.0)
|
||||||
|
|
||||||
|
env = Envelope.new(attack, decay, sustain, release)
|
||||||
|
|
||||||
|
# half attack
|
||||||
|
env.amplitude(time: 1.0, started_at: 0.5).should eq(0.5)
|
||||||
|
# peak
|
||||||
|
env.amplitude(time: 1.5, started_at: 0.5).should eq(1.0)
|
||||||
|
# half decay
|
||||||
|
env.amplitude(time: 2.0, started_at: 0.5).should eq(0.9)
|
||||||
|
# sustain
|
||||||
|
env.amplitude(time: 2.6, started_at: 0.5).should eq(0.8)
|
||||||
|
# release at half of release time (sustain / 2)
|
||||||
|
env.amplitude(time: 3.0, started_at: 0.5, released_at: 2.5).should eq(0.4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
src/audio.cr
19
src/audio.cr
|
@ -1,16 +1,19 @@
|
||||||
module PF
|
module PF
|
||||||
class Audio
|
class Audio
|
||||||
|
# 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
|
||||||
@device_id : LibSDL::AudioDeviceID
|
@device_id : LibSDL::AudioDeviceID
|
||||||
property volume = 0.5
|
property volume = 0.5
|
||||||
property clipping_scaler = 0.5
|
# https://dsp.stackexchange.com/questions/3581/algorithms-to-mix-audio-signals-without-clipping
|
||||||
|
property headroom = 0.4
|
||||||
delegate :freq, to: @spec
|
delegate :freq, to: @spec
|
||||||
@playing : Bool = false
|
@playing : Bool = false
|
||||||
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 : Float64, UInt8 -> Float64)
|
||||||
|
# Information to be passed to the audio callback
|
||||||
boxed_data = Box.box({
|
boxed_data = Box.box({
|
||||||
callback,
|
callback,
|
||||||
(1 / freq) / channels, # the time per sample
|
(1 / freq) / channels, # the time per sample
|
||||||
|
@ -18,7 +21,7 @@ module PF
|
||||||
pointerof(@time),
|
pointerof(@time),
|
||||||
pointerof(@channel),
|
pointerof(@channel),
|
||||||
channels,
|
channels,
|
||||||
pointerof(@clipping_scaler),
|
pointerof(@headroom),
|
||||||
})
|
})
|
||||||
@@box = boxed_data
|
@@box = boxed_data
|
||||||
|
|
||||||
|
@ -28,24 +31,24 @@ module PF
|
||||||
channels: channels,
|
channels: channels,
|
||||||
samples: samples,
|
samples: samples,
|
||||||
callback: ->(userdata : Void*, stream : UInt8*, len : Int32) {
|
callback: ->(userdata : Void*, stream : UInt8*, len : Int32) {
|
||||||
# return if len == 0
|
# Convert the stream into the correct type, AUDIO_S16SYS is a signed 16 bit integer
|
||||||
# Convert the stream into the correct type
|
|
||||||
stream = stream.as(Pointer(Int16))
|
stream = stream.as(Pointer(Int16))
|
||||||
# Calculate the correct length in size of Int16 (according to audio spec AUDIO_S16SYS)
|
# Calculate the correct length in size of Int16 (according to audio spec AUDIO_S16SYS)
|
||||||
length = len // (sizeof(Int16) // sizeof(UInt8))
|
length = len // (sizeof(Int16) // sizeof(UInt8))
|
||||||
# Unbox the user callback
|
# Unbox the user callback and other data
|
||||||
unboxed_data = Box(Tuple(typeof(callback), Float64, Float64*, Float64*, UInt8*, UInt8, Float64*)).unbox(userdata)
|
unboxed_data = Box(Tuple(typeof(callback), Float64, Float64*, Float64*, UInt8*, UInt8, Float64*)).unbox(userdata)
|
||||||
user_callback, time_step, volume, time, channel, channel_count, clipping_scaler = unboxed_data
|
user_callback, time_step, volume, time, channel, channel_count, headroom = unboxed_data
|
||||||
# Iterate over the size of the buffer
|
# Iterate over the size of the buffer
|
||||||
0.upto(length - 1) do |x|
|
0.upto(length - 1) do |x|
|
||||||
# Call the user callback and recieve the sample
|
# Call the user callback and recieve the sample
|
||||||
sample = user_callback.call(time.value, channel.value)
|
sample = user_callback.call(time.value, channel.value)
|
||||||
|
# Channel is incremented every sample, because samples are interlaced
|
||||||
channel.value = (channel.value + 1) % channel_count
|
channel.value = (channel.value + 1) % channel_count
|
||||||
# Increment the time
|
# Increment the time, time_step was calculated as 1 out of the audio frequency divided by number of channels
|
||||||
time.value += time_step
|
time.value += time_step
|
||||||
# Fill the buffer location with the sample
|
# Fill the buffer location with the sample
|
||||||
# Make sure to convert the Float64 into a signed Int16 for compatability with the audio format
|
# Make sure to convert the Float64 into a signed Int16 for compatability with the audio format
|
||||||
(stream + x).value = (sample * Int16::MAX * volume.value * clipping_scaler.value).clamp(Int16::MIN, Int16::MAX).to_i16
|
(stream + x).value = (sample * Int16::MAX * volume.value * headroom.value).clamp(Int16::MIN, Int16::MAX).to_i16
|
||||||
end
|
end
|
||||||
},
|
},
|
||||||
userdata: boxed_data
|
userdata: boxed_data
|
||||||
|
|
|
@ -1,64 +1,92 @@
|
||||||
module PF
|
module PF
|
||||||
struct Envelope
|
# Enevelope represents an ADSR cycle to control the amplitude of a sound throughout its lifecycle
|
||||||
@attack_time : Float64 = 0.05
|
class Envelope
|
||||||
@decay_time : Float64 = 0.1
|
# An Envelope::Stage is a slice of time within an enveleope (Either A,D,S, or R)
|
||||||
@sustain_level : Float64 = 0.8
|
struct Stage
|
||||||
@release_time : Float64 = 0.5
|
# Linear interpolation function
|
||||||
@initial_level : Float64 = 1.0
|
def self.lerp
|
||||||
|
->(time : Float64, duration : Float64, initial : Float64, level : Float64) do
|
||||||
@started_at : Float64 = 0.0
|
initial + (time / duration) * (level - initial)
|
||||||
@released_at : Float64? = nil
|
|
||||||
@released : Bool = false
|
|
||||||
@finished : Bool = false
|
|
||||||
|
|
||||||
def initialize(@started_at : Float64, @attack_time = 0.05, @decay_time = 0.1, @sustain_level = 0.8, @release_time = 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
def finished?
|
|
||||||
@finished
|
|
||||||
end
|
|
||||||
|
|
||||||
def amplitude(time : Float64)
|
|
||||||
amp = 0.0
|
|
||||||
|
|
||||||
duration = time - @started_at
|
|
||||||
|
|
||||||
if duration <= @attack_time
|
|
||||||
# Attack phase
|
|
||||||
amp = (duration / @attack_time) * @initial_level
|
|
||||||
elsif duration > @attack_time && duration <= (@attack_time + @decay_time)
|
|
||||||
# Decay phase
|
|
||||||
amp = ((duration - @attack_time) / @decay_time) * (@sustain_level - @initial_level) + @initial_level
|
|
||||||
else
|
|
||||||
if released_at = @released_at
|
|
||||||
duration = time - released_at
|
|
||||||
|
|
||||||
if duration <= @release_time
|
|
||||||
# Release phase
|
|
||||||
amp = ((duration / @release_time) * (-@sustain_level)) + @sustain_level
|
|
||||||
else
|
|
||||||
@finished = true
|
|
||||||
amp = 0.0
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# Sustain phase
|
|
||||||
amp = @sustain_level
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
amp < 0.0001 ? 0.0 : amp
|
# Pulsating linear interpolation
|
||||||
|
def self.wavy_lerp(hertz : Float64 = 50, amount : Float64 = 0.7)
|
||||||
|
->(time : Float64, duration : Float64, initial : Float64, level : Float64) do
|
||||||
|
lerp = (initial + (time / duration) * (level - initial))
|
||||||
|
(1.0 - amount) * lerp * Math.sin(time * hertz) + (amount * lerp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# The length of time in seconds that this stage lasts
|
||||||
|
property duration : Float64
|
||||||
|
|
||||||
|
# The initial level of the amplitude
|
||||||
|
property initial : Float64 = 0.0
|
||||||
|
|
||||||
|
# The finial level of the amplitude
|
||||||
|
property level : Float64 = 1.0
|
||||||
|
|
||||||
|
# This function determines the shape of this stage (defaults to linear)
|
||||||
|
# params: time, duration, initial, level
|
||||||
|
property shape : Float64, Float64, Float64, Float64 -> Float64 = Stage.lerp
|
||||||
|
|
||||||
|
def initialize(@duration, @initial = 1.0, @level = 1.0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(@duration, @initial, @level, @shape)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(@duration, @initial = 1.0, @level = 1.0, &@shape : Float64, Float64, Float64, Float64 -> Float64)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get the amplitude for this stage for *time*
|
||||||
|
# *time* should be relative to the start of this stage
|
||||||
|
def amplitude(time : Float64)
|
||||||
|
return 0.0 if time > @duration
|
||||||
|
shape.call(time, @duration, @initial, @level)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def held?
|
property attack : Stage = Stage.new(0.5, 0.0, 1.0)
|
||||||
@released_at.nil?
|
property decay : Stage = Stage.new(0.1, 1.0, 0.8)
|
||||||
|
property sustain : Stage = Stage.new(Float64::INFINITY, 0.8, 0.8)
|
||||||
|
property release : Stage = Stage.new(0.5, 1.0, 0.0)
|
||||||
|
|
||||||
|
def initialize
|
||||||
end
|
end
|
||||||
|
|
||||||
def released?
|
def initialize(@attack, @decay, @sustain, @release)
|
||||||
!@released_at.nil?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def release(time : Float64)
|
# The length of time this envelope should last for
|
||||||
@released_at = time
|
# note: might be inifinite if sustain has an infinite duration
|
||||||
|
def duration
|
||||||
|
attack.duration + decay.duration + sustain.duration + release.duration
|
||||||
|
end
|
||||||
|
|
||||||
|
# Given a *relative_time* to when the current stage of the envelope was started,
|
||||||
|
# returns the current stage (ADSR), along with the relative_time into that stage
|
||||||
|
def stage(relative_time : Float64)
|
||||||
|
return {@attack, relative_time} if relative_time < @attack.duration
|
||||||
|
relative_time -= @attack.duration
|
||||||
|
return {@decay, relative_time} if relative_time < @decay.duration
|
||||||
|
relative_time -= @decay.duration
|
||||||
|
return {@sustain, relative_time}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Givin an absolute *time*, along with when the envelope was *started_at*, and *released_at?*
|
||||||
|
# returns the current aplitude of the enveloped sound
|
||||||
|
def amplitude(time : Float64, started_at : Float64, released_at : Float64? = nil)
|
||||||
|
current_stage, relative_time = stage(time - started_at)
|
||||||
|
amp = current_stage.amplitude(relative_time)
|
||||||
|
|
||||||
|
if released_at
|
||||||
|
# The release stage is calculated based on the time into the current stage
|
||||||
|
amp * @release.amplitude(time - released_at)
|
||||||
|
else
|
||||||
|
amp
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
109
src/audio/instrument.cr
Normal file
109
src/audio/instrument.cr
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
module PF
|
||||||
|
class Instrument
|
||||||
|
property name : String = "Unnamed Instrument"
|
||||||
|
property envelope : Envelope
|
||||||
|
property wave : Sound::Wave
|
||||||
|
|
||||||
|
getter sounds : Array(Sound) = [] of Sound
|
||||||
|
@notes : Hash(UInt32, Sound) = {} of UInt32 => Sound
|
||||||
|
@note_id = 0_u32
|
||||||
|
|
||||||
|
def initialize(@envelope, @wave)
|
||||||
|
end
|
||||||
|
|
||||||
|
def on(hertz : Float64, time : Float64)
|
||||||
|
@note_id += 1_u32
|
||||||
|
sound = Sound.new(hertz, @envelope, time, @wave)
|
||||||
|
@notes[@note_id] = sound
|
||||||
|
@sounds << sound
|
||||||
|
@note_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def off(note_id : UInt32, time : Float64)
|
||||||
|
sound = @notes[note_id]
|
||||||
|
sound.release!(time)
|
||||||
|
|
||||||
|
spawn do
|
||||||
|
sleep @envelope.release.duration
|
||||||
|
@sounds.delete(sound)
|
||||||
|
@notes.delete(note_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class RetroVoice < Instrument
|
||||||
|
def initialize
|
||||||
|
@name = "Retro"
|
||||||
|
@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.saw_wave(7.0, 0.001)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class PianoVoice < Instrument
|
||||||
|
def initialize
|
||||||
|
@name = "Piano"
|
||||||
|
@envelope = Envelope.new(
|
||||||
|
attack: Envelope::Stage.new(0.001, 0.0, 1.0),
|
||||||
|
decay: Envelope::Stage.new(0.7, 1.0, 0.0),
|
||||||
|
sustain: Envelope::Stage.new(0.0, 0.0, 0.0),
|
||||||
|
release: Envelope::Stage.new(0.5, 1.0, 0.0)
|
||||||
|
)
|
||||||
|
@wave = Sound.triangle_wave(6.0, 0.0005)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Flute < Instrument
|
||||||
|
def initialize
|
||||||
|
@name = "Flute"
|
||||||
|
@envelope = Envelope.new(
|
||||||
|
attack: Envelope::Stage.new(0.1, 0.0, 1.0),
|
||||||
|
decay: Envelope::Stage.new(0.3, 1.0, 0.7),
|
||||||
|
sustain: Envelope::Stage.new(5.0, 0.7, 0.0),
|
||||||
|
release: Envelope::Stage.new(0.5, 1.0, 0.0)
|
||||||
|
)
|
||||||
|
@wave = Sound.sin_wave(5.0, 0.001)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class KickDrum < Instrument
|
||||||
|
def initialize
|
||||||
|
@name = "KickDrum"
|
||||||
|
@envelope = Envelope.new(
|
||||||
|
attack: Envelope::Stage.new(0.0005, 0.0, 1.0),
|
||||||
|
decay: Envelope::Stage.new(0.052, 1.0, 0.0, Envelope::Stage.wavy_lerp(60, 1.0)),
|
||||||
|
sustain: Envelope::Stage.new(0.0, 0.0, 0.0),
|
||||||
|
release: Envelope::Stage.new(0.3, 1.0, 0.0)
|
||||||
|
)
|
||||||
|
@wave = ->(time : Float64, hertz : Float64) do
|
||||||
|
hertz = 180.31
|
||||||
|
av = 2 * Math::PI * (hertz / 2.0) * time
|
||||||
|
drop_time = 10.0
|
||||||
|
drop = (drop_time - time) / drop_time
|
||||||
|
Math.cos(av * drop - 1.0) * 3.0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class SnareDrum < Instrument
|
||||||
|
def initialize
|
||||||
|
@name = "SnareDrum"
|
||||||
|
@envelope = Envelope.new(
|
||||||
|
attack: Envelope::Stage.new(0.0005, 0.0, 1.0),
|
||||||
|
decay: Envelope::Stage.new(0.052, 1.0, 0.0, Envelope::Stage.wavy_lerp(60, 1.0)),
|
||||||
|
sustain: Envelope::Stage.new(0.0, 0.0, 0.0),
|
||||||
|
release: Envelope::Stage.new(0.3, 1.0, 0.0)
|
||||||
|
)
|
||||||
|
@wave = ->(time : Float64, hertz : Float64) do
|
||||||
|
av = 2 * Math::PI * (hertz / 2.0) * time
|
||||||
|
drop_time = 10.0
|
||||||
|
drop = (drop_time - time) / drop_time
|
||||||
|
Math.cos(av * drop - 1.0) * 3.0 + rand(-0.2..0.2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,49 +1,74 @@
|
||||||
module PF
|
module PF
|
||||||
struct Note
|
struct Note
|
||||||
TWELFTH_ROOT = 2 ** (1 / 12)
|
NAMES = %w[C C#/Db D D#/Eb E F F#/Gb G G#/Ab A A#/Bb B]
|
||||||
NOTES = %w[A AS B C CS D DS E F FS G GS]
|
ACCIDENTALS = StaticArray[1u8, 3u8, 6u8, 8u8, 10u8]
|
||||||
|
|
||||||
property note : Int8 = 0
|
getter tuning : Float64 = 440.0
|
||||||
property octave : Int8 = 4
|
getter number : Float64
|
||||||
|
@hertz : Float64? = nil
|
||||||
|
@index : UInt8? = nil
|
||||||
|
@name : String? = nil
|
||||||
|
@is_accidental : Bool? = nil
|
||||||
|
|
||||||
def initialize
|
def initialize(@number, @tuning = 440.0)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(@note, @octave = 4i8)
|
def initialize(number : Number, tuning : Number = 440.0)
|
||||||
|
@number, @tuning = number.to_f, tuning.to_f
|
||||||
end
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
NOTES[@note % 12]
|
@name ||= NAMES[index]
|
||||||
end
|
end
|
||||||
|
|
||||||
def base_hertz
|
def index
|
||||||
27.5 * (2 ** @octave)
|
@index ||= @number.to_u8 % 12
|
||||||
|
end
|
||||||
|
|
||||||
|
def octave
|
||||||
|
(@number.to_i // 12) - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def accidental?
|
||||||
|
@is_accidental ||= ACCIDENTALS.includes?(index)
|
||||||
end
|
end
|
||||||
|
|
||||||
def hertz
|
def hertz
|
||||||
base_hertz * (TWELFTH_ROOT ** @note)
|
@hertz ||= tuning * ((2 ** ((@number - 69) / 12)))
|
||||||
end
|
end
|
||||||
|
|
||||||
def +(value : UInt8)
|
def tuning=(value : Float64)
|
||||||
octave_shift, note = (@note.to_i + value).divmod(12)
|
Note.new(@number, value)
|
||||||
octave = (@octave + octave_shift).clamp(0i8, 8i8)
|
|
||||||
Note.new(@note + value, @octave)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def -(value : Int)
|
def note=(value : Float64)
|
||||||
octave_shift, note = (@note.to_i - value).divmod(12)
|
Note.new(value, @tuning)
|
||||||
octave = (@octave + octave_shift).clamp(0i8, 8i8)
|
end
|
||||||
Note.new(note.to_i8, octave.to_i8)
|
|
||||||
|
def +(value : Float64)
|
||||||
|
Note.new(@number + value, tuning)
|
||||||
|
end
|
||||||
|
|
||||||
|
def -(value : Float64)
|
||||||
|
Note.new(@number - value, tuning)
|
||||||
|
end
|
||||||
|
|
||||||
|
def *(value : Float64)
|
||||||
|
Note.new(@number * value, tuning)
|
||||||
|
end
|
||||||
|
|
||||||
|
def /(value : Float64)
|
||||||
|
Note.new(@number / value, tuning)
|
||||||
end
|
end
|
||||||
|
|
||||||
# # Decabells to volume
|
# # Decabells to volume
|
||||||
# def db_to_volume(db : Float64)
|
# def db_to_volume(db : Float64)
|
||||||
# 10.0 ** (0.05 * db)
|
# 10.0 ** (0.05 * db)
|
||||||
# end
|
# end
|
||||||
#
|
|
||||||
# # Volume to decabells
|
# # Volume to decabells
|
||||||
# def volume_to_db(volume : Float64)
|
# def volume_to_db(volume : Float64)
|
||||||
# 20.0 * Math.log10f(volume)
|
# 20.0 * Math.log(volume, 10)
|
||||||
# end
|
# end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
module PF
|
|
||||||
module Oscilator
|
|
||||||
TWELFTH_ROOT = 2 ** (1 / 12)
|
|
||||||
|
|
||||||
def self.base_freq(hertz : Float64, time : Float64, lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0)
|
|
||||||
av(hertz) * time + lfo_amp * hertz * Math.sin(av(lfo_hertz) * time)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Calculate a sine wave ~~~~~~~~
|
|
||||||
def self.sin(hertz : Float64, time : Float64, lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0)
|
|
||||||
Math.sin(base_freq(hertz, time, lfo_hertz, lfo_amp))
|
|
||||||
# Math.sin(av(hertz) * time)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Calculate a square wave _|-|_|-|_
|
|
||||||
def self.square(hertz : Float64, time : Float64, lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0)
|
|
||||||
Math.sin(base_freq(hertz, time, lfo_hertz, lfo_amp)) > 0 ? 1.0 : 0.0
|
|
||||||
end
|
|
||||||
|
|
||||||
# Calculate a triangle wave /\/\/\/\/
|
|
||||||
def self.triangle(hertz : Float64, time : Float64, lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0)
|
|
||||||
Math.asin(Math.sin(base_freq(hertz, time, lfo_hertz, lfo_amp))) * 2.0 / Math::PI
|
|
||||||
end
|
|
||||||
|
|
||||||
# Calculate a sawtooth wave by addition of sine waves
|
|
||||||
# the more *sins* specified, the closer the waveform will
|
|
||||||
# match a straight sawtooth wave
|
|
||||||
# /|/|/|/|
|
|
||||||
def self.saw(hertz : Float64, time : Float64, sins : Int, lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0)
|
|
||||||
value = 0.0
|
|
||||||
n = 0.0
|
|
||||||
while (n += 1.0) < sins
|
|
||||||
value += Math.sin(n * base_freq(hertz, time, lfo_hertz, lfo_amp)) / n
|
|
||||||
end
|
|
||||||
value * (2.0 / Math::PI)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Calculate a sawtooth wave
|
|
||||||
def self.saw(hertz : Float64, time : Float64, lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0)
|
|
||||||
(2.0 / Math::PI) * (hertz * Math::PI * (time % (1.0 / hertz)) - (Math::PI / 2.0))
|
|
||||||
end
|
|
||||||
|
|
||||||
# Produces static noise
|
|
||||||
def self.noise(hertz : Float64, time : Float64, lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0)
|
|
||||||
rand(-1.0..1.0)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Convert hertz into angular velocity
|
|
||||||
def self.av(hertz)
|
|
||||||
2.0 * Math::PI * hertz
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
81
src/audio/sound.cr
Normal file
81
src/audio/sound.cr
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
module PF
|
||||||
|
class Sound
|
||||||
|
private TWO_PI = 2 * Math::PI
|
||||||
|
# Params: time, hertz
|
||||||
|
alias Wave = Float64, Float64 -> Float64
|
||||||
|
|
||||||
|
# Calculate a sine wave ~~~~~~~~
|
||||||
|
def self.sin_wave(lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0) : Wave
|
||||||
|
->(time : Float64, hertz : Float64) do
|
||||||
|
Math.sin(TWO_PI * hertz * time +
|
||||||
|
lfo_amp * hertz * Math.sin(TWO_PI * lfo_hertz * time))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# # Calculate a square wave _|-|_|-|_
|
||||||
|
def self.square_wave(lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0) : Wave
|
||||||
|
->(time : Float64, hertz : Float64) do
|
||||||
|
Math.sin(TWO_PI * hertz * time +
|
||||||
|
lfo_amp * hertz * Math.sin(TWO_PI * lfo_hertz * time)) > 0 ? 0.7 : -0.7
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculate a triangle wave /\/\/\/\/
|
||||||
|
def self.triangle_wave(lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0) : Wave
|
||||||
|
->(time : Float64, hertz : Float64) do
|
||||||
|
Math.asin(Math.sin(
|
||||||
|
TWO_PI * hertz * time +
|
||||||
|
lfo_amp * hertz * Math.sin(TWO_PI * lfo_hertz * time)
|
||||||
|
)) * (2 / Math::PI)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculate a sawtooth wave by addition of sine waves
|
||||||
|
# the more *sins* specified, the closer the waveform will
|
||||||
|
# match a straight sawtooth wave
|
||||||
|
# /|/|/|/|
|
||||||
|
def self.saw_wave(sins : Int, lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0) : Wave
|
||||||
|
->(time : Float64, hertz : Float64) do
|
||||||
|
value = 0.0
|
||||||
|
n = 0.0
|
||||||
|
while (n += 1.0) < sins
|
||||||
|
value += Math.sin(TWO_PI * hertz * time + lfo_amp * hertz * Math.sin(TWO_PI * lfo_hertz * time)) / n
|
||||||
|
end
|
||||||
|
value * (2.0 / Math::PI)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculate a sawtooth wave
|
||||||
|
# /|/|/|/|
|
||||||
|
def self.saw_wave(lfo_hertz : Float64 = 0.0, lfo_amp : Float64 = 0.0) : Wave
|
||||||
|
->(time : Float64, hertz : Float64) do
|
||||||
|
(2.0 / Math::PI) * (hertz * Math::PI * (time % (1.0 / hertz)) - (Math::PI / 2.0))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
property hertz : Float64
|
||||||
|
property lfo_hertz : Float64 = 7.0
|
||||||
|
property lfo_amp : Float64 = 0.002
|
||||||
|
property envelope : Envelope
|
||||||
|
property started_at : Float64
|
||||||
|
property wave : Wave
|
||||||
|
property released_at : Float64? = nil
|
||||||
|
|
||||||
|
def initialize(@hertz, @envelope, @started_at, @wave = Sound.sin_wave)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sample(time : Float64)
|
||||||
|
@wave.call(time - @started_at, @hertz) *
|
||||||
|
@envelope.amplitude(time, @started_at, @released_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def release!(time : Float64)
|
||||||
|
@released_at = time
|
||||||
|
end
|
||||||
|
|
||||||
|
def finished?(time : Float64)
|
||||||
|
return false unless release_time = @released_at
|
||||||
|
(time - release_time) > @envelope.release.duration
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,16 +0,0 @@
|
||||||
module PF
|
|
||||||
abstract class Voice
|
|
||||||
delegate :start, :release, :held?, :released?, :finished?, to: @envelope
|
|
||||||
property envelope : Envelope
|
|
||||||
property note : Note
|
|
||||||
|
|
||||||
def initialize(@note : Note, time : Float64)
|
|
||||||
@envelope = Envelope.new(time)
|
|
||||||
end
|
|
||||||
|
|
||||||
abstract def hertz(time : Float64)
|
|
||||||
|
|
||||||
def finished?(time : Float64)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
10
src/game.cr
10
src/game.cr
|
@ -24,6 +24,7 @@ module PF
|
||||||
@fps_current : UInt32 = 0 # the current FPS.
|
@fps_current : UInt32 = 0 # the current FPS.
|
||||||
@fps_frames : UInt32 = 0 # frames passed since the last recorded fps.
|
@fps_frames : UInt32 = 0 # frames passed since the last recorded fps.
|
||||||
@last_time : Float64 = Time.monotonic.total_milliseconds
|
@last_time : Float64 = Time.monotonic.total_milliseconds
|
||||||
|
@engine_started_at : Float64 = Time.monotonic.total_milliseconds
|
||||||
|
|
||||||
def initialize(@width, @height, @scale = 1, @title = self.class.name,
|
def initialize(@width, @height, @scale = 1, @title = self.class.name,
|
||||||
flags = SDL::Renderer::Flags::ACCELERATED,
|
flags = SDL::Renderer::Flags::ACCELERATED,
|
||||||
|
@ -59,7 +60,11 @@ module PF
|
||||||
end
|
end
|
||||||
|
|
||||||
def elapsed_time
|
def elapsed_time
|
||||||
Time.monotonic.total_milliseconds
|
Time.monotonic.total_milliseconds - @engine_started_at
|
||||||
|
end
|
||||||
|
|
||||||
|
def elapsed_seconds
|
||||||
|
elapsed_time / 1000
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear(r = 0, g = 0, b = 0)
|
def clear(r = 0, g = 0, b = 0)
|
||||||
|
@ -84,10 +89,11 @@ module PF
|
||||||
end
|
end
|
||||||
|
|
||||||
private def engine_update(event)
|
private def engine_update(event)
|
||||||
et = elapsed_time
|
et = Time.monotonic.total_milliseconds
|
||||||
calculate_fps(et)
|
calculate_fps(et)
|
||||||
update((et - @last_time) / 1000.0, event)
|
update((et - @last_time) / 1000.0, event)
|
||||||
@last_time = et
|
@last_time = et
|
||||||
|
Fiber.yield
|
||||||
GC.collect
|
GC.collect
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ module PF
|
||||||
def draw_string(msg : String, x : Int, y : Int, color : Pixel = Pixel.black, bg : Pixel? = nil)
|
def draw_string(msg : String, x : Int, y : Int, color : Pixel = Pixel.black, bg : Pixel? = nil)
|
||||||
cur_y = 0
|
cur_y = 0
|
||||||
cur_x = 0
|
cur_x = 0
|
||||||
leading = 0
|
leading = 2
|
||||||
|
|
||||||
msg.chars.each do |c|
|
msg.chars.each do |c|
|
||||||
if c == '\n'
|
if c == '\n'
|
||||||
|
|
Loading…
Reference in a new issue