おもちゃを作ってみようとしたが
SoundEffectに配列から音を生成する機能を追加したから、これを使っておもちゃ的なプログラムを作ろうと考えていた。
例えば正弦波を出力するブロックがあって、それを縦とか横とかに繋げて音を変化させるようなもの。縦に繋ぐと和音になって、横に繋ぐと周波数変調になるとか。
とりあえず波形やエンベロープが見えて調整可能なウィンドウを作って、それを2つ(コードで)接続することはできるようになった。
見てわかるようにDXRubyWSを使っている。こういったマウスによる操作を扱うものは素のDXRubyよりも簡単にできるかもしれない。スライダーも新たにコントロールを作ったが、ドラッグ操作などはDraggableをincludeするだけで何も考えなくていいし、ラクである。描画もお任せな感じ。
波形の計算は地道にそれっぽい計算式を起こして突っ込んだ。ディケイの減衰計算が手抜きで、ほんとはアナログ的な減衰をしたかったがとりあえずこれでいいや、みたいな。
やってる内容は2オペレータのFM音源そのもので、パラメータが異様に少ないのでほんとにしょぼい。もうちょい増やさないと遊ぶことすらできない。
まあ、でもこういうアプリを作るという地味で丁寧な作りこみ作業は俺には向かないようなのでここまででおしまいになるかもしれない。
ソースは続きに。
# coding: utf-8 require 'dxruby' require_relative '../lib/dxrubyws' require_relative '../lib/standardgui' require_relative '../lib/fontcache' # スーパー簡易FM音源。モジュレータの出力でキャリアの波形が周波数変調されます。 Window.width, Window.height = 640, 320 # スライダーコントロール module WS class WSSliderH < WSContainer class WSSlider < WSControl include Draggable def initialize(x, y, width, height) super self.image = Image.new(width, height, C_GRAY).draw_border(true) end end def initialize(x, y, width, height, val) super(x, y, width, height) @val = val @slider = WSSlider.new((width - 10) * val, 0, 10, height) self.add_control(@slider) @slider.add_handler(:drag_move) do |obj, x| @slider.x = (@slider.x + x).clamp(0, width - 10) signal(:slide, @slider.x / (width - 10.0)) # 0.0〜1.0でシグナルに値をのせる end end def draw self.image.draw_line(0, @height/2, @width-1, @height/2, C_BLACK) super end # 0.0〜1.0で指定する def set_val(x) @slider.x = x * width end end end module WS # フォーム定義 class SoundBlockWindow < WSWindow attr_accessor :btn_play, :wave_image, :volume_image def initialize(x, y, title) super(x, y, 226, 200, title) @btn_play = WSButton.new(160, 150, 50, 20, "Play") self.client.add_control(@btn_play, :btn_play) @btn_play.add_handler(:click) {|obj, tx, ty|self.btn_play_clicked(tx, ty)} @wave_image = WSImage.new(10, 10, 100, 60) @wave_image.image = Image.new(100, 60, C_BLACK) self.client.add_control(@wave_image, :wave_image) @volume_image = WSImage.new(10, 80, 100, 60) @volume_image.image = Image.new(100, 60, C_BLACK) self.client.add_control(@volume_image, :volume_image) @slider_a = WSSliderH.new(120, 60, 80, 16, 0) @slider_d = WSSliderH.new(120, 80, 80, 16, 0) @slider_s = WSSliderH.new(120, 100, 80, 16, 0) @slider_r = WSSliderH.new(120, 120, 80, 16, 0) self.client.add_control(@slider_a) self.client.add_control(@slider_d) self.client.add_control(@slider_s) self.client.add_control(@slider_r) @slider_a.add_handler(:slide) {|obj, x|self.a_slide(x)} @slider_d.add_handler(:slide) {|obj, x|self.d_slide(x)} @slider_s.add_handler(:slide) {|obj, x|self.s_slide(x)} @slider_r.add_handler(:slide) {|obj, x|self.r_slide(x)} @label_a = WSLabel.new(205, 60, 30, 16, "A") @label_d = WSLabel.new(205, 80, 30, 16, "D") @label_s = WSLabel.new(205, 100, 30, 16, "S") @label_r = WSLabel.new(205, 120, 30, 16, "R") self.client.add_control(@label_a) self.client.add_control(@label_s) self.client.add_control(@label_d) self.client.add_control(@label_r) init end end # アプリコード class SoundBlockWindow attr_accessor :modulator, :carrier def self.connect(mod, car) mod.carrier = car car.modulator = mod car.draw_wave end def init @se = nil # デフォルト値 @wave = WAVE_SIN @f = 440.0 @v = 1.0 @attack_time = 0.005 @decay_time = 0.3 @sustain_level = 0.4 @release_time = 0.1 @slider_a.set_val(@attack_time) @slider_s.set_val(@decay_time) @slider_d.set_val(@sustain_level) @slider_r.set_val(@release_time) draw_wave draw_volume end def a_slide(val) @attack_time = val draw_volume end def d_slide(val) @decay_time = val draw_volume end def s_slide(val) @sustain_level = val draw_volume end def r_slide(val) @release_time = val draw_volume end def draw_wave @wave_image.image.fill(C_BLACK) x = 0 y = 30 100.times do |i| tmp = w_calc_for_image(44100.0/@f/100*i) * 30 + 30 @wave_image.image.line(x, y, i, tmp, C_WHITE) x = i y = tmp end end def draw_volume @volume_image.image.fill(C_BLACK) x = 0 y = 60 100.times do |i| tmp = 60-v_calc(441.0 * i)*60.0 @volume_image.image.line(x, y, i, tmp, C_WHITE) x = i y = tmp end end def w_calc_for_image(t) # 1/44100単位 if @modulator Math.sin(2 * Math::PI / (44100.0 / @f) * t + @modulator.w_calc(t)) else Math.sin(2 * Math::PI / (44100.0 / @f) * t) end end def w_calc(t) # 1/44100単位 if @modulator Math.sin(2 * Math::PI / (44100.0 / @f) * t + @modulator.w_calc(t) * @modulator.v_calc(t)) else Math.sin(2 * Math::PI / (44100.0 / @f) * t) end end def v_calc(t) # 1/44100単位 t = t / 44100.0 if t <= @attack_time # アタック中 return 0.0 if @attack_time == 0.0 (t / @attack_time) * @v elsif t >= 1.0 - @release_time # リリース中 (1 - ((t - (1.0 - @release_time)) / @release_time)) * @sustain_level else # 減衰 or 保持中 if (t - @attack_time) > @decay_time @sustain_level else (1 - (t - @attack_time) / @decay_time) * (@v - @sustain_level) + @sustain_level end end end def btn_play_clicked(tx, ty) if @se @se.stop @se.dispose end @se = SoundEffect.new(Array.new(44100){|i|w_calc(i) * v_calc(i)}) @se.play end end end w1 = WS::SoundBlockWindow.new(0, 0, "SoundBlock Modulator") WS.desktop.add_control(w1) w2 = WS::SoundBlockWindow.new(300, 0, "SoundBlock Carrier") WS.desktop.add_control(w2) WS::SoundBlockWindow.connect(w1, w2) w2.btn_play.activate WS.desktop.add_key_handler(K_ESCAPE) do break end Window.loop do WS.update end