Rubyでマリオを奏でよう! 〜_whyのbloopsaphoneの紹介
2009年8月19日Ruby界の鬼才_why*1が
ネットから忽然と姿を消しました*2
この突然の出来事にRubyist達は驚きそして困惑しました
なぜなら彼自身がただネットから消えたのではなく
_whyはネット上の彼の痕跡も同時にすべて消し去ったのです!
Rubyのガイドブック ブログ 音楽 カートゥーン
そしてアクティブなライブラリを含む多数のプログラムコード
camping hpricot redcloth shoes TryRuby metaid...
_whyは削除コマンドを実行して
_whyの作品群をクラウドのメモリーから削除しました
このような行動に対しネット上には賛否両論がありましたが
多くの人はそこに至った_whyの心情を悲しみました
そう_whyは_whyの手によって抹殺されたのです...
しかし
_whyにも削除できない記憶域がありました
それはクラウドの向こう側にいる人々の脳内記憶でした
_whyというユニークなペルソナを
人々の記憶から消し去ることは彼にもできなかったのです
その結果としてコミュニティにより
_whyの作品群はネット上に再生されました
ポストモダン時代の天才として
_whyの軌跡を追う記事が最近書かれました
Why: A Tale Of A Post-Modern Genius
改めて_whyのユニークさを実感します
そう_whyは今も生きているのです...
bloopsaphone
先の記事で_whyのbloopsaphoneというライブラリを知りました
以下ではその使い方を簡単に説明したいと思います
bloopsaphoneはファミコンサウンドのような
電子音を作るためのRubyとCのライブラリです
mental's bloopsaphone at master - GitHub
まずはbloopsaphoneで作った音を聞いてください
これはライブラリに付属の次のコードを再生したものです
test.rb require 'bloops' # the song object b = Bloops.new b.tempo = 320 # an instrument saw = b.sound Bloops::SINE #saw = b.sound Bloops::SAWTOOTH # assign a track to the song b.tune saw, "c5 c6 b4 b5 d5 d6 e5 e6b" # make it go b.play sleep 1 while !b.stopped? # a percussion beat = b.sound Bloops::NOISE beat.repeat = 0.6 # assign a track to the song b.tune beat, "4 4 b4 4 d5 4 e5 e6" # make it go b.play sleep 1 while !b.stopped?
ライブラリにはドキュメントがないですが
サンプルコードを見れば大体使い方がわかります
手順としては次のようになります
- Bloops.newでsongオブジェクトを作り(Bloopsクラスのインスタンス生成)
- tempoメソッドでテンポを決め
- soundメソッドで楽器(音)を選び(Soundクラスのインスタンス生成)
- setterメソッドで音質調整をし
- tuneメソッドで楽譜をセットして(Trackクラスのインスタンス生成)
- playメソッドで再生する
複数トラックの再生が可能で
この例では1回目は1トラック
2回目は2トラックで再生しています
楽器(sound)にはSQUARE SAWTOOTH SINE NOISEの4種があって
そのベース音に対して音質調整用メソッド(ここではrepeat)で
音質調整して最終的な音質を決めます
楽譜はRTTTL(Ring Tone Transfer Language)*3を
ベースにした記法の文字列を渡します
音符の音価(長さ)と音高と音程(オクターブ)を
数字・アルファベット・数字の組で表します
例えば"8:F#4"は"8分音符F#(ファ#)オクターブ4"を表します
音価と音程は省略可能でありその場合はデフォルト値が使われます
オクターブは"+"または"-"で表すこともできます
また":"は省略可能です
休符は数字のみで表されて例えば"4"は"4分休符"を表します
(READMEにある)シンプソンのテーマは次のようになります
32 + C E F# 8:A G E C - 8:A 8:F# 8:F# 8:F# 2:G
これは以下を表しています
32分休符 1オクターブ上げ 4分C 4分E 4分F# 8分A 4分G 4分E 4分C 1オクターブ下げ 8分A 8分F# 8分F# 8分F# 2分G
インストール
sudo port install portaudio sudo gem install bloopsaphone -- --with-opt-lib=/opt/local/lib --with-opt-include=/opt/local/include
bloopsaphoneはportaudioというオーディオI/Oライブラリに依存しています
音質調整メソッド
音質調整はBloops#soundメソッドで生成されるSoundクラスの
インスタンスメソッドで行います
それぞれ0.0〜1.0の値をセットします
lead = b.sound Bloops::SQUARE lead.volume = 0.5 lead.attack = 0.02 lead.sustain = 0.4 lead.decay = 0.7 lead.phase = 0.0 lead.psweep = 0.0 lead.vibe = 0.0 lead.vspeed = 0.5
各メソッドの機能は名前から推測できるものもありますが
そうでないものもあります
値を調整して試してみるほかないようです
音質調整は設定ファイルをロードして行うこともできます
require 'bloops' bases = Dir["../../sounds/*.blu"] b = Bloops.new bases.each do |base| puts "** playing scale using #{base[/\w+.blu/]}" sound = b.load base b.tune sound, "c c# d eb e f f# g ab a bb b + c" b.play sleep 1 while !b.stopped? b.clear end
このコードはライブラリに添付の複数の設定ファイルを読み出し
各音質で同じコードを再生するものです
音質調整で様々な音が再現できることが分かると思います
ここで再生した各設定ファイルは以下の通りです
dart.blu type noise punch 0.524 sustain 0.160 decay 0.367 freq 0.296 slide -0.373 vibe 0.665 vspeed 0.103 phase 0.141 psweep -0.005
error.blu type square sustain 0.333 decay 0.380 freq 0.336 slide 0.292 square 0.289 sweep 0.020 vibe 0.002 lpf 0.220 lsweep 0.015 resonance 0.875 aspeed 0.035 repeat 0.551
ice.blu type square punch 0.441 sustain 0.067 decay 0.197 freq 0.499
jump.blu type square sustain 0.266 decay 0.187 freq 0.268 slide 0.179 square 0.326 vibe 0.227 vspeed 0.231
pogo.blu type square volume 0.977 punch 0.190 sustain 0.165 slide 0.100 dslide 0.030 square 0.048 sweep -0.055 vibe 0.437 vspeed 0.310 lpf 0.355 resonance 0.185 hpf 0.205 hsweep 0.255 arp 0.677 aspeed 0.275 phase 0.200 psweep -0.565 repeat 0.500
stun.blu type sawtooth sustain 0.306 decay 0.477 freq 0.429 slide 0.217 repeat 0.677
メソッド一覧
bloopsaphoneのメソッド群は次のようになります
Bloopsクラス instance methods: clear, load, play, sound, stopped?, tempo, tempo=, tune constants: SQUARE, SAWTOOTH, SINE, NOISE Soundクラス instance methods: arp, arp=, aspeed, aspeed=, attack, attack=, decay, decay=, dslide, dslide=, freq, freq=, hpf, hpf=, hsweep, hsweep=, limit, limit=, lpf, lpf=, lsweep, lsweep=, phase, phase=, psweep, psweep=, punch, punch=, repeat, repeat=, resonance, resonance=, slide, slide=, square, square=, sweep, sweep=, sustain, sustain=, type, type=, vibe, vibe=, vspeed, vspeed=, vdelay, vdelay=, volume, volume= Trackクラス instance methods: to_s
BloopSong DSL
bloopsaphoneで長い楽譜を再生しようとすると
以下のような問題がありました
- CPUの占有率が徐々に上がって途中で再生不能になる
- トラック数が増えるとコードの可読性が下がる
- トラック数を変えたり楽譜の一部の小節だけを再生をするのに手間が掛かる
そこでこれらの問題に対応するために
BloopSongという簡単なDSLを書きました
require "bloops" class BloopSong def self.init(tempo) @bloops = Bloops.new @bloops.tempo = tempo yield self self end def self.play(score, opt={}) score = read_score(score) tunes = Array(opt[:tune] || :lead) range_max = score[tunes.first].length-1 for i in range(opt[:range], range_max) tunes.each do |tune| @bloops.tune send(tune), score[tune][i] end @bloops.play sleep 0.01 until @bloops.stopped? @bloops.clear end end def self.define_class_method(name) (class << self; self end).module_eval { define_method(name) { yield } } end def self.sound(name=:lead, type) self.instance_variable_set("@#{name}", @bloops.sound( Bloops.module_eval(type.to_s) )) define_class_method(name) { self.instance_variable_get("@#{name}") } yield send(name) if block_given? @bloops end def self.read_score(score) q = Hash.new([]) flag = :lead score.each_line do |line| next if line =~ /^\s*$/ case line when /^:(\w+)/ then flag = $1 else q[flag.to_sym] += [line] end end q end def self.range(opt, max) if !opt (0..max) elsif opt.end > max || opt.end < 0 (opt.begin..max) else opt end end end
以下のように使います
require_relative "bloopsong" mario = BloopSong.init(216) do |b| b.sound(:lead, :SQUARE) do |s| s.volume = 0.4 s.punch = 0.441 s.sustain = 0.067 s.decay = 0.297 s.freq = 0.499 end b.sound(:lead2, :SQUARE) do |s| s.volume = 0.4 s.punch = 0.441 s.sustain = 0.067 s.decay = 0.297 s.freq = 0.499 end b.sound(:base, :SQUARE) do |s| s.volume = 0.4 s.punch = 0.641 s.sustain = 0.197 s.decay = 0.197 s.freq = 0.499 end end mario.play(DATA, :range => 0..-1, :tune => [:lead, :lead2, :base]) __END__ :lead 8E5 8E5 8 8E5 8 8C5 4E5 4G5 4 4G4 4 4C5 8 8G4 4 4E4 8 4A4 4B4 8A#4 4A4 6G4 6E5 6G5 4A5 8F5 8G5 8 4E5 8C5 8D5 4B4 8 4C5 8 8G4 4 4E4 8 4A4 4B4 8A#4 4A4 6G4 6E5 6G5 4A5 8F5 8G5 8 4E5 8C5 8D5 4B4 8 4 8G5 8F#5 8F5 4D#5 8E5 8 8G#4 8A4 8C5 8 8A4 8C5 8D5 4 8G5 8F#5 8F5 4D#5 8E5 8 4C6 8C6 4C6 4 4 8G5 8F#5 8F5 4D#5 8E5 8 8G#4 8A4 8C5 8 8A4 8C5 8D5 :lead2 8F#4 8F#4 8 8F#4 8 8F#4 4F#4 4G4 4 4G4 4 4E4 8 8E4 4 4C4 8 4C4 4D4 8C#4 4C4 6C4 6E4 6B4 4C5 8A4 8B4 8 4A4 8E4 8F4 4D4 8 4E4 8 8E4 4 4C4 8 4C4 4D4 8C#4 4C4 6C4 6E4 6B4 4C5 8A4 8B4 8 4A4 8E4 8F4 4D4 8 4 8E5 8D#5 8D5 4B4 8C5 8 8E4 8F4 8A4 8 8C4 8E4 8F4 4 8E5 8D#5 8D5 4B4 8C5 8 4G5 8G5 4G5 4 4 8E5 8D#5 8D5 4B4 8C5 8 8E4 8F4 8A4 8 8C4 8E4 8F4 :base 8D3 8D3 8 8D3 8 8D3 4D3 4G3 4 4G3 4 4G3 8 8E3 4 4C3 8 4F3 4G3 8F#3 4E3 6E3 6C4 6E4 4F4 8D4 8E4 8 4C4 8A3 8B3 4G3 8 4G3 8 8E3 4 4C3 8 4F3 4G3 8F#3 4E3 6E3 6C4 6E4 4F4 8D4 8E4 8 4C4 8A3 8B3 4G3 8 4C3 8 8G3 4 4C4 4F3 8 8C4 4C4 4F3 4C3 8 8E3 4 8G3 8C4 2 4 4G3 4C3 8 8G3 4 4C4 4F3 8 8C4 4C4 4F3
BloopSong.initの引数にテンポを渡し
そのブロック内で各楽器の設定を行います
楽器を指定するsoundメソッドの引数には
楽器名とその種類をシンボルで渡し
そのブロック内で音質調整を行います
楽譜はplayメソッドの第1引数に文字列として渡します
渡す文字列においてパート名はシンボル表記します
省略すると:leadが自動的にセットされます
BloopSong.playメソッドは
:rangeと:tuneの2つキーワード引数を取ります
rangeは楽譜の再生行(小節ではない)を指定します
例のようにするか省略した時にはすべてを再生します
tuneで指定したパートのみを再生します
コードは以下のリンクにあります
melborne's bloopsong at master - GitHub
(追記:2010-12-4)bloopsongの修正に伴い、コードおよび一部記述を修正しました。
[関連サイト]
Early 8-bit Sounds from _why's Bloopsaphone
A chiptune cover of Phoenix’s “1901”
AdminMyServer - ruby pong with shoes and bloopsaphone