summaryrefslogtreecommitdiffhomepage
path: root/samples/07_advanced_audio/02_sound_synthesis/app
diff options
context:
space:
mode:
Diffstat (limited to 'samples/07_advanced_audio/02_sound_synthesis/app')
-rw-r--r--samples/07_advanced_audio/02_sound_synthesis/app/main.rb593
1 files changed, 593 insertions, 0 deletions
diff --git a/samples/07_advanced_audio/02_sound_synthesis/app/main.rb b/samples/07_advanced_audio/02_sound_synthesis/app/main.rb
new file mode 100644
index 0000000..af2be9d
--- /dev/null
+++ b/samples/07_advanced_audio/02_sound_synthesis/app/main.rb
@@ -0,0 +1,593 @@
+begin # region: top level tick methods
+ def tick args
+ defaults args
+ render args
+ input args
+ process_audio_queue args
+ end
+
+ def defaults args
+ args.state.sine_waves ||= {}
+ args.state.square_waves ||= {}
+ args.state.saw_tooth_waves ||= {}
+ args.state.triangle_waves ||= {}
+ args.state.audio_queue ||= []
+ args.state.buttons ||= [
+ (frequency_buttons args),
+ (sine_wave_note_buttons args),
+ (bell_buttons args),
+ (square_wave_note_buttons args),
+ (saw_tooth_wave_note_buttons args),
+ (triangle_wave_note_buttons args),
+ ].flatten
+ end
+
+ def render args
+ args.outputs.borders << args.state.buttons.map { |b| b[:border] }
+ args.outputs.labels << args.state.buttons.map { |b| b[:label] }
+ args.outputs.labels << args.layout
+ .rect(row: 0, col: 11.5)
+ .yield_self { |r| r.merge y: r.y + r.h }
+ .merge(text: "This is a Pro only feature. Click here to watch the YouTube video if you are on the Standard License.",
+ alignment_enum: 1)
+ end
+
+
+ def input args
+ args.state.buttons.each do |b|
+ if args.inputs.mouse.click && (args.inputs.mouse.click.inside_rect? b[:rect])
+ parameter_string = (b.slice :frequency, :note, :octave).map { |k, v| "#{k}: #{v}" }.join ", "
+ args.gtk.notify! "#{b[:method_to_call]} #{parameter_string}"
+ send b[:method_to_call], args, b
+ end
+ end
+
+ if args.inputs.mouse.click && (args.inputs.mouse.click.inside_rect? (args.layout.rect(row: 0).yield_self { |r| r.merge y: r.y + r.h.half, h: r.h.half }))
+ args.gtk.openurl 'https://www.youtube.com/watch?v=zEzovM5jT-k&ab_channel=AmirRajan'
+ end
+ end
+
+ def process_audio_queue args
+ to_queue = args.state.audio_queue.find_all { |v| v[:queue_at] <= args.tick_count }
+ args.state.audio_queue -= to_queue
+ to_queue.each { |a| args.audio[a[:id]] = a }
+
+ args.audio.find_all { |k, v| v[:decay_rate] }
+ .each { |k, v| v[:gain] -= v[:decay_rate] }
+
+ sounds_to_stop = args.audio
+ .find_all { |k, v| v[:stop_at] && args.state.tick_count >= v[:stop_at] }
+ .map { |k, v| k }
+
+ sounds_to_stop.each { |k| args.audio.delete k }
+ end
+end
+
+begin # region: button definitions, ui layout, callback functions
+ def button args, opts
+ button_def = opts.merge rect: (args.layout.rect (opts.merge w: 2, h: 1))
+
+ button_def[:border] = button_def[:rect].merge r: 0, g: 0, b: 0
+
+ label_offset_x = 5
+ label_offset_y = 30
+
+ button_def[:label] = button_def[:rect].merge text: opts[:text],
+ size_enum: -2.5,
+ x: button_def[:rect].x + label_offset_x,
+ y: button_def[:rect].y + label_offset_y
+
+ button_def
+ end
+
+ def play_sine_wave args, sender
+ queue_sine_wave args,
+ frequency: sender[:frequency],
+ duration: 1.seconds,
+ fade_out: true
+ end
+
+ def play_note args, sender
+ method_to_call = :queue_sine_wave
+ method_to_call = :queue_square_wave if sender[:type] == :square
+ method_to_call = :queue_saw_tooth_wave if sender[:type] == :saw_tooth
+ method_to_call = :queue_triangle_wave if sender[:type] == :triangle
+ method_to_call = :queue_bell if sender[:type] == :bell
+
+ send method_to_call, args,
+ frequency: (frequency_for note: sender[:note], octave: sender[:octave]),
+ duration: 1.seconds,
+ fade_out: true
+ end
+
+ def frequency_buttons args
+ [
+ (button args,
+ row: 4.0, col: 0, text: "300hz",
+ frequency: 300,
+ method_to_call: :play_sine_wave),
+ (button args,
+ row: 5.0, col: 0, text: "400hz",
+ frequency: 400,
+ method_to_call: :play_sine_wave),
+ (button args,
+ row: 6.0, col: 0, text: "500hz",
+ frequency: 500,
+ method_to_call: :play_sine_wave),
+ ]
+ end
+
+ def sine_wave_note_buttons args
+ [
+ (button args,
+ row: 1.5, col: 2, text: "Sine C4",
+ note: :c, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 2, text: "Sine D4",
+ note: :d, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 2, text: "Sine E4",
+ note: :e, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 2, text: "Sine F4",
+ note: :f, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 2, text: "Sine G4",
+ note: :g, octave: 4, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 2, text: "Sine A5",
+ note: :a, octave: 5, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 2, text: "Sine B5",
+ note: :b, octave: 5, type: :sine, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 2, text: "Sine C5",
+ note: :c, octave: 5, type: :sine, method_to_call: :play_note),
+ ]
+ end
+
+ def square_wave_note_buttons args
+ [
+ (button args,
+ row: 1.5, col: 6, text: "Square C4",
+ note: :c, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 6, text: "Square D4",
+ note: :d, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 6, text: "Square E4",
+ note: :e, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 6, text: "Square F4",
+ note: :f, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 6, text: "Square G4",
+ note: :g, octave: 4, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 6, text: "Square A5",
+ note: :a, octave: 5, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 6, text: "Square B5",
+ note: :b, octave: 5, type: :square, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 6, text: "Square C5",
+ note: :c, octave: 5, type: :square, method_to_call: :play_note),
+ ]
+ end
+ def saw_tooth_wave_note_buttons args
+ [
+ (button args,
+ row: 1.5, col: 8, text: "Saw C4",
+ note: :c, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 8, text: "Saw D4",
+ note: :d, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 8, text: "Saw E4",
+ note: :e, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 8, text: "Saw F4",
+ note: :f, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 8, text: "Saw G4",
+ note: :g, octave: 4, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 8, text: "Saw A5",
+ note: :a, octave: 5, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 8, text: "Saw B5",
+ note: :b, octave: 5, type: :saw_tooth, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 8, text: "Saw C5",
+ note: :c, octave: 5, type: :saw_tooth, method_to_call: :play_note),
+ ]
+ end
+
+ def triangle_wave_note_buttons args
+ [
+ (button args,
+ row: 1.5, col: 10, text: "Triangle C4",
+ note: :c, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 10, text: "Triangle D4",
+ note: :d, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 10, text: "Triangle E4",
+ note: :e, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 10, text: "Triangle F4",
+ note: :f, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 10, text: "Triangle G4",
+ note: :g, octave: 4, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 10, text: "Triangle A5",
+ note: :a, octave: 5, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 10, text: "Triangle B5",
+ note: :b, octave: 5, type: :triangle, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 10, text: "Triangle C5",
+ note: :c, octave: 5, type: :triangle, method_to_call: :play_note),
+ ]
+ end
+
+ def bell_buttons args
+ [
+ (button args,
+ row: 1.5, col: 4, text: "Bell C4",
+ note: :c, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 2.5, col: 4, text: "Bell D4",
+ note: :d, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 3.5, col: 4, text: "Bell E4",
+ note: :e, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 4.5, col: 4, text: "Bell F4",
+ note: :f, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 5.5, col: 4, text: "Bell G4",
+ note: :g, octave: 4, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 6.5, col: 4, text: "Bell A5",
+ note: :a, octave: 5, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 7.5, col: 4, text: "Bell B5",
+ note: :b, octave: 5, type: :bell, method_to_call: :play_note),
+ (button args,
+ row: 8.5, col: 4, text: "Bell C5",
+ note: :c, octave: 5, type: :bell, method_to_call: :play_note),
+ ]
+ end
+end
+
+begin # region: wave generation
+ begin # sine wave
+ def defaults_sine_wave_for
+ { frequency: 440, sample_rate: 48000 }
+ end
+
+ def sine_wave_for opts = {}
+ opts = defaults_sine_wave_for.merge opts
+ frequency = opts[:frequency]
+ sample_rate = opts[:sample_rate]
+ period_size = (sample_rate.fdiv frequency).ceil
+ period_size.map_with_index do |i|
+ Math::sin((2.0 * Math::PI) / (sample_rate.to_f / frequency.to_f) * i)
+ end.to_a
+ end
+
+ def defaults_queue_sine_wave
+ { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 }
+ end
+
+ def queue_sine_wave args, opts = {}
+ opts = defaults_queue_sine_wave.merge opts
+ frequency = opts[:frequency]
+ sample_rate = 48000
+
+ sine_wave = sine_wave_for frequency: frequency, sample_rate: sample_rate
+ args.state.sine_waves[frequency] ||= sine_wave_for frequency: frequency, sample_rate: sample_rate
+
+ proc = lambda do
+ generate_audio_data args.state.sine_waves[frequency], sample_rate
+ end
+
+ audio_state = new_audio_state args, opts
+ audio_state[:input] = [1, sample_rate, proc]
+ queue_audio args, audio_state: audio_state, wave: sine_wave
+ end
+ end
+
+ begin # region: square wave
+ def defaults_square_wave_for
+ { frequency: 440, sample_rate: 48000 }
+ end
+
+ def square_wave_for opts = {}
+ opts = defaults_square_wave_for.merge opts
+ sine_wave = sine_wave_for opts
+ sine_wave.map do |v|
+ if v >= 0
+ 1.0
+ else
+ -1.0
+ end
+ end.to_a
+ end
+
+ def defaults_queue_square_wave
+ { frequency: 440, duration: 60, gain: 0.3, fade_out: false, queue_in: 0 }
+ end
+
+ def queue_square_wave args, opts = {}
+ opts = defaults_queue_square_wave.merge opts
+ frequency = opts[:frequency]
+ sample_rate = 48000
+
+ square_wave = square_wave_for frequency: frequency, sample_rate: sample_rate
+ args.state.square_waves[frequency] ||= square_wave_for frequency: frequency, sample_rate: sample_rate
+
+ proc = lambda do
+ generate_audio_data args.state.square_waves[frequency], sample_rate
+ end
+
+ audio_state = new_audio_state args, opts
+ audio_state[:input] = [1, sample_rate, proc]
+ queue_audio args, audio_state: audio_state, wave: square_wave
+ end
+ end
+
+ begin # region: saw tooth wave
+ def defaults_saw_tooth_wave_for
+ { frequency: 440, sample_rate: 48000 }
+ end
+
+ def saw_tooth_wave_for opts = {}
+ opts = defaults_saw_tooth_wave_for.merge opts
+ sine_wave = sine_wave_for opts
+ period_size = sine_wave.length
+ sine_wave.map_with_index do |v, i|
+ (((i % period_size).fdiv period_size) * 2) - 1
+ end
+ end
+
+ def defaults_queue_saw_tooth_wave
+ { frequency: 440, duration: 60, gain: 0.3, fade_out: false, queue_in: 0 }
+ end
+
+ def queue_saw_tooth_wave args, opts = {}
+ opts = defaults_queue_saw_tooth_wave.merge opts
+ frequency = opts[:frequency]
+ sample_rate = 48000
+
+ saw_tooth_wave = saw_tooth_wave_for frequency: frequency, sample_rate: sample_rate
+ args.state.saw_tooth_waves[frequency] ||= saw_tooth_wave_for frequency: frequency, sample_rate: sample_rate
+
+ proc = lambda do
+ generate_audio_data args.state.saw_tooth_waves[frequency], sample_rate
+ end
+
+ audio_state = new_audio_state args, opts
+ audio_state[:input] = [1, sample_rate, proc]
+ queue_audio args, audio_state: audio_state, wave: saw_tooth_wave
+ end
+ end
+
+ begin # region: triangle wave
+ def defaults_triangle_wave_for
+ { frequency: 440, sample_rate: 48000 }
+ end
+
+ def triangle_wave_for opts = {}
+ opts = defaults_saw_tooth_wave_for.merge opts
+ sine_wave = sine_wave_for opts
+ period_size = sine_wave.length
+ sine_wave.map_with_index do |v, i|
+ ratio = (i.fdiv period_size)
+ if ratio <= 0.5
+ (ratio * 4) - 1
+ else
+ ratio -= 0.5
+ 1 - (ratio * 4)
+ end
+ end
+ end
+
+ def defaults_queue_triangle_wave
+ { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 }
+ end
+
+ def queue_triangle_wave args, opts = {}
+ opts = defaults_queue_triangle_wave.merge opts
+ frequency = opts[:frequency]
+ sample_rate = 48000
+
+ triangle_wave = triangle_wave_for frequency: frequency, sample_rate: sample_rate
+ args.state.triangle_waves[frequency] ||= triangle_wave_for frequency: frequency, sample_rate: sample_rate
+
+ proc = lambda do
+ generate_audio_data args.state.triangle_waves[frequency], sample_rate
+ end
+
+ audio_state = new_audio_state args, opts
+ audio_state[:input] = [1, sample_rate, proc]
+ queue_audio args, audio_state: audio_state, wave: triangle_wave
+ end
+ end
+
+ begin # region: bell
+ def defaults_queue_bell
+ { frequency: 440, duration: 1.seconds, queue_in: 0 }
+ end
+
+ def queue_bell args, opts = {}
+ (bell_to_sine_waves (defaults_queue_bell.merge opts)).each { |b| queue_sine_wave args, b }
+ end
+
+ def bell_harmonics
+ [
+ { frequency_ratio: 0.5, duration_ratio: 1.00 },
+ { frequency_ratio: 1.0, duration_ratio: 0.80 },
+ { frequency_ratio: 2.0, duration_ratio: 0.60 },
+ { frequency_ratio: 3.0, duration_ratio: 0.40 },
+ { frequency_ratio: 4.2, duration_ratio: 0.25 },
+ { frequency_ratio: 5.4, duration_ratio: 0.20 },
+ { frequency_ratio: 6.8, duration_ratio: 0.15 }
+ ]
+ end
+
+ def defaults_bell_to_sine_waves
+ { frequency: 440, duration: 1.seconds, queue_in: 0 }
+ end
+
+ def bell_to_sine_waves opts = {}
+ opts = defaults_bell_to_sine_waves.merge opts
+ bell_harmonics.map do |b|
+ {
+ frequency: opts[:frequency] * b[:frequency_ratio],
+ duration: opts[:duration] * b[:duration_ratio],
+ queue_in: opts[:queue_in],
+ gain: (1.fdiv bell_harmonics.length),
+ fade_out: true
+ }
+ end
+ end
+ end
+
+ begin # audio entity construction
+ def generate_audio_data sine_wave, sample_rate
+ sample_size = (sample_rate.fdiv (1000.fdiv 60)).ceil
+ copy_count = (sample_size.fdiv sine_wave.length).ceil
+ sine_wave * copy_count
+ end
+
+ def defaults_new_audio_state
+ { frequency: 440, duration: 60, gain: 1.0, fade_out: false, queue_in: 0 }
+ end
+
+ def new_audio_state args, opts = {}
+ opts = defaults_new_audio_state.merge opts
+ decay_rate = 0
+ decay_rate = 1.fdiv(opts[:duration]) * opts[:gain] if opts[:fade_out]
+ frequency = opts[:frequency]
+ sample_rate = 48000
+
+ {
+ id: (new_id! args),
+ frequency: frequency,
+ sample_rate: 48000,
+ stop_at: args.tick_count + opts[:queue_in] + opts[:duration],
+ gain: opts[:gain].to_f,
+ queue_at: args.state.tick_count + opts[:queue_in],
+ decay_rate: decay_rate,
+ pitch: 1.0,
+ looping: true,
+ paused: false
+ }
+ end
+
+ def queue_audio args, opts = {}
+ graph_wave args, opts[:wave], opts[:audio_state][:frequency]
+ args.state.audio_queue << opts[:audio_state]
+ end
+
+ def new_id! args
+ args.state.audio_id ||= 0
+ args.state.audio_id += 1
+ end
+
+ def graph_wave args, wave, frequency
+ if args.state.tick_count != args.state.graphed_at
+ args.outputs.static_lines.clear
+ args.outputs.static_sprites.clear
+ end
+
+ wave = wave
+
+ r, g, b = frequency.to_i % 85,
+ frequency.to_i % 170,
+ frequency.to_i % 255
+
+ starting_rect = args.layout.rect(row: 5, col: 13)
+ x_scale = 10
+ y_scale = 100
+ max_points = 25
+
+ points = wave
+ if wave.length > max_points
+ resolution = wave.length.idiv max_points
+ points = wave.find_all.with_index { |y, i| (i % resolution == 0) }
+ end
+
+ args.outputs.static_lines << points.map_with_index do |y, x|
+ next_y = points[x + 1]
+
+ if next_y
+ {
+ x: starting_rect.x + (x * x_scale),
+ y: starting_rect.y + starting_rect.h.half + y_scale * y,
+ x2: starting_rect.x + ((x + 1) * x_scale),
+ y2: starting_rect.y + starting_rect.h.half + y_scale * next_y,
+ r: r,
+ g: g,
+ b: b
+ }
+ end
+ end
+
+ args.outputs.static_sprites << points.map_with_index do |y, x|
+ {
+ x: (starting_rect.x + (x * x_scale)) - 2,
+ y: (starting_rect.y + starting_rect.h.half + y_scale * y) - 2,
+ w: 4,
+ h: 4,
+ path: 'sprites/square-white.png',
+ r: r,
+ g: g,
+ b: b
+ }
+ end
+
+ args.state.graphed_at = args.state.tick_count
+ end
+ end
+
+ begin # region: musical note mapping
+ def defaults_frequency_for
+ { note: :a, octave: 5, sharp: false, flat: false }
+ end
+
+ def frequency_for opts = {}
+ opts = defaults_frequency_for.merge opts
+ octave_offset_multiplier = opts[:octave] - 5
+ note = note_frequencies_octave_5[opts[:note]]
+ if octave_offset_multiplier < 0
+ note = note * 1 / (octave_offset_multiplier.abs + 1)
+ elsif octave_offset_multiplier > 0
+ note = note * (octave_offset_multiplier.abs + 1) / 1
+ end
+ note
+ end
+
+ def note_frequencies_octave_5
+ {
+ a: 440.0,
+ a_sharp: 466.16, b_flat: 466.16,
+ b: 493.88,
+ c: 523.25,
+ c_sharp: 554.37, d_flat: 587.33,
+ d: 587.33,
+ d_sharp: 622.25, e_flat: 659.25,
+ e: 659.25,
+ f: 698.25,
+ f_sharp: 739.99, g_flat: 739.99,
+ g: 783.99,
+ g_sharp: 830.61, a_flat: 830.61
+ }
+ end
+ end
+end
+
+$gtk.reset