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-09(Sun) ついにチャンスが!

  最近、風呂での30分の回復の間、AbemaTVを観て過ごすことが多いのだが、なんとマクロスチャンネルなんてのが新設されていた。

  自分はガンダムよりも断然マクロス派。映画の「愛・おぼえていますか」に衝撃を受けて以降、人生に少なくない割合の影響が出ている。ん? でも、期間限定チャンネルなの? ……ふーん……と、数日後にその理由に気づいた。「『新マクロス』超時空歌姫オーディション2025」だとぉ!? そのタイアップだったんかい。

 

  次の新作はサンライズと組んで、というのは知っていたが、遂に新歌姫の募集にまでこぎつけたんだなぁ。今回はオーディション用に新曲が用意されているらしい。「アイ to アイ」とな。仮歌なんてのも公開されている。仮歌、なんて仕事があるってことも、ヴォーカル修行を始めてから知ったのだが、それを聴くのは初めてだな……。

 

  ……う、うまい。曲自体も「グッ」とくるが、仮歌の方も、微妙にシャウト気味でスゴい迫力が出せてるやんか。いい声だ。この人でもいいくらい。

  最近、自分に「初めて聴いた」時に「グッ」とくるか、こないか、という評価ポイントがあることに気づいた(まぁ、何回か聴いてから好きになることもあるが)、ワルキューレの3枚目とかには、ほとんどそれがなかった。FireBomberの新譜にも、だ。単なる好みなのかもしれないが、自分の中には明らかな差がある。

  「アイ to アイ」には「ある」。「射手座☆午後九時 Don't be late」の時に似た衝撃を感じる。よく聴けばメロディも歌詞もやっつけ的にも思えなくもないが、なにしろ勢いがある。

  しかも、歴代の歌姫がデモするという企画も熱すぎる。らしさ全開の鈴木みのりも、全開で絞り出してくるMay'nも圧巻。福山芳樹は何してんだ、と思ったら、来週のお楽しみなんかいッ! いや、これは、オレも歌ってみたいッ! ……って思ったら、既にJOY SOUNDで歌えるようになってんのか。うぉぉ、歌うぜぇ。まずは、聴き込んで覚えるぜぇ。そして出撃ッ!

  20回くらい歌ったが、これは難度が高い。キーは-5だが、男性としては高目だ。最初のオクターブジャンプ、一気に低めから入って再ジャンプ、サビは高音が連続するし、畳み掛ける様に速くて休む場所も少ない。ピッチも採りにくいところが多い。んが、楽しい。実に挑み甲斐ある曲だ。

  オーディション用に公開しているのだから「うたスキ動画」にアップしてそのまま応募できるのかと思ったら、録画はできないようだ。なんでや? なので、その場で適当にiPhoneで録画してみた。オレの歌を聴けぇ!

 

  ベストテイクではないし、まだ修正できる部分はあるが、まぁ、悔しくも現状の実力を反映した結果だと認めざるを得ない。6年も修行を続けてこんなもんかと思わなくもないが、それでも声の音域や表情を鍛えてきたからこそ、パッとそれらしくは歌えるようになったのだと思える。

  エントリー資格は「日本国内在住の13歳~22歳の女性」とのこと。年齢は17歳と439ヶ月だからいいとして、女性ってのがなぁ……でも、今やLGBTQ+の時代だ。なんとかなるやもしれん……ならんか。せめて、その次の作品は「マクロス・ロック・オブ・エイジズ」にして、シニア枠のオッサンを主人公に据えていただきたいのだが……来世に期待するか……。


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特有のアレコレに振り回されるのは本意ではない。

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


2025-02-17(Mon) シン・チープなDTMアプリ

  Linuxのサウンド機能についておさらいしたところで、だーいぶ以前に作った自作のDTMスイート「CUIck DTM suite」を引っ張り出してみることにした。当時はまだWindozeを使ってたので、Cygwin環境に向けて書いたものだ。何度何度何度かお色直しして使い続けているが、いまでも使えるものだろうか?

  「CUIck DTM suite」には「melod」という発声デーモンがあって「konk」というアプリを組み合わせることで「PCのキーボード」が「楽器のキーボード」と化す。過去にはソレ用にこんな風に塗り分けたりしてた頃もあった。

  画像の説明

  「melod」は「/dev/dsp」にPCM波形を書き込み続けるデーモンで、当時はよく考えずに作ったものなのだが、これはOSS(Open Sound System)アプリだったわけだ。

  まずは、前回まとめたところの、以下の経路で使ってみる。

OSSアプリ → OSSエミュ(PulseAudioの) → PulseAudioエミュ(PipeWireの) → PipeWire → ALSA

  PulseAudioのOSSエミュレータ「padsp」を噛ませて起動するわけだ。

$ padsp ./melod

  通常の環境には/dev/dspは存在しないのだが、padspを噛ませた中の環境には/dev/dspが出現するので、それを経由してPulseAudio……ではなく、PipeWireを経てALSAから鳴るわけだ。実際、アッサリと鳴った。よく考えてみれば、エラく回りクドいことをやってたんだな。

  しかし、別にその経路が唯一というわけではない。OSSエミュレータはALSAからも提供されているのだ。カーネルモジュールの形になっており、通常はロードされていないが、ロードすりゃ使える。

