1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DXRuby/DXOpal: 任意の音を実行時に生成して鳴らす

Last updated at Posted at 2026-01-12

音声データをファイルとして用意せずに実行時にプログラムで好きな音を生成して使えたらいいのにな……と以前から考えていたのですが、最近になって 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 が普通に使えます。

この記事を読んだ人は(ひょっとしたら)こちらも読んでいます

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?