SVX日記
2025-03-07(Fri) 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)
}
2025-03-09(Sun) ついにデビューのチャンスが!
自分はガンダムよりも断然マクロス派。映画の「愛・おぼえていますか」に衝撃を受けて以降、人生に少なくない割合の影響が出ている。ん? でも、期間限定チャンネルなの? ……ふーん……と、数日後にその理由に気づいた。「『新マクロス』超時空歌姫オーディション2025」だとぉ!? そのタイアップだったんかい。
次の新作はサンライズと組んで、というのは知っていたが、遂に新歌姫の募集にまでこぎつけたんだなぁ。今回はオーディション用に新曲が用意されているらしい。「アイ to アイ」とな。仮歌なんてのも公開されている。仮歌、なんて仕事があるってことも、ヴォーカル修行を始めてから知ったのだが、それを聴くのは初めてだな……。
最近、自分に「初めて聴いた」時に「グッ」とくるか、こないか、という評価ポイントがあることに気づいた(まぁ、何回か聴いてから好きになることもあるが)、ワルキューレの3枚目とかには、ほとんどそれがなかった。FireBomberの新譜にも、だ。単なる好みなのかもしれないが、自分の中には明らかな差がある。
しかも、歴代の歌姫がデモするという企画も熱すぎる。らしさ全開の鈴木みのりも、全開で絞り出してくるMay'nも圧巻。福山芳樹は何してんだ、と思ったら、来週のお楽しみなんかいッ! いや、これは、オレも歌ってみたいッ! ……って思ったら、既にJOY SOUNDで歌えるようになってんのか。うぉぉ、歌うぜぇ。まずは、聴き込んで覚えるぜぇ。そして出撃ッ!
20回くらい歌ったが、これは難度が高い。キーは-5だが、男性としては高目だ。最初のオクターブジャンプ、一気に低めから入って再ジャンプ、サビは高音が連続するし、畳み掛ける様に速くて休む場所も少ない。ピッチも採りにくいところが多い。んが、楽しい。実に挑み甲斐ある曲だ。
オーディション用に公開しているのだから「うたスキ動画」にアップしてそのまま応募できるのかと思ったら、録画はできないようだ。なんでや? なので、その場で適当にiPhoneで録画してみた。オレの歌を聴けぇ!
2025-03-19(Wed) 「星を継ぐもの」シリーズを読み進める
長らく水曜は歌のレッスンの日で、その直前にはカラオケ屋で練習するのがルーティーンだったのだが、行きつけがツブれたり、その代わりとなる店が満室だったりで、仕方なく近所の丸善で時間をツブすことが多くなった。先日「アグレッサーズ 戦闘妖精・雪風」のサイン本を買ったのも、その経緯で起きたことである。
で、別に忘れていたわけではないのだが、その時に書棚に見かけて「読まねば」と思ったのが「ミネルヴァ計画」だ。
だいぶ前。2015年末だ。なんとなく評判の高い「星を継ぐもの」を読み始めた。冒頭部分があまりにつまらないので読み飛ばす、という珍しい経験をしつつ、そこからのあまりの面白さにグイグイと引き込まれ、そしてラスト、2段オチには悶絶させられた。
なぜかその続編の「ガニメデの優しい巨人」と「巨人たちの星」はオーディオブックで購入したのだが、ガニメデはその結末に感心させられたものの、巨人たちは結末の記憶が曖昧。そして、その時点で続編の「内なる宇宙(上)(下)」は出ているものの、最終巻の「Mission to Minerva」は未訳と知って、ちょっとテンションが下がって宙ぶらりんな状態にしてあった。
で、最近になって、最終巻が「ミネルヴァ計画」として出版されると聞いて、そんな長らく放置されていた作品なんて微妙なデキなのでは……と思ったものの、イザ書棚に並んでいるのを見ると……その気になってくるんだよなぁ。まぁ、食い飽きたソバでも、目の前にあれば食いたくなる、みたいなもんか。
しかし問題は「巨人たちの星」の結末の記憶が曖昧なこと……えーいッ! こうなったら全部を復習してやるぜッ! ということで、巨人三部作をオーディオブックで聴き返すことにした。最初はどれだけかかるのやらと思ったが、星を継ぐものに約1週間、ガニメデに約2週間、巨人たちに約2週間と、良いペースで聴き進めることができた。ややこしいストーリではあるが、ジョギング中でも問題なく聴けるものなんだなぁ。結局、巨人たちは結末まで聴いてなかったようだ。ジェヴレン人が出てくる直前くらいで止まっていた。そこまでがつまらないとまでは言わないが、そこからグッと面白くなって、そしてラスト、2段オチに悶絶。またやられた。
で、途中「インサイト 戦闘妖精・雪風」が入ったりもしたのだが……意外な伏兵が間に入ってきた。「人体、なんでそうなった?」だ。まさに、書店で偶然の出会った本だ。SFではなく理科学書。パラパラと中身を確認するが、絶対に面白いとしか思えない。というか「星を継ぐもの」シリーズが人類の進化を扱っていることもあり、間に読むのにピッタリすぎるのだ。まるでダンチェッカー先生の課外授業だ。結局、ポチって読んだのだが、期待以上の面白さだった。
2025-03-21(Fri) 「由比」を旅する
「由比」という場所は、山と海に挟まれた極めて狭い場所で、鉄道と、主要道と、高速道が密集している変な場所である。一応、観光地っぽい場所もあるが、観光地とまでは言えず、割と地味な場所と言っていい。しかし、通過するたび、そのガーリッシュな名前や、景色に好感を抱いていて、いつかその極めて妙な地形を散歩して体感してみたかったのである。
で、開通したばかりの蒲郡バイパスを通って向かったのだが……ひどい渋滞だ。開通したばかりだから交通が集中しているからなのか? 新しい開通区間以外の区間に渋滞が増えている。名古屋〜浜松間がまるごと使い物にならない道になってしまった感じだな。
2025-03-22(Sat) 今日も「由比」を旅する
昨日、旧東海道の散歩を済ませてしまったが、当初の予定では、昨日は宿でゆっくりし、本日、散歩をする予定であった。んが、良い感じの景色、良い感じの負荷でもあるので、今日も、今度は逆から通ってみることにした。「あらゆる道は双方向に通過したらクリア」というルールもあるしね。
ついでに、由比は桜えびが名物ということで、桜えび丼が食べられるという由比漁港を目指すことにした。歩いて峠を超え、由比の駅を通り過ぎ、漁港へ。それほど厳密に時間調整したわけではないが、開店の30分ほど前に着いた。もう何人か並んでいる。それほどのものなのか。
生桜えび丼の大盛りと、かきあげと、桜えびの味噌汁のフルコースを頼む。ふむ……むむむ……これ、旨いのか? テーブルのだし醤油で食べるが、肝心の桜えびの味がわからない。後半、だし汁をかけてお茶漬け風にするが、だし汁は効いているものの、やっぱり桜えびの味がわからない。エビのカラの食感だけが虚しい。かきあげも、さして特別感はない。味噌汁はうまかったが、単なる味噌汁としてだ。