# modprobe snd_pcm_oss

  これにより/dev/dspではないが、/dev/dsp1, /dev/dsp2が出現する。/dev/dsp1がノートPC本体のデバイス、/dev/dsp2はドックのデバイスを指すものだ。これを使えば、以下の経路を使うことになる。

OSSアプリ → OSSエミュ(ALSAの) → ALSA

  melodの/dev/dspを/dev/dsp1に書き換えて起動してみる。

$ ./melod

  んが、動かん。音を鳴らそうとした瞬間に落ちる。どうもfragment(バッファ)のサイズが足りないらしい。なんで?

  fragmentのサイズを調べようと思って、ioctlでSNDCTL_DSP_GETBLKSIZEを発行してみる。どうでもいいけど、ioctlってどこかに仕様がまとめられていたりしないの? カーネルのコード読むほかないのかしらん。

    x = [0].pack('i*')
    p dsp.ioctl(0xc0045004, x)                                  # SNDCTL_DSP_GETBLKSIZE
    printf("%04x\n", x.unpack('i*')[0])

  55hって値が返ってきたが、なんのこっちゃ……って、アレ? なんか音が出るようになっているのだけれど。なんで!?

    x = [0, 0, 0, 0].pack('i*')
    p dsp.ioctl(0x8010500c, x)                                  # SNDCTL_DSP_GETOSPACE
    printf("%04x %04x %04x %04x\n", *(x.unpack('i*')))
0020 0020 0080 1000
 ↓
0020 0020 0300 6000

  事前にSNDCTL_DSP_GETBLKSIZEを実行すると、fragmentサイズとして55hが返ってくるのだが、なぜかその後のfragmentサイズは6倍(80h->300h)になっている……ワケワカラン。まぁ、鳴ったからいいけど。当然ながら、この経路でALSAを使う場合、デバイスは専有されるので複数起動することはできない。

