SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|11|12|
2025|01|02|03|

2025-03-07(Fri) ALSAでPulseAudioで音を鳴らす

  OSSアプリを書いたら、ALSAアプリも、PulseAudioアプリも書いてみたくなった。それを順に辿ることでサウンド機能の進化を体験できるかもしれない。

  しかし情報が少ない。Cで書くのは面倒だからRubyで書きたい。GEMにALSAのバインディングくらいあんじゃないの? えぇい、AIに聞いちゃえ。「RubyでALSAで音を鳴らすプログラムを書いてください」。そのまんまだ。

  出てきた……て、なんだこれ。GEMを使ってない。FFIてナニ? 調べたら「Foreign function interface」とある。え。Ruby上でバインディングを書けるってこと!? そういうのもあるのか! 知らんかった。

  前にもあったが、サンプルコード的なものを書かせると、AIはちゃんとしたものを出してくる。ほぼそのまま動いたが、あちこちオレ風にリライトして仕上げたのが以下。その作業を通じてコードはオレの血肉となるのだ。

#!/usr/bin/env ruby
 
# $ bundle add ffi
require 'bundler/setup'
 
require 'ffi'
include Math
 
module ALSA
    extend FFI::Library
    ffi_lib 'asound'
 
    # https://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html
    attach_function :snd_pcm_open, [:pointer, :string, :int, :int], :int
    attach_function :snd_pcm_set_params, [:pointer, :int, :int, :uint, :uint, :int, :uint], :int
    attach_function :snd_pcm_avail, [:pointer], :int
    attach_function :snd_pcm_writei, [:pointer, :pointer, :ulong], :long
    attach_function :snd_pcm_close, [:pointer], :int
 
    SND_PCM_STREAM_PLAYBACK = 0
    SND_PCM_FORMAT_S16_LE = 2
    SND_PCM_ACCESS_RW_INTERLEAVED = 3
end
 
p_pcm = FFI::MemoryPointer.new(:pointer)
name = 'default'                                # 'hw:2,0', 'plughw:2,0'
ALSA.snd_pcm_open(p_pcm, name, ALSA::SND_PCM_STREAM_PLAYBACK, 0) == 0 or raise('snd_pcm_open failed.')
pcm = p_pcm.read_pointer
 
channels = 1
rate = 44100
ALSA.snd_pcm_set_params(pcm, ALSA::SND_PCM_FORMAT_S16_LE, ALSA::SND_PCM_ACCESS_RW_INTERLEAVED, channels, rate, 1, 500000) == 0 or raise('snd_pcm_set_params failed.')
 
freq = 440
duration = 2
p_buffer = FFI::MemoryPointer.new(:int16, frames = 2048)
omega = 2.0 * PI * freq / rate
theta = 0
(duration * rate).times {|p|
    p_buffer.put_int16(p % frames * 2, (sin(theta += omega) * 32700).to_i)
    if((p + 1) % frames == 0)
        a0 = ALSA.snd_pcm_avail(pcm)
        ALSA.snd_pcm_writei(pcm, p_buffer, frames) < 0 and raise('snd_pcm_writei failed.')  # blocking
        puts('%s: %5d -> %5d' % [Time.now.strftime('%H:%M:%S.%N'), a0, ALSA.snd_pcm_avail(pcm)])
    end
}
 
ALSA.snd_pcm_close(pcm)

  引き続き、PulseAudio版。こっちも情報が少ない。Simple APIのサンプルは見つかったが、発声中にブロッキングされる仕様では、ゲームに使えない。Asynchronous APIを使うべきだが……無闇に複雑な前処理が要るんだなぁ。結局、CのサンプルをRubyのFFI向けに書き直した。

#!/usr/bin/env ruby
 
# $ bundle add ffi
require 'bundler/setup'
 
require 'ffi'
include Math
 
