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