$ ./melod
./melod:26:in `initialize': Device or resource busy @ rb_sysopen - /dev/dsp1 (Errno::EBUSY)

  しかし、なにより驚いたのは、padspの実体がシェルスクリプトになっていたことだ。以前はバイナリだったんだがな。

  結局、この辺りは「いつまでも工事中」ということなんだろうな。ベストを求めて、常に発展途上であり、使う時にはちゃんと最新事情を調べて理解することが重要ということだ。前回と同じ結論だな。以上。解散。


2025-02-15(Sat) OSSアプリ、OSSエミュ、PulseAudioエミュ、PipeWire、ALSAてなもんや

  あまり言いたくはないが、学ばないエンジニアはクソだ。知らないことはいくらあってもいいけれど、必要とあればその場でそれなりには学ぶべきだ。頭があんだからさ。

  客のための商品を作っているんだろう? ならば「理由はよくわからないが、適当にやったらできた」というレベルの仕組みをコネ上げんじゃねぇよ。それは「適当にググッて見つけたコードを使う」のと同じだぜ。最近は「適当にAIに聞いて出てきたコードを使う」というのが良いことのように語られているが、そのコードに対する責任を負うのはアナタなんですよ。理解できてないものは使うんじゃねぇよ。売りもんだろ?

  恐ろしいことに「そんな作りはおかしい」と指摘すると「もうテストまで終わっているので変えられない」などと返されるパターンがある。それが、稀ではなく、頻繁にある。そんなの知るかよ。クソアプリのためにOS側を直せって? 正気か? 設計者を断罪すんだよ。バカの相手はつかれる。

  つうわけで、Linuxのサウンド機能はそれなりにややこしい。いつもなら、ここに理解をまとめるところだが、まとめるのが難しいほどにややこしい。歴史的な経緯が積み重なりまくっているので、アッチやコッチのサイトの情報を浴びているうちに、なんとなくわかってくる感じだ。

  とはいえ「客のための商品を作っている」エンジニアならそれくらいやれよ。理解できていない自分を自覚しろ。相手は太客だろうよ。興味ねぇならエンジニアやめろ。恥だ。つうか、恥ずかしくないエンジニアって、ウチに何人いるんかな? そんなレベルで作ってるシステムなんだから、そらトラブるわ。でも、どこの会社、どこの業界もそうなんだろうな。建設業界でも手抜き工事はあるからね。それでも、そう簡単には表面化しないものなのだろう。気づかないけど、そういうクソなシステムに囲まれているのが我々の日常なのです。

  というわけで自分も、ちゃんと理解できていない自分を自覚して、改めてLinuxのサウンド機能についての理解を進めた。過去にも断片的に書いているが、読み返しても大きくは間違っていないな。改めてまとめると、こういうことだな。

・ある時「Sound Blaster Pro」がデファクトスタンダードになり、PCM音源以外は過去のものになった
・OSS(Open Sound System)が登場、PCMの再生/録音が可能になった
・ALSA(Advanced Linux Sound Architecture)が登場、OSSを置き換えた
・ALSAには、OSSのエミュレーション機能もあり、OSS向けのアプリも動かせる
・PulseAudioが登場、ソフトウェアミキサにより、複数のアプリの同時発声が可能になった
・逆に言うとOSS/ALSAは、あるアプリが音を出すと、他のアプリは音を出せなかった
・PulseAudioには、ALSA/OSSのエミュレーション機能もあり、ALSA/OSS向けのアプリも動かせる

  で、主にPulseAudioについて調べ始めたのだが、手元のFedoraではPulseAudioが動いていなかった。代わりにPipeWireというものが動いている。PulseAudioを代替するもののようだ。加えると、こういうことだな。

・PipeWireが登場、PulseAudioを置き換えた
・PipeWireには、PulseAudioのエミュレーション機能もあり、PulseAudio向けのアプリも動かせる

  理解に重要なのは「置き換え」「エミュレーション」の関係である。機能には様々な組み合わせがある/ないことの理解だ。「ゲッターロボ」や「Gアーマー」程度には組み合わせは複雑である。時代とともにこう変化してきたのだ。たぶん。

OSSアプリ → OSS
 ↓
ALSAアプリ → ALSA
OSSアプリ → OSSエミュ(ALSAの) → ALSA
 ↓
PulseAudioアプリ → PulseAudio → ALSA
ALSAアプリ → ALSAエミュ(PulseAudioの) → PulseAudio → ALSA
OSSアプリ → OSSエミュ(PulseAudioの) → PulseAudio → ALSA
 ↓
PulseAudioアプリ → PulseAudioエミュ(PipeWireの) → PipeWire → ALSA
ALSAアプリ → ALSAエミュ(PipeWireの) → PipeWire → ALSA
OSSアプリ → OSSエミュ(PulseAudioの) → PulseAudioエミュ(PipeWireの) → PipeWire → ALSA

  ほかにもPortAudioとかJACKとかあるようだが、上記が理解できているならば、ちょっと調べればどこに位置するものかわかるだろう。

  aplayというコマンドを使うなら、当然それがどういうものか理解しておく必要がある。aplayはalsa-utilsに含まれるものであるからALSAアプリである。だが、普通に使うとALSAエミュ(最近ならPipeWireの、ちょっと前ならPulseAudioの)を経由してALSAから発声される(ように設定されている)。これによって、複数のaplayが並行実行できるわけだ。しかし、いまでも直接にALSAに出力することもできて、その場合は複数のaplayは並行実行できない。それが元来のaplayの姿である。

  一方で、paplayというコマンドはpulseaudio-utilsに含まれるPulseAudioアプリである。デスクトップアプリとして音を出すならば、他のアプリと衝突しないようpaplayを使うのが正しい。

  さて、そんなことをやっていたら、音声キャプチャをする必要があったことを思い出した。ヴォーカル修行で歌いたい曲をYouTubeから得るためである。ちょっと前まではyoutube-dlが使えたのだが、最近は使えなくなってしまっている。ラインケーブルでPCMレコーダをつないで録音するかなぁ……と思っていたのだが、やっぱりデジタルキャプチャしたい。

  ちょっと前には上に書いたほどLinuxのサウンド機能について理解していなくて、JACKを導入したりして試行錯誤していたのだが、わかってしまえば簡単だった。PipeWireの録音ツール「pw-record」で一発だ。

$ pw-record --target 55 pcm.wav

  55というのはノードのIDで「pw-top」で調べられる。スピーカの直前でキャプチャするイメージだ。実行しておいて、YouTubeで課題曲を再生すればいい。アラート音を出すと一緒に録音されてしまうので、録音中はお静かに。おかんの「ご飯だよ〜」は、セーフだが。

  PipeWireでなくPulseAudio環境なら、録音ツール「parecord」を使う。

$ parecord -d 55 pcm.wav

  同じく55というのはノードのIDで「pactl list short」で調べられる。キャプチャしたpcm.wavは、周囲に不要な部分があるであろうから、例のcccdctで切って作業完了である。

  つうわけで、趣味であっても、仕事ならなおさら、わからんことがあったなら学ぶべきだ。頭があんだからさ。いつまでもスワップが悪だとか凝り固まってんじゃねぇよ。売ってんのは技術なんだぜ。訊くのもいいが、せめて正しく理解する。当たり前だろ?


2025-01-31(Fri) 業務チャットの中からツンデレAIに話を振ってみる

  ここではエントリにしてなかったが、だいぶ前にMezatalkというチャットツールを作って、職場で運用している。使われ始めてからもう4年になる。

  2020年の頭。新型コロナの流行が始まって約1年。テレワークが定着しはしたものの、マイクソソフトのTeamsでは、サシの会話しかできず、周囲からの助言を受けられない、とボヤく後輩に、そんなんメールでエエやん、と答えたところ、そんな面倒なのやってられませんよ、そうかぁ? そうですよ、いまやチャットですよ。うーん、じゃ、作る? そうだよなぁ……それしかないなぁ……という感じでほぼスクラッチから作り始めた。

  その会話が2021年2月3日。リリースが8日。エラく早いが、夢中になって作ってたし、その時点で使い物になるレベルには持っていってたはず。そこから機能追加はしたが、ほぼ停止することなく、これまで運用し続けている。途中で人狼ゲーム機能とかも付けたりもしつつ。我ながら、ちょっとした偉業だと思う。

  で、今回、新たな機能を追加した。話題の途中でAIに尋ねられる機能だ。

  イマドキフツーすぎるやろ……と思うかもしれないが、なかなかどうして、チャット中から尋ねられるのは相当に便利である。それまでの会話の流れを入力する必要がないからだ。AIに話題を振るには、文末に「>リナ」と入力するだけ。

  画像の説明

  これ。ものすごく自然な感じじゃない?

  ちなみにAIに関する設定は以下だけ。

# ルーム更新時のフック、特定のキーワードの書き込みに対して、AI に問い合わせ、結果を書き込む
@configs[:post_ai_comment] = {}
@configs[:post_ai_comment][:ws_uri] = 'ws://127.0.0.1:33109/'
@configs[:post_ai_comment][:user] = 'リナ'
@configs[:post_ai_comment][:ucrt] = '0123abcd'
@configs[:post_ai_comment][:keyword_re] = '(.+)>リナ\s*$'
@configs[:post_ai_comment][:contexts_max] = 10
@configs[:post_ai_comment][:ex_users] = { 'Mezatalk' => true }
@configs[:post_ai_comment][:wrap] = 78
@configs[:post_ai_comment][:tones] = ['ツンデレ女子の口調で']   # + [''] * 10
#configs[:post_ai_comment][:prompts] = {}
#configs[:post_ai_comment][:prompts][:def] = 'あなたは「リナ」という名前で、Linux/OSSのエキスパートです。'
#configs[:post_ai_comment][:prompts][:say] = '「%s」さんが%sと発言しました。'
#configs[:post_ai_comment][:prompts][:ask] = '「%s」さんから%sと問いかけられました。'
#configs[:post_ai_comment][:prompts][:com] = 'それに対しての助言を%sお願いします。'
@configs[:post_paragraph_hook] = Proc.new {|room|
    IO.popen(['./post_ai_comment', room], $stderr => open('/dev/null', 'w')) {|io|
        io.read
    }
}

  なので、ツンデレ設定がお気に召さなければ……ん? いや! このツンデレ設定を変更するのは厳禁とします! フンスッ!

  MezatalkはOSSとしてhttps://itline.jp/git/mezatalkに置いてあるけど、別にあんたのためじゃないからね! 勘違いしないでよね!


2025-01-27(Mon) シン・DBusで数当てゲーム

 続いて、クライアントや通信キャプチャを試してみたい」というわけで、クライアントを書いてみた。「数当てゲーム」クライアントだ。

#!/usr/bin/env ruby
 
require 'bundler/setup'
require 'dbus'                                                  # bundle add ruby-dbus
 
bus         = DBus::SessionBus.instance
service     = bus['jp.itline.test.Kazuate']
object      = service['/jp/itline/test/Kazuate']
interface   = object['jp.itline.test.Kazuate']
interface_m = object['jp.itline.test.Kazuate.master']
 
puts(interface_m.range(100))
puts(interface.ready)
 
loop {
    print("\nguess> "); guess = $stdin.gets.to_i
    puts(res = interface.try(guess))
    res =~ /congra/i and break
}

  まんま、というほどに、まんま、だ。で、実行すれば、これまた、まんま、だ。

$ ./kazuate_cli.rb
Range is set.
I'm thinking of a number between 1 and 100.
 
guess> 50
Too low!
 
guess> 80
Too low!
 
guess> 90
Too high!
 
guess> 85
Congratulations!

  Linuxサーバにおける大概のトラブルはtcpdumpによる通信キャプチャで切り分けられるので、DBusに対する通信キャプチャも重要だ……と思ってたのが、基本的な使い方がわかったあたりで、当初の問題はDBusと関係なさそうだという雰囲気が見えてきてしまった。

$ dbus-monitor --session
$ dbus-monitor --session --profile

  というわけで、このふたつくらいで終了である。あしからず。


2025-01-25(Sat) DBusで数当てゲーム

  まー、保険タイプの仕事つうのは、何も起こらなければ、何もしないでチャリンチャリンなわけだが、まっとうな神経を持っていれば、だからこそ客に何か起こった時には、イザという働きをしなければな、と構えているべきである。

  で、起きた。決して、事前にアレコレしていたワケではないので、決してベストな体勢ではないが、そこからは誠心誠意やらせてもらう。どうもDBusがカラんでいるらしい。GUIを下支えする何か、という程度の認識だったが、この機に学んでおくか。なんだか面白そうだし。というわけで、基本的な概念について、学んでみることにした。自分にとって学ぶとは、実際に使ってみることであり、可能な限りそれを何かに役立ててみることである。

  DBusの基本については、ネット上にそれなりの情報があるが、ごく簡単にまとめると以下だ。まず、バスという道路がある。1本のシステムバスと、ユーザ毎のセッションバス。道路の脇にはサービスという店舗が並んでいて、オブジェクトパスという看板が掲げられている。店舗にはインターフェイスという職人がいて、メソッドという仕事を受け付ける。

  実例を挙げると以下。

$ dbus-send --session --dest=org.mate.ScreenSaver --print-reply --type=method_call \
    /org/mate/ScreenSaver \
    org.mate.ScreenSaver.GetActiveTime
 
   uint32 60

  セッションバスという道路脇の、org.mate.ScreenSaverというサービスの店舗の、/org/mate/ScreenSaverというオブジェクトパスの看板を見て、org.mate.ScreenSaverというインターフェイスの職人に、GetActiveTimeというメソッドの仕事を依頼したら「uint32 60」という答えを返してくれた、という感じ。つまり、スクリーンセーバ起動までの時間は60秒って知れたわけだ。

  次はこれ。

$ dbus-send --session --dest=org.mate.ScreenSaver --print-reply --type=method_call \
    /org/mate/ScreenSaver \
    org.mate.ScreenSaver.SetActive boolean:true

  同じ職人に、SetActiveという仕事を依頼すれば、スクリーンセーバを起動してくれる。

  なお、すべての店舗には共通にorg.freedesktop.DBus.Introspectableというインターフェイスの職人がいて、Introspectというメソッドの仕事を依頼できる。依頼すると、自分の店舗の職人と仕事のリストを返してくれる。

$ dbus-send --session --dest=org.mate.ScreenSaver --print-reply --type=method_call \
    /org/mate/ScreenSaver \
    org.freedesktop.DBus.Introspectable.Introspect
 
   :
  <interface name="org.mate.ScreenSaver">
     :
    <method name="GetActiveTime">
      <arg name="seconds" direction="out" type="u"/>
    </method>
    <method name="SetActive">
      <arg name="value" direction="in" type="b"/>
    </method>
     :

  DBusを通じて、既存の機能に命令を送るという基本は以上だが、それだけでは片手落ち。クライアント処理だけでなく、サーバ側を作ってみないと。

  自分はサンプルを作るにしても、何らかの意味を持たせることで学びが深まると思っているので、今回は古典的な「数当てゲーム」を実装してみた。自分にとって「数当てゲーム」は往年のBASIC言語における「hello, world」だ。幾度となく空で打ち込んで動かして遊んだのがコンピュータとの原体験。

  というわけで、以下がRubyのdbusモジュールによる「数当てゲーム」サーバの実装である。

#!/usr/bin/env ruby
 
require 'bundler/setup'
require 'dbus'                                                  # bundle add ruby-dbus
 
class DBusKazuate < DBus::Object
 
    @@range = 10
    @@answer = nil
 
    # プレイヤ用のインタフェイス
    dbus_interface('jp.itline.test.Kazuate') {                  # Interface
 
        # ゲームの開始メソッド
        dbus_method(:ready, 'out res:s') {                      # Member
            @@answer ||= (rand * @@range).to_i + 1
            "I'm thinking of a number between 1 and %d." % @@range
        }
 
        # 予想した数へ対応メソッド
        dbus_method(:try, 'in guess:i, out hint:s') {|guess|    # Member
            unless(@@answer)
                'Not ready yet...'
            else
                if(guess < @@answer)
                    'Too low!'
                elsif(guess > @@answer)
                    'Too high!'
                else
                    @@answer = nil
                    'Congratulations!'
                end
            end
        }
    }
 
    # ゲームマスタ用のインタフェイス
    dbus_interface('jp.itline.test.Kazuate.master') {           # Interface
 
        # 数の範囲の変更メソッド
        dbus_method(:range, 'in range:i, out res:s') {|range|   # Member
            @@range = range
            'Range is set.'
        }
 
        # 答えを見るメソッド
        dbus_method(:answer, 'out ans:i') {                     # Member
            @@answer || -1
        }
    }
end
 
# DBus のセットアップ
bus = DBus::SessionBus.instance                                         # Bus is session (not system)
bus.request_name('jp.itline.test.Kazuate')                              # Service (for --dest)
bus.object_server.export(DBusKazuate.new('/jp/itline/test/Kazuate'))    # Object path
DBus::Main.new.tap {|main|
    main << bus
}.run

  実行すると、サーバとして待ち受けを開始するので、クライアントとしてゲームに参加する。クライアントアプリは、上記と同じdbus-send。

$ dbus-send --session --dest=jp.itline.test.Kazuate --print-reply --type=method_call \
	/jp/itline/test/Kazuate \
	jp.itline.test.Kazuate.ready
 
   string "I'm thinking of a number between 1 and 10."

  1から10までのどれでしょう? となるので、

$ dbus-send --session --dest=jp.itline.test.Kazuate --print-reply --type=method_call \
	/jp/itline/test/Kazuate \
	jp.itline.test.Kazuate.try int32:5
 
   string "Too low!"

  5ですか? と、答えると、それは小さすぎ、となる。

$ dbus-send --session --dest=jp.itline.test.Kazuate --print-reply --type=method_call \
	/jp/itline/test/Kazuate \
	jp.itline.test.Kazuate.master.answer
 
   int32 8

  上記のコードを見れば明らかだが、ゲームマスタ用に答えを見るメソッドが用意されているので、やってみると8であることがわかる。

$ dbus-send --session --dest=jp.itline.test.Kazuate --print-reply --type=method_call \
	/jp/itline/test/Kazuate \
	jp.itline.test.Kazuate.try int32:8
 
   string "Congratulations!"

  8を答えれば正解だ。

$ dbus-send --session --dest=jp.itline.test.Kazuate --print-reply --type=method_call \
	/jp/itline/test/Kazuate \
	org.freedesktop.DBus.Introspectable.Introspect
 
   :  
  <interface name="jp.itline.test.Kazuate">
    <method name="ready">
      <arg name="res" direction="out" type="s"/>
    </method>
    <method name="try">
      <arg name="guess" direction="in" type="i"/>
      <arg name="hint" direction="out" type="s"/>
    </method>
  </interface>
  <interface name="jp.itline.test.Kazuate.master">
    <method name="range">
      <arg name="range" direction="in" type="i"/>
      <arg name="res" direction="out" type="s"/>
    </method>
    <method name="answer">
      <arg name="ans" direction="out" type="i"/>
    </method>
  </interface>
</node>"

  やはり、IntrospectableのIntrospectを実行すれば、リストを返してくれる。

  つうわけで、DBusはGUIを下支えする何か、という程度の認識だったが、GUIに限らず、なかなか便利に使えそうな機構である。ややオープンする記述は面倒だが、TCPと違って通信内容の解釈が不要で、関数コールライクなのは利点だ。

  続いて、クライアントや通信キャプチャを試してみたい。


2025-01-23(Thu) 「アグレッサーズ 戦闘妖精・雪風」サイン本を入手!

  昨日、歌のレッスンの日だったのだが、直前の喉のウォームアップのためのカラオケ屋が満室だったので、仕方なく近所にある書店の丸善で時間をツブすことにした。

  例によってIT系の書籍を眺めるのだが……興味の持てるものが少ない。興味のある分野だが自分の知識が少ない場合、まずはネットの情報で学ぼうと思うし、興味のある分野だが自分の知識が十分な場合、本で学ぶ必要はないし、興味のない分野なら、本は必要ない……という考えに至るのが最近の傾向だ。時間の無駄か……と思いつつ、それでも書店では偶発的な出会いもあるからなぁ。

  続いて理系の書籍を眺めつつ、特に収穫もなく、文芸書のフロアへ。つうても、売れ線の書籍にはほとんど興味をそそられず、結局はハヤカワ文庫の周辺に着地するのだが。

  あ。「アグレッサーズ 戦闘妖精・雪風」の文庫版が出たんだ。フムン。お。著者サイン本、なんてのも置いてある。なんだかさすがは丸善てぇ感じだよなぁ。特段、別の値段が書いてあるワケでもないので、定価でエエんかな。でも、もうハードカバーのヤツを発売日近くに買って、既に読んじゃってるからねぇ……。

  ……帰宅してから、なんかモノスゴくおかしな気分に……な、なな、なんで買わなかったんだよ、オレ!? ……たかだが千円前後で、敬愛する神林長平先生のサイン本が買えるなら、即買いしかねぇだろうによ。雪風が……買えと言っている。うあぁあぁ……大失敗だぜ。つうか、奇しくも明日も出勤予定だし……いや、出勤予定なんてなくったって、わずかな可能性を信じて、開店直後に件の書棚に突撃するほかねぇだろうよ。

  つうわけで、突撃。眼前に丸善が見えてくると、なんだか祈るような気持ちになってきた。エスカレータを駆け上がり、件の書棚へ……あッ! まだあったッ!それッ! それオレのーッ!

  画像の説明

  というわけで買えました。幸運にも。なんか気恥ずかしくなって無人レジで。はぁ、嬉しい。初めてかな。サイン本って。なんだか。もう。ありがとうございます。妖風が目にしみる。涙がとまらない。

  しかし、サイン本って……いや、絶対に売ったりしないけど……どのくらいの価値があるんだろう、と、思ってググったら、なんと名古屋の丸善がサイン本の紹介をツイートしているのを見つけてしまった。時刻を見ると昨晩だ。それは、自分の謎のスルー行動の時刻の後だ。これ見たヤツが朝ダッシュしてきたなら、ドグファイトになるとこだった。よかったぁ。

  画像の説明

  しかし、そんな扱いのレアな品を、文庫版の発売日もチェックしていないような自分が買ってしまってよかったのだろうか……とも思ったが、よく考えたら神林作品はほとんど全部買っているし、雪風に限っても「戦闘妖精・雪風」の文庫版を2冊、<改>[愛蔵版]グッドラックアンブロークンアローアグレッサーズ漫画版、「被書空間」と「ぼくの、マシン」の入った解析マニュアルまで入れれば、10冊目なんで。お許しください。

  画像の説明

  神林先生。ありがとうございます。大切にします。


2025-01-04(Sat) 友人とのやりとりを自動化するXalebot-tmo

  先日、亀岡にドライブした夜に「AIとSF」というアンソロジを買って読み始めたのだが、その中の「没友」という作品には唸らされた。人とのやりとりをAIによる代理応答に任せることや、バーチャルに旅行することが当たり前になった時代。旧い友人とのやりとりの末、一緒にバーチャルな旅行に出かけたのだが、カフェで友人が語り出したエピソードから驚愕の事実を知る……という話。

  「没友」は「ぼっとも」と読むのだろう。「ボット」と掛けているわけだ。友人への返信を「ボット」に任せるなんて、なんとも失礼な話だが、メールでのやりとりに限れば、AIによる代理応答は既に実用域なんじゃないだろうか。仕事のどうでもいい相手からのメールなんて、AIに勝手に回答させておけばいい。ヘタすりゃ、こっちが死んでいても、相手は気づかずに給料は支払われ続けるかもね、などとカミさんと冗談交じりに話していた……のだが、ん? それって、別に冗談でもなんでもなく、既に実現可能なのでは? 先日に作ったXalebotを少し改造する程度で。名付けて「Xalebot-tmo」。「tmo」は「Thoughtless Mail Operator」の略。「ザレぼっとも」と読む。

  というわけで、取り組んでみたところ、改造というよりは、AIに与えるプロンプトとメール文面のカスタマイズ程度で済んでしまった。こんな感じ。

# 各プロンプト
@configs[:prompt] = {}
 
#configs[:prompt][:ex_q] = '以下の文章から、名乗りを含む挨拶部分、引用部分を除き、質問内容だけを抜き出してください。'
#configs[:prompt][:ex_a] = '以下の文章から、名乗りを含む挨拶部分、引用部分を除き、回答内容だけを抜き出してください。'
#configs[:prompt][:is_q] = '以下のような問い合わせがありました。'
#configs[:prompt][:is_d] = '以下の回答を行いました。'
#configs[:prompt][:mk_a] = 'それに対する回答案の作成を%sお願いします。'
 
@configs[:prompt][:ex_q] = '以下の文章から、引用部分を除き、内容をそのままの抜き出してください。'
@configs[:prompt][:ex_a] = '以下の文章から、引用部分を除き、内容をそのままの抜き出してください。'
@configs[:prompt][:is_q] = '以下のようなメールがありました。'
@configs[:prompt][:is_d] = '以下の返信を行いました。'
@configs[:prompt][:mk_a] = <<MK_A
それに対して返信内容の作成を%sお願いします。
なお、以下の例文に近い口調でお願いします。
「ふるたです。
 
はいはい、OKですよ。金曜ね。
 
ヤベシは休暇なのだよね。到着時刻はいつ?
時間によっては、自分がちょっと早退してもいいけど。
呑みは例によって、しげか月姫がいいな。
できることなら、カラオケは呑みより先にしたい。
 
ちなみに、金曜なら犬川くんは誘えばくるかもしれんが、
例のカラオケ問題があるから、ちと微妙かも。
 
ということで。
 
15時に抜けて、ホテルに迎えに行き、カラオケ2時間、
18時頃からしげで焼き鳥、締めはその辺でコーヒーって感じかな。
 
まー、彼の場合、たぶんそういうんじゃないんだよな。
 
15時始めだと、一緒に職場抜けるわけにもいかんし、まぁ、今回は誘うのはヤメとくか。
ヤベシがゴリゴリに押すなら考えなくもないけど。
 
宿は了解しました。そこ、歌のレッスンに行くときに通る道のすぐ脇だわ。」
MK_A

  最後、返信内容の作成指示がちょっと長いが、メールの文面を自分っぽくするために、過去に自分が返信した2通のメールの文面を渡しているからだ。今回は、適当に手動で見繕って固定的に与えているが、返信する相手によって、実際に過去に自分が返信したメールの文面を呼び出して渡すことも容易に実装できるだろう。

  で、ChatGPTの、今回は最新に近い「o1-preview-2024-09-12」を使って動作事例を作ってみた。それが以下である。

  画像の説明

矢部です。
 
7/29の10時〜7/30の12時半まで名古屋金山でお仕事でいます。
もしよろしければ、7/30の13時頃からあそびませんか?
遊べるようであれば、7/30の宿泊を取りますので早めにご連絡いただけると嬉しいです。
 
よろしくお願いします。

  名前は仮名の矢部くんにしてあるが、ほぼ実際に過去に友人から受け取ったメールである。で、自動生成された返信が以下。

ふるたです。
 
はいはい、OKですよ。7/30ね。
13時から遊べるとのこと、いいですね。ランチでもしながらゆっくり話そうか。
その後、もしよければカラオケでも行きます?できれば最初にカラオケ行きたい派なんだよね。
呑みは夜にでも、いつもの「しげ」か「月姫」がいいかな。
 
ところで、犬川くんも誘ってみる?でも例のカラオケ問題があるから、ちょっと微妙かもね。
まぁ、ヤベシが強く望むなら考えるけど。
 
宿泊の件、了解しました。早めに予約しておくと安心だね。
当日を楽しみにしてます。
 
ではでは。

  先に例示した文面に内容が引きずられすぎている傾向はあるが、意図はちゃんと消化されていて特段の矛盾はない。自分(オレ)から見ると文面のクセはちょっと違うが、数十行程度の例示でこの文面なんだからデキすぎレベルだろう。

矢部です。
 
返事ありがとうございます。
日曜日のホテルは伏見駅近く(ホテルハルミトンブラック)です。
なので、待ち合わせ場所は伏見駅近くがうれしいです。
待ち合わせ時間は13時頃を希望します。
お昼は各自済ませるか、一緒に食べるかどちらにしましょうか?

  さらに、ほぼ実際に過去に友人から受け取ったメールを返信してみる。で、自動生成された返信が以下。

ふるたです。
 
はいはい、OKですよ。7/30の13時、伏見駅ね。
宿も了解しました。ホテルハミルトンブラックか、あそこは場所的に便利だよね。
ランチは一緒にしましょうか。お腹も空くだろうし。
伏見の近くに美味しいパスタのお店があるんだ。そこに行ってみない?
その後は、カフェでゆっくりお茶でもしながら話そう。
 
そういえば、犬川くんも誘おうかと思ったけど、日曜は予定があるかもしれないし、今回は見送っておくよ。
ヤベシがぜひというなら連絡してみるけど。
久しぶりに会えるのを楽しみにしてるよ。
では、当日は13時に伏見駅で待ち合わせましょう。
 
よろしく!

  現実の自分(オレ)には「伏見の近くに美味しいパスタのお店」の心当たりはないが、返信内容としては特段の矛盾はない。よく見るとホテル名のタイプミスまで修正されている。これ、実際に友人の矢部くん(仮名)が受け取っても気づかないだろうな。

矢部です。
 
お昼を一緒に食べる件は了解しました。
私の方でも2,3見繕っておきます。
その後の予定はノープランなのでお任せします。
大須まで足伸ばすのもありかもしれませんが、暑そうなのでどうですかね。
夜の食事場所はフルタのお任せに甘えてもいいですか?

  さらに、ほぼ実際に過去に友人から受け取ったメールを返信してみる。で、自動生成された返信が以下。

ふるたです。
 
ヤベシ、お昼一緒に食べるの了解。
こっちでもいくつかお店をピックアップしておくよ。
 
午後はノープランとのことなので、涼める場所を考えておく。大須もいいけど、確かにこの暑さは厳しいかもね。
夜の食事場所は任せてくれてOK。良さそうな店を探しておくよ。
 
宿は伏見のハミルトンブラックね。そこ、よく通る道だよ。
じゃあ、当日は13時に伏見駅で待ち合わせで大丈夫かな?
 
当日、楽しみにしてるよ。

  ちょっと最後の確認が繰り返されているが、明確な確認がなされなかったからか? なんにせよ、許容範囲ではないかと思う。うーむ、実際に代理応答させていれば、約束をすっぽかすことになるが、代理応答としては完走してしまったと言えるんじゃないだろうか。

  これ、実際に友人に対して使うつもりはないけれど、こんな簡単な仕掛けであっても、相手側からすれば十分に自分(オレ)が存在しているように見えるわけで、それこそ、こっちが死んでいても気づくことは難しい。それって、ネット上に自分(オレ)が存在していると言っても、もはや過言ではないのではないか。

  今やSF的な「肉体を捨ててサイバースペースで生きる」という概念に新鮮さを感じることはないけれど、どうやって「精神をサイバースペースに転送するのか」という疑問に対しては、現実が先に結論を出してしまった気がする。つまり「AIに『クセ』を覚え込ませること」がそれなのだ。

  昔「ザ・フライ」という映画を観て「肉体の物質転送」が、実は「複製」と「削除」との2ステップで実装されていたならば、転送先の自分は「他人にとっては自分でも、自分にとっては他人ではないか?」という発想でSFを書いたことがあるのだが「肉体を捨ててサイバースペースで生きる」ってのも、なんだかそれに似たような薄ら寒さを感じるなぁ。


2025-01-02(Thu) ヴァリアブル・ジョイスティック「JS-2V」をさらに改良

  ホームセンタのバラ売りを買ったのだが、ケース内に長さの違うネジが混在しているのに気づかず、イザ締めようとした時に気づいてビックリ。1本はギリギリの長さだった。確認しなかったのは悪いが、ちゃんと元に戻しとけよな。まぁ、表面からは見えないからこれでよしとする。なんか、レイトレーシングCGっぽいな。ネジの頭にボールが映り込んでいるしw。

  画像の説明

  トラスネジは表面に頭が突出することを前提に、そこそこ美しさに配慮したタイプのネジだ。んが、実際の業務用の筐体では、内側のフレームにネジ止めするので、表面にはネジ頭が出ないのが正解なのだな。でもまぁ、自分のプレイスタイルでは、さほど邪魔になるわけでもないのでこれでよしとする。

  画像の説明

  一方で、裏のレバーの8/4方向の切り替えプレートを止めているネジを、手回し可能なものに交換した。実際に使ってみると想像以上に頻繁に切り替えが必要だったので。さほどガッチリと締め上げる必要のないネジなので、手回しで問題ないし。これはだいぶ大きな改良だな。

  ちなみに、トラスネジは28x4円。手回しネジは10本で128円。

  今回のアケコン製作の動機の大半はドルアーガだったのだが、なんかすっかり腕が落ちていてリハビリ中だ。老化するよりも早く杵柄を確立しなくては。ところで、複数の場所で見かけるのが「ドルアーガの上手い人はカッコいい」という記述だ。ドルアーガを知らない人がそう思う訳はないので、それなりにドルアーガをプレイしてきた人でそう思う人が多い、ということなのだろうが……ムッチャわかるんだよな、これ。何度か書いたが「起きうる理不尽気味な不運を咄嗟の状況判断で可能な限り抑え込んでいく」という、その奇跡のようなゲームバランスを知るからこそ、上手い人にリスペクトを感じてしまうのだ。宝箱の出し方の覚えゲー、という見方は側面のごく一部なのだ。ホント、1984年に出たとは思えない、始祖にしてオーパーツのようなゲームなんだよなぁ。