module PulseAudio
    extend FFI::Library
    ffi_lib 'pulse'
 
    # https://freedesktop.org/software/pulseaudio/doxygen/mainloop_8h.html
 
    # pa_mainloop *pa_mainloop_new(void);
    attach_function :pa_mainloop_new, [], :pointer
    # pa_mainloop_api* pa_mainloop_get_api(pa_mainloop*m);
    attach_function :pa_mainloop_get_api, [:pointer], :pointer
    # pa_context *pa_context_new(pa_mainloop_api *mainloop, const char *name);
    attach_function :pa_context_new, [:pointer, :string], :pointer
 
    # void (*pa_context_notify_cb_t)(pa_context *c, void *userdata);
    callback :pa_context_notify_cb, [:pointer, :pointer], :void
    # void pa_context_set_state_callback(pa_context *c, pa_context_notify_cb_t cb, void *userdata);
    attach_function :pa_context_set_state_callback, [:pointer, :pa_context_notify_cb, :pointer], :void
 
    # int pa_context_connect(pa_context *c, const char *server, pa_context_flags_t flags, const pa_spawn_api *api);
    attach_function :pa_context_connect, [:pointer, :string, :int, :pointer], :int
 
    # pa_context_state_t pa_context_get_state(const pa_context *c);
    attach_function :pa_context_get_state, [:pointer], :int
 
    # pa_stream* pa_stream_new(pa_context *c, const char *name, const pa_sample_spec *ss, const pa_channel_map *map);
    attach_function :pa_stream_new, [:pointer, :string, :pointer, :pointer], :pointer
 
    # void (*pa_stream_request_cb_t)(pa_stream *p, size_t nbytes, void *userdata);
    callback :pa_stream_request_cb, [:pointer, :int, :pointer], :void
    # void pa_stream_set_write_callback(pa_stream *p, pa_stream_request_cb_t cb, void *userdata);
    attach_function :pa_stream_set_write_callback, [:pointer, :pa_stream_request_cb, :pointer], :void
 
    # int pa_stream_connect_playback(pa_stream *s, const char *dev, const pa_buffer_attr *attr, pa_stream_flags_t flags, const pa_cvolume *volume, pa_stream *sync_stream);
    attach_function :pa_stream_connect_playback, [:pointer, :string, :pointer, :int, :pointer, :pointer], :int
 
    # int pa_stream_write(pa_stream *p, const void *data, size_t nbytes, pa_free_cb_t free_cb, int64_t offset, pa_seek_mode_t seek);
    attach_function :pa_stream_write, [:pointer, :pointer, :int, :pointer, :int, :int], :int
 
    # int pa_mainloop_run(pa_mainloop *m, int *retval);
    attach_function :pa_mainloop_run, [:pointer, :pointer], :int
    # int pa_mainloop_iterate(pa_mainloop *m, int block, int *retval);
    attach_function :pa_mainloop_iterate, [:pointer, :int, :pointer], :int
 
    PA_CONTEXT_NOAUTOSPAWN      = 1
 
    PA_CONTEXT_UNCONNECTED      = 0
    PA_CONTEXT_CONNECTING       = 1
    PA_CONTEXT_AUTHORIZING      = 2
    PA_CONTEXT_SETTING_NAME     = 3
    PA_CONTEXT_READY            = 4
    PA_CONTEXT_FAILED           = 5
    PA_CONTEXT_TERMINATED       = 6
 
    PA_SAMPLE_U8                = 0
    PA_SAMPLE_ALAW              = 1
    PA_SAMPLE_ULAW              = 2
    PA_SAMPLE_S16LE             = 3
    PA_SAMPLE_S16BE             = 4
 
    PA_SEEK_RELATIVE            = 0
    PA_SEEK_ABSOLUTE            = 1
    PA_SEEK_RELATIVE_ON_READ    = 2
    PA_SEEK_RELATIVE_END        = 3
end
 
class Pa_sample_spec < FFI::Struct
    layout(
        :format,    :int,
        :rate,      :uint32,
        :channels,  :uint8,
    )
end
 
