超絶技巧 Ruby プログラミングの質疑で「どうやって音を鳴らしているのか」という質問があったので、自分のための記録を兼ねて簡単に紹介。
といっても Linux Sound programming with OSS API にある通り。Ruby で書くとこんな感じ。
# デフォルトでは 8bit 8000 Hz SampleSize = 256 SamplingRate = 8000 # ... 0:ラ 1:ラ# 2:シ 3:ド 4:ド# 5:レ 6:レ# 7:ミ 8:ファ 9:ファ# 10:ソ 11:ソ# 12:ラ ... tone = 3 # ボリューム: 0 〜 SampleSize/2 まで volume = 60 # 再生する長さ: 秒 length = 2 # 周波数: 基準のラは 440Hz 、1 オクターブ上がると倍になる freq = 440 * 2**(tone / 12.0) open("/dev/dsp", "wb") do |f| phase = freq.to_f / SamplingRate wave = (0 ... length * SamplingRate).map do |i| Math.sin(2 * Math::PI * phase * i) # 正弦波 #(1 - 2 * ((phase * i) % 1)) # ノコギリ波 end wave = wave.map {|v| (SampleSize / 2 + volume * v).round } f.print(wave.pack("C*")) end
ノコギリ波は倍音がいっぱい混ざった波形なので、バイオリン系の豊かな音になるらしい。
以下は超絶技巧 Ruby プログラミングで使った音楽 (G 線上のアリア) を再生するコード。ろくに清書してないので汚い。あと変数名 (score とか tone とか) は定義をよく調べないで使ってるので当てにしないこと。あとこれは 8000 Hz なので音質が悪い。発表では、サンプリングレートを 44100 Hz に上げていた。そのためには ioctl を呼ぶ必要があって、そのためには sys/soundcard.h とかで定義されている SOUND_PCM_WRITE_RATE の値が必要で、そのために gcc を呼んでいたりする。
Tone = { "a" => 0, "a+" => 1, "b" => 2, "c" => -9, "c+" => -8, "d" => -7, "d+" => -6, "e" => -5, "f" => -4, "f+" => -3, "g" => -2, "g+" => -1, } def mml_compile(mml) scores = ["", "", "", ""] mml.each_line do |line| next unless line =~ /^(\d+):/ scores[$1.to_i - 1] << $' end seqs = scores.map { [] } scores.zip(seqs, [2, 2, 0, -1]) do |score, seq, octave| score.split.join.scan(/([a-gr<>,]\+?)([\d\-+]+)?/) do |tone, length| length = eval(length.gsub(/\d+/) { 32 / $&.to_i }) if length case tone when "," when ">"; octave += 1 when "<"; octave -= 1 when "r"; seq.concat([nil] * length) else seq.concat([octave * 12 + Tone[tone]] * length) end end end seqs end seqs1 = mml_compile(<<END) 1:d1 ,d4<b4>c+4-32<b32a4 ,>a2a16f+16c16<b16>e16d+16a16g16, 2:f+1 ,f+8b16g16e16d16c+16d16<a2,a8>c16<b16>c8a16c16<b8>r8r4 , 3:d8>d8c+8<c+8<b8>b8a8<a8,g8>g8g+8<g+8>e2 ,e8d+4e8f+8r8r4 , 4:a2b2 ,<b4>e4<a8>a8g8<g8 ,f+8>f+8e8<e8d+8>d+8<b8>b8 , 1:g2g16e16<b16a16>d16c+16g16f+16,<a2a8g+16a16b8g+8 ,a8a4g+8e2> , 2:<b8>e16d16e16f+16g16e16<a8r8r4,>f+4f+8g+16a16d8d32e32f+8e16e16d16,c+16<b16b32>c+32d16d8c+16<b16a2> , 3:e8b4e8e8r8r4 ,d4d8e8f+8d8b8e8 ,e8f+8b8e8c+2 , 4:<e8>e8d8<d8c+8>c+8<a8>a8 ,d8>d8c+8<c+8<b8>b8g+8e8 ,a8d8e8<e8a16b16>c+16d16e16g16f+16e16, END seqs2 = mml_compile(<<END) 1:c+4+16d32c+32<b32>c+32<a16>a4a8c8,<b8>b8+16a16g16f+16g4+32f+32e32d32c+16<b16, 2:<a2+16b16>c8+16<b16a16g16 ,f+4+8b8e8>e8d8<d8 , 3:e2e8d+16e16f+4 ,f+16g16a16g16d+8>d+8e2 , 4:<a8>a8g8<g8f+8>f+8e8<e8 ,d+8>d+8f+8<b8>b4<b4> , 1:a+16b16>c+8+16d16e8+16f+16g4f+8 ,e16d16c+16<b16>c+16d32e32d8<b2, 2:c+8>c+8<b8<b8a+8b8>c+8<a+8> ,<b8>g8e8f+8<b8>b8a8<a8> , 3:e2+16d16c+16<b16a+16b16>c+8 ,<b8b8b8a+8f+2 , 4:c+16d16e16f+16g16f+16g16e16f+8e16d16c+8f+8,f+8e16d16g8f+16e16d2 , 1:>d4+16f+16e16d16b4+8a16g+16,f+32e32a16<a4g+8a2 ,a8b16>c16<b16>c+16d8+8c+16<b16>c+16d+16e8, 2:e4f+4<b8>e16f+16g+16a16b8 ,b8a8b8+16>c+32d32c+8+16<b16a4 ,>d4+8f+16e16e4+8g16f+16 , 3:<b8>b8a16g+16a8g+8+16f+16e4,e8e8f+8e8e8+16d16c+16d16f+16c+16,<f+8>f+8g8<g8g+8>g+8a8<a8 , 4:<g+8>g+8f+8<f+8e8>e8d8<d8 ,c+8>c+8d8e8<a8>a8g8<g8 ,a8>d4<b8+8>e4c+8 , 1:e8d+16c+16d+16e16f+8g2 ,<a4+16>c+16e16g16g16e16f+8+8+16g32a32,d4+16f+16a16>c16<b4+8d8, 2:f+4+8a16g16r16d+16e16<b16e4,e16c+16e16a16>c+8<a8+8>c+16d16<d4 ,d8e8f+4d2 , 3:a+8>a+8b8<b8>e8>e8d8<d8 ,a8g8f+8e8d4a4 ,a8g8a4g2 , 4:c+8f+4d+8<b4+16>b16g16e16 ,c+8>c+8<a8>c+8d8<d8c8>c8< ,b8<b8a8>a8g8<g8f+8>f+8 , 1:c+16e16g4d8<a8>e16f+32g32+16f+8e16,d32c+32<b8>c+16d8c+16d16d2, 2:e16<b16>e16g16b16a16g16f+16e8a4g8 ,a4g16f+16g8f+2> , 3:g8b8>e4+16d16c+16<b16a8b8 ,f+4e8a8a2 , 4:e8<e8d8>d8c+8<a8>d8g8 ,a8g8a8<a8d2 , END seqs = seqs1.zip(seqs1, seqs2, seqs2).map {|seqs| seqs.flatten } seqs[3][370,14] = [-24] * 14; seqs = seqs.transpose def play(rate, seqs) unit = rate * 10000 / 44100 seqs.each_with_index do |tones, w| wave = [128] * unit tones.each do |tone| next unless tone tone = 440.0 / rate * 2**(tone / 12.0) unit.times {|i| wave[i] += 10 - 20 * ((tone * (w * unit + i)) % 1) } end yield wave.pack("C*") end end if true # output to /dev/dsp open("/dev/dsp", "wb") do |f| play(8000, seqs) do |s| f << s f.flush end end else # generate air.wav header = ["WAVEfmt ", 16, 1, 1, 44100, 44100, 1, 8].pack("A8VvvVVvv") data = "" play(44100, seqs) {|s| data << s } data = ["data", data.size].pack("A4V") << data open("air.wav", "wb") do |f| f << ["RIFF", header.size + data.size].pack("A4V") << header << data end end
ちなみに最後の if 文を false にすると 44100 Hz モノラルの wav が出力されます。無圧縮の wav はとても簡単ですね。