音声データをファイルとして用意せずに実行時にプログラムで好きな音を生成して使えたらいいのにな……と以前から考えていたのですが、最近になって DXRuby の Sound.load_from_memory の存在を知りました。これを使えば実現できます。
DXRuby Sound.load_from_memory(DXRuby 1.4.6 Reference Manual)
単に「ファイルを用意せずに実行時にプログラムで音を生成して使う」だけなら SoundEffect を使う選択肢もあり、簡易に音を作る場合はとても手軽で便利なのですが、あくまで一定の制約の中での音作りになります。たとえば「ファミコンのノイズ音を作って鳴らしたい」「波形メモリ音源を使いたい」といったケースはカバーできないんですよね。
デモ
DXOpal 版のデモです。
概観・インターフェイス
DXRuby の Sound.load_from_memory を見てみます。
DXRuby Sound.load_from_memory(DXRuby 1.4.6 Reference Manual)
リファレンスマニュアルには細々とした説明はありませんが、wav ファイルの中身をそのまま渡せばよいようです(参考: DirectSoundとRubyのプログラミング その10 - mirichiの日記)。
bin = File.binread("sample.wav")
s1 = Sound.load_from_memory(bin, TYPE_WAVE)
s1.play
できれば DXOpal でも似た感じのインターフェイスで使いたくて、音声データを保持するクラスとラッパーメソッドを使って次のような構成にしてみました。
class MemorySound
# ...
end
def sound_load_from_memory(memory_sound)
# ...
end
# MemorySound のインスタンスを用意する
# (音声データを生成して MemorySound インスタンスに持たせる)
memory_sound = ...
# DXRuby の場合
s1 = sound_load_from_memory(memory_sound)
# DXOpal の場合はこんな感じで
sound_register_from_memory(:mysound1, memory_sound)
DXRuby と DXOpal で完全に同じにはしなくてもよいと思いますが、移植する場合に手間や考えることを減らしたい、wavファイルのバイナリがどうとか base64 がどうとかの内部の詳細をアプリの側に露出させたくないみたいな気持ちです。
(移植とか考えなければ気にしなくてもよいですし、そんなにこだわらなくていいかなとも思います。あくまで一例ということで。)
インスタンスの用意と sound_load_from_memory の2つに分けずに、SoundEffect に寄せて
s1 = create_sound(500) { |i, t|
# ...
}
s1.class #=> Sound
こうした方が使う側としては手軽かもしれません。このあたりはお好みで……。
DXRuby
wavefile gem を使うと簡単に wavファイルの中身に相当するデータに変換できます。
require "dxruby"
require "wavefile"
require "stringio"
class MemorySound
SAMPLING_RATE = 44100
attr_reader :bin
def initialize(bin)
@bin = bin
end
def self._to_wav(samples)
buf_fmt = WaveFile::Format.new(:mono, :float, SAMPLING_RATE)
file_fmt = WaveFile::Format.new(:mono, :pcm_16, SAMPLING_RATE)
buf = WaveFile::Buffer.new(samples, buf_fmt)
# エンコーディングを気にしなければ sio = StringIO.new だけでもよい
sio = StringIO.new(String.new(encoding: Encoding::BINARY))
WaveFile::Writer.new(sio, file_fmt) do |writer|
writer.write(buf)
end
sio.string
end
def self.generate(duration_msec)
dur_sec = duration_msec.to_f / 1000
num_samples = dur_sec.to_f * SAMPLING_RATE
samples = [] # Float の配列。値の範囲は -1.0 <= ... <= 1.0
(0...num_samples).each do |i|
ratio = i.to_f / num_samples
t_sec = dur_sec * ratio
samples << yield(i, t_sec)
end
bin = _to_wav(samples)
MemorySound.new(bin)
end
end
def sound_load_from_memory(memory_sound)
Sound.load_from_memory(memory_sound.bin, TYPE_WAV)
end
# --------------------------------
dur_msec = 500 # 音の長さ(ミリ秒)
freq = 440 # 周波数(Hz)
volume = 0.2 # 音量
memory_sound = MemorySound.generate(dur_msec) do |i, t_sec|
Math.sin(2 * Math::PI * freq * t_sec) * volume
end
s1 = sound_load_from_memory(memory_sound)
Window.loop do
if Input.mouse_push?(M_LBUTTON)
s1.play
end
end
DXRuby 1.4.7 では一般的なフォーマット
- サンプリングレート 44100 Hz
- signed int32, リトルエンディアン
- 16 bit/sample
であればモノラル・ステレオどちらでも再生できました。ステレオのデータであればステレオで再生されます。
DXOpal
DXOpal には Sound.load_from_memory がありません。なので、同じことをしたければ DXOpal 本体に手を入れる必要があるかも? ……などと考えていたのですが、data URL(data: URL - URI | MDN)を使えば普通のファイルのURLと同じように Sound.register に渡せることに気付きました。
DXOpal でも wavefile gem を使えれば同じように書けて楽なのですが、今回は JavaScript で wav のデータに変換する作りにしてみました。
全体は GitHub の方で見てください:
// lib.js ... index.html で読み込んでおく
class JsMemorySound {
constructor(params) {
// ...
}
// ...
generate(samples) {
// wavファイルの中身に相当するバイナリデータに変換する
// (ここはありきたりな処理なので生成 AI に聞きながら書きました)
}
// ...
toBase64() {
return new Uint8Array(this.buffer).toBase64();
}
// ...
}
Ruby(Opal)側の MemorySound クラスは JavaScript 側の JsMemorySound クラスのラッパーみたいな感じ。
# main.rb
require "dxopal"
%x{
// lib.js の内容を直接ここに書いてもよい
}
class MemorySound
SAMPLING_RATE = 44100
attr_reader :base64str
def initialize(base64str)
@base64str = base64str
end
def self.generate(duration_msec, &block)
b64str = nil
%x{
const jsms = new JsMemorySound({
bitsPerSample: 16,
numChannels: 1,
sampleRate: #{SAMPLING_RATE},
durationMsec: duration_msec,
});
jsms.generate(block);
b64str = jsms.toBase64();
}
MemorySound.new(b64str)
end
end
def sound_register_from_memory(name, mem_sound)
Sound.register(name, "data:audio/wav;base64," + mem_sound.base64str)
end
# ----------------
dur_msec = 500 # 音の長さ(ミリ秒)
freq = 440 # 周波数(Hz)
volume = 0.2 # 音量
memory_sound = MemorySound.generate(dur_msec) { |_, t_sec|
Math.sin(2 * Math::PI * t_sec * freq) * volume
}
sound_register_from_memory(:s1, memory_sound)
Window.load_resources do
Window.loop do
if Input.mouse_push?(M_LBUTTON)
Sound[:s1].play
end
end
end
ファミコン風の音など、いくつかサンプルを作ってみました。上の方に貼ったものと同じです。
DXOpal の場合は JavaScript のライブラリも利用できます。せっかくなので Tone.js の FMSynth と MetalSynth(金属っぽい音が作れる)で作ったサンプルも並べてみました。
DXJRuby
v0.2.7 で対応しました。
インターフェイスは DXOpal 版を踏襲していますが wavefile gem が普通に使えます。
この記事を読んだ人は(ひょっとしたら)こちらも読んでいます