class Userdata < FFI::Struct
    layout(
        :gain,      :int,
        :omega,     :double,
        :theta,     :double,
        :data,      :pointer,
    )
end
 
def pa_context_notify
    FFI::Function.new(:void, [:pointer, :pointer]) {|c, p_userdata|
        state = PulseAudio.pa_context_get_state(c)
        puts('state: %d' % state)
        case(state)
          when PulseAudio::PA_CONTEXT_UNCONNECTED
          when PulseAudio::PA_CONTEXT_CONNECTING
          when PulseAudio::PA_CONTEXT_AUTHORIZING
          when PulseAudio::PA_CONTEXT_SETTING_NAME
          when PulseAudio::PA_CONTEXT_READY
 
            ss = Pa_sample_spec.new
            ss[:format] = PulseAudio::PA_SAMPLE_S16LE
            ss[:rate] = 44100
            ss[:channels] = 1
 
            stream = PulseAudio.pa_stream_new(c, 'SineWave', ss, nil)
            puts('stream: 0x%016X' % stream)
 
            PulseAudio.pa_stream_set_write_callback(stream, pa_stream_request, p_userdata)
 
            r = PulseAudio.pa_stream_connect_playback(stream, nil, nil, 0, nil, nil)
            puts('connect_playback: %d' % r)
 
          when PulseAudio::PA_CONTEXT_FAILED
          when PulseAudio::PA_CONTEXT_TERMINATED
        end
    }
end
 
def pa_stream_request
    FFI::Function.new(:void, [:pointer, :int, :pointer]) {|p, nbytes, p_userdata|
        userdata = Userdata.new(p_userdata)
        data = userdata[:data]                                  # ループの外にある必要!?
        nbytes.times {|t|
            v = userdata[:gain] * sin(userdata[:theta] += userdata[:omega])
            data.put_int16(t * 2, v.to_i)
#           userdata[:data].put_int16(t * 2, v.to_i)            # これだとなぜか SEGV...
        }
        r = PulseAudio.pa_stream_write(p, userdata[:data], nbytes, nil, 0, PulseAudio::PA_SEEK_RELATIVE)
        puts('stream_write(%d): %d' % [nbytes, r])
    }
end
 
#-------------------------------------------------------------------------------
#
#   Main
#
mainloop = PulseAudio.pa_mainloop_new()
puts('mainloop: 0x%016X' % mainloop)
 
mainloop_api = PulseAudio.pa_mainloop_get_api(mainloop)
puts('mainloop_api: 0x%016X' % mainloop_api)
 
context = PulseAudio.pa_context_new(mainloop_api, 'SineWaveAsync')
puts('context: 0x%016X' % context)
 
userdata = Userdata.new
userdata[:gain] = 32700
userdata[:omega] = 2.0 * PI * 440 / 44100
userdata[:theta] = 0
userdata[:data] = FFI::MemoryPointer.new(:int16, 32768)
PulseAudio.pa_context_set_state_callback(context, pa_context_notify, userdata)
 
r = PulseAudio.pa_context_connect(context, nil, PulseAudio::PA_CONTEXT_NOAUTOSPAWN, nil)
puts('context connect: %d' % r)
 
#r = PulseAudio.pa_mainloop_run(mainloop, nil)
loop {
    r = PulseAudio.pa_mainloop_iterate(mainloop, 0, nil)
    print('.')
    sleep(0.01)
}

  んが、セグメンテーションフォルト連発。音は出るものの、すぐに落ちてしまう。うーむ。久々に動かない地獄を長々と這いずり回ってしまった。でも、PulseAudioで音を出す手順は習得できたし、それでよしとするか。常に同じ症状が出ないので、メモリの扱い周りに問題があるのだろうが、FFI特有のアレコレに振り回されるのは本意ではない。

  ちょっと今回は長すぎるグダグダで疲れてしまった。サウンド関係はしばらく倉庫に押し込んで別のことをやることにするよ、パトラッシュ。