Skip to content

Instantly share code, notes, and snippets.

@astellon
Last active April 1, 2019 11:13
Show Gist options
  • Save astellon/b91a3cd72a6ea2a585b9c14aeb87a731 to your computer and use it in GitHub Desktop.
Save astellon/b91a3cd72a6ea2a585b9c14aeb87a731 to your computer and use it in GitHub Desktop.
simple sin wave synthesizer + sequencer in Crystal
require "socket"
require "osc-crystal"
lib LibC
fun nanosleep(req : Timespec*, rem : Timespec*) : Int32
end
# helper
def note_to_heltz(note : Int)
(440*Math.exp2((note - 69.0)/12.0)).to_f32
end
class Sequencer(T)
getter seqs : T
getter pos : Int32
getter client : OSC::Client
property running : Bool
def initialize(@client, @seqs : T) forall T
@pos = 0
@running = false
end
def start(bpm)
spawn do
@running = true
while @running
process(@seqs[@pos], bpm, @seqs.size)
@pos = (@pos + 1) % seqs.size
end
end
end
def stop
@running = @running ^ false
end
def process(seq : Int32, bpm, n)
client.send(OSC::Bundle.new(Time.utc_now + Time::Span.new(nanoseconds: 10_000), OSC::Message.new("/note", seq + 60))) # mid C
puts seq
wait(bpm, n)
end
def process(seq : Tuple, bpm, n)
seq.each do |i|
process(i, bpm, n * seq.size)
end
end
def wait(bpm, n)
milisec = 60.0 * 1000.0 / bpm * 4.0 / n
sec = (milisec / 1000).to_i32
nse = ((milisec % 1000) * 1000000).to_i32
req = LibC::Timespec.new(tv_sec: sec, tv_nsec: nse)
rem = LibC::Timespec.new(tv_sec: 0, tv_nsec: 0)
LibC.nanosleep(pointerof(req), pointerof(rem))
end
end
def main
client = UDPSocket.new
client.connect "127.0.0.1", 8000
osc_client = OSC::Client.new(client)
s = Sequencer.new(osc_client, {0, {5, 7, 2, 5}, {4, 7}, 2})
s.start(120)
at_exit {
s.stop
client.close
}
sleep
end
main
# Standard library
require "socket"
# denpendencies
require "osc-crystal"
require "quartz"
include Quartz
# note number to helz
def note_to_heltz(note : Int)
(440*Math.exp2((note - 69.0)/12.0)).to_f32
end
# streaming sin wave
class Synth
property phase = 0.0_f32
property delta_p = 0.0_f32
property time = 0.0
property freq_queue = Deque(Tuple(Float64, Float32)).new
def callback(input, output, nframe)
update()
@time += nframe / 44100.0
(0..nframe - 1).each do |i|
output[2*i] = output[2*i + 1] = Math.sin(@phase) * 0.5
@phase += @delta_p
end
@phase = @phase.modulo(2 * Math::PI)
return 0
end
def update
return if @freq_queue.size == 0
t, freq = @freq_queue.first
if @time > t
@delta_p = (2 * Math::PI * freq / 44100.0).to_f32
@freq_queue.shift
puts @phase
end
end
end
def main
synth = Synth.new
# server
server = UDPSocket.new
server.bind "localhost", 8000
stream = Quartz::AudioStream(Float32).new(2, 2, 44100.0, 256_u64)
at_exit {
running = false
server.close
}
stream.start(synth)
running = true
start_time = Time.utc_now
while running
message, address = server.receive
bundle = OSC::Bundle.new(message.bytes)
time = bundle.time - start_time # => Time::Span
msg1 = bundle.elm(0).as(OSC::Message)
case note = msg1.arg(0)
when Int32
synth.freq_queue.push({time.total_seconds, note_to_heltz note})
end
end
end
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment