予想外のことが起きる。この手のラッキーなことには縁がなかったのに。
猫のゆりかご
これは、あんまり好きではない。
WirePlumber(PipeWire)の音声出力先切り替えスクリプトを書いた
単純なAudio Sinkの切り替えアプリを作ったが
と思ったので、スクリプトをXMonadから実行するように変更した。スクリーンショットを取ってみたけど、知らない人が見たら何だかわからないなこれ。

switch_audio_sink.sh
#!/bin/bash declare -A devices while IFS=$'\t' read -r id name; do devices["$name"]="$id" done < <(wpctl status | awk ' /Sinks:/ {found=1; next} /Sources:/ {found=0} found && /^\s*│/ { gsub(/^[ \t│*]+/, "") if (match($0, /^([0-9]+)\.\s+([^\[]+)/, arr)) { id = arr[1] name = arr[2] sub(/ *$/, "", name) print id "\t" name } } ') count=${#devices[@]} if [ $count -eq 0 ]; then notify-send "Audio Device Selector" "No audio device found" -u critical exit 1 fi name_list=$(printf "%s\n" "${!devices[@]}") selected=$(echo "$name_list" | dmenu -l $count -p "Select Audio sink:") if [ -z "$selected" ]; then exit 0 fi selected_id=${devices["$selected"]} wpctl set-default $selected_id
RustでGUIアプリを試しに作ってみた
Windows11のCTRL+WIN+vで表示されるAudio Sinkの切り替えショートカットがLinux(XMonad)で欲しくなり、探したけど丁度いいものが見つからなかった。
チープに作ればこんなの簡単に作れるはずだと考え、Rustで作ってみることにした。GUIツールキットで何を使うかが問題で、歴史が浅いために決定的に良いものが存在しないらしい。結果として、サンプルアプリを作りながら右往左往することになった。
最初の試作
まず、tray-iconで試作した。タスクバーにアイコンを表示して、そこからメニューも出せる。ここまでは順調だったけど、ラジオボタンの表示がどうしてもできない。この時点で「今回の用途には合わないな」と判断してボツにしたけど、今思えばこれで十分だったかもしれない。
2回目の試作
次は libappindicator と gtk3 の組み合わせで試作した。アイコンもメニューも出せるところまではいったんだけど、gtk-rs(gtk3バインディング)が、今後のメンテが怪しいと知って破棄することになった。
3回目の試作
じゃぁ gtk4-rs を使おうとしたら、libappindicator との連携が効かない。タスクバーにアイコンを置くことを諦めて、iced を使うことにした。
ラジオボタン付きのメニュー表示まで作ったけど、キーボードでの操作ができない。icedはまだマウス前提のUIしか用意されてないっぽい。自分の目的には合わないのでこれも破棄。
4回目の試作
最終的に gtk4-rs を使うことになった。ただこれ、ラジオボタンすら自前実装が必要で、やる気が削がれた。ListBox で項目を並べて、キーボードで選べるだけの超シンプルな構成にした。
で、このアプリをXMonadの Ctrl + Win + v に割り当てたことで、やっと目的は達成。キーボードだけでオーディオ出力先を切り替えられるようになった。
でも最初から dmenu(かrofi)を使う前提でスクリプト書いたほうが、ずっと素直だった。

miniaudioでdecoding APIを使わないサンプル
マルチプラットフォームでpcm再生できるライブラリを探した結果、miniaudioを使うことにした。ドキュメントにはdecoding APIを使うサンプルがあるので、それを使えば簡単に使うことが出来る。
で、本来の目的の方でチョット問題があったので、decoding APIを使わない実装にすることを考えて、サンプルコードを書いた。https://x68kace.hatenadiary.org/entry/2025/04/05/160252に書いた loadWavFile()
も使っている。
#define MINIAUDIO_IMPLEMENTATION #define MA_NO_ENCODING #define MA_NO_DECODING #define MA_NO_GENERATION #define MA_NO_RESOURCE_MANAGER #define MA_NO_NODE_GRAPH #define MA_NO_ENGINE // #define MA_NO_DEVICE_IO // #define MA_NO_THREADING #define MA_NO_STDIO #define MA_NO_WAV #define MA_NO_FLAC #define MA_NO_MP3 #include <condition_variable> #include <filesystem> #include <iostream> #include <mutex> #include "load_wave.h" #include "miniaudio.h" typedef struct { const void *pcmData; size_t pcmFrames; // フレーム単位でサイズを保持 size_t currentFrame; // フレーム単位で現在位置を保持 ma_uint32 bytesPerFrame; // フレーム単位のbyte数 std::mutex mtx; std::condition_variable cv; bool running; } UserData; // とりあえず、pcmであることを前提に、ma_formatを得る ma_format get_ma_format(uint16_t bitsPerSample) { switch (bitsPerSample) { case 8: return ma_format_u8; case 16: return ma_format_s16; case 24: return ma_format_s24; case 32: return ma_format_s32; default: return ma_format_unknown; } } void stop_callback(ma_device *pDevice) { (void)pDevice; std::cout << "stop_callback called" << std::endl; } void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uint32 frameCount) { (void)pInput; UserData *userData = static_cast<UserData *>(pDevice->pUserData); size_t remainingFrames = userData->pcmFrames - userData->currentFrame; size_t framesToCopy = std::min<size_t>(remainingFrames, frameCount); if (framesToCopy > 0) { size_t bytesToCopy = framesToCopy * userData->bytesPerFrame; memcpy(pOutput, static_cast<const uint8_t *>(userData->pcmData) + userData->currentFrame * userData->bytesPerFrame, bytesToCopy); userData->currentFrame += framesToCopy; } else { // currentFrameがpcmFramesを超えることがない // つまり、コピーするデータがなければ最後までデータを渡した { std::lock_guard<std::mutex> lock(userData->mtx); userData->running = false; } userData->cv.notify_one(); } } int main(int argc, char *argv[]) { if (argc == 1) { std::cerr << "No Wave file" << std::endl; return 0; } std::filesystem::path abs_path = std::filesystem::absolute(argv[1]); std::string path_string = abs_path.string(); WavData wav_data; if (LoadWavResult::Success != loadWavFile(path_string, wav_data)) { std::cerr << "loadWavFile failed" << std::endl; return -1; } ma_context context; // The configuration of the audio device remains the same ma_device_config deviceConfig; ma_device device; UserData userData; if (ma_context_init(nullptr, 0, nullptr, &context) != MA_SUCCESS) { std::cerr << "Failed to initialize context" << std::endl; return -1; } deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = get_ma_format(wav_data.header.bitsPerSample); deviceConfig.playback.channels = wav_data.header.numChannels; deviceConfig.sampleRate = wav_data.header.sampleRate; deviceConfig.dataCallback = data_callback; deviceConfig.pUserData = &userData; deviceConfig.stopCallback = stop_callback; // 本当は不要 if (ma_device_init(&context, &deviceConfig, &device) != MA_SUCCESS) { std::cerr << "Failed to initialize playback device" << std::endl; ma_context_uninit(&context); return -1; } // 初期化時に一度だけ呼び出して取得する ma_uint32 bytesPerSample = ma_get_bytes_per_sample(device.playback.format); if (bytesPerSample == 0) { std::cerr << "Invalid format" << std::endl; ma_device_uninit(&device); ma_context_uninit(&context); return -1; } userData.pcmData = wav_data.pcmData.data(); userData.bytesPerFrame = bytesPerSample * deviceConfig.playback.channels; userData.pcmFrames = wav_data.dataSize / userData.bytesPerFrame; userData.currentFrame = 0; userData.running = true; // 再生中とする ma_device_start(&device); std::cout << "音声再生開始" << std::endl; { std::unique_lock<std::mutex> lock(userData.mtx); // ありえないが、先に通知が来た場合に対応 while (userData.running) { userData.cv.wait(lock); } } std::cout << "音声再生終了、デバイス停止" << std::endl; ma_device_stop(&device); // data_callback内で停止 ma_device_uninit(&device); ma_context_uninit(&context); return 0; }
wavファイルの読み込み処理をいまさら書いた
ある処理に問題があって、それを直すために、2025年にもなってwavファイルの読み込みコードを書いた。PCM以外のwavファイルを扱うのは確認が面倒だったので、非対応として弾くことにした。エラーチェックはそこそこ入れたけど、多分バグがある気がする。
C++で実装したのは、問題のコードがC++で書かれていたから。 std::filesystem::absolute
を使うために C++17 のコードになっている。あと、C言語からでも呼びたくなったときに修正しやすくするため、クラスにはしなかった。
wavファイル読み込み関数
#include "load_wave.h" #include <cstring> #include <fstream> LoadWavResult loadWavFile(const std::string &filename, WavData &wav) { std::ifstream file(filename, std::ios::binary); if (!file) { return LoadWavResult::FailedToOpen; } file.read(reinterpret_cast<char *>(&wav.header), sizeof(WavHeader)); if (file.eof()) { return LoadWavResult::InvalidFileSize; } // Check RIFF Chunks if (std::strncmp(wav.header.riff, "RIFF", 4) != 0) { return LoadWavResult::InvalidRiffChunk; } if (std::strncmp(wav.header.wave, "WAVE", 4) != 0) { return LoadWavResult::InvalidWaveFormat; } if (std::strncmp(wav.header.fmt, "fmt ", 4) != 0) { return LoadWavResult::InvalidFmtChunk; } // Currently only PCM support if (wav.header.audioFormat != 1) { return LoadWavResult::UnsupportedAudioFormat; } if (wav.header.fmtSize < 16 || wav.header.fmtSize % 2 != 0) { return LoadWavResult::InvalidFmtSize; } if (wav.header.fmtSize > 16) { file.seekg(wav.header.fmtSize - 16, std::ios::cur); // Skip variable part } // Find data chunks char chunkId[4]; uint32_t chunkSize; bool found = false; while (file.read(chunkId, 4)) { if (file.eof()) { return LoadWavResult::UnexpectedEndOfFile; } file.read(reinterpret_cast<char *>(&chunkSize), 4); if (file.eof()) { return LoadWavResult::UnexpectedEndOfFile; } if (std::memcmp(chunkId, "data", 4) == 0) { wav.dataSize = chunkSize; found = true; break; } // Skip to the next chunk file.seekg(chunkSize, std::ios::cur); } if (!found) { return LoadWavResult::NoDataChunk; } wav.pcmData.resize(wav.dataSize); file.read(reinterpret_cast<char *>(wav.pcmData.data()), wav.dataSize); if (file.eof()) { return LoadWavResult::InvalidDataSize; } return LoadWavResult::Success; } #endif
外部公開用のヘッダファイル
#include <cstdint> #include <string> #include <vector> #pragma pack(push, 1) struct WavHeader { char riff[4]; // "RIFF" uint32_t fileSize; // File Size char wave[4]; // "WAVE" char fmt[4]; // "fmt " uint32_t fmtSize; // Format chunk size uint16_t audioFormat; // Audio format (1 = PCM) uint16_t numChannels; // Number of channels uint32_t sampleRate; // Sample Rate uint32_t byteRate; // Byte Rate uint16_t blockAlign; // Block Arrival uint16_t bitsPerSample; // Bit Depth // Data chunks and PCM data sizes vary }; #pragma pack(pop) struct WavData { WavHeader header; uint32_t dataSize; // PCM data size std::vector<uint8_t> pcmData; }; enum class LoadWavResult { Success = 0, FailedToOpen, InvalidFileSize, InvalidDataSize, InvalidRiffChunk, InvalidWaveFormat, InvalidFmtChunk, UnsupportedAudioFormat, InvalidFmtSize, NoDataChunk, UnexpectedEndOfFile, }; LoadWavResult loadWavFile(const std::string &filename, WavData &wav);
作った関数の動作確認(pcm再生は別)
#include <filesystem> #include <iostream> // Function to display the contents of a WavHeader void printWavHeader(const WavHeader &header) { std::cout << "========== WAV Header ==========" << std::endl; std::cout << "RIFF Header : " << std::string(header.riff, 4) << std::endl; std::cout << "File Size : " << header.fileSize + 8 << " bytes" << std::endl; std::cout << "WAVE Header : " << std::string(header.wave, 4) << std::endl;; std::cout << "FMT Header : " << std::string(header.fmt, 4) << std::endl;; std::cout << "FMT Chunk Size : " << header.fmtSize << " bytes" << std::endl; std::cout << "Audio Format : " << header.audioFormat << " (1 = PCM)" << std::endl; std::cout << "Channels : " << header.numChannels << std::endl; std::cout << "Sample Rate : " << header.sampleRate << " Hz" << std::endl; std::cout << "Byte Rate : " << header.byteRate << " bytes/sec" << std::endl; std::cout << "Block Align : " << header.blockAlign << " bytes" << std::endl; std::cout << "Bits Per Sample : " << header.bitsPerSample << " bits" << std::endl; std::cout << "================================" << std::endl; } int main(int argc, char *argv[]) { if (argc > 1) { std::filesystem::path abs_path = std::filesystem::absolute(argv[1]); std::string path_string = abs_path.string(); std::cout << path_string << std::endl; WavData wav_data; loadWavFile(path_string, wav_data); printWavHeader(wav_data.header); std::cout << "Data Size : " << wav_data.dataSize << " bytes" << std::endl; } else { std::cout << "No file" << std::endl; } return 0; }
US配列キーボード購入
約1年、JP109キーボードにUS配列を設定して使い続けたので、「そろそろ本格的にUS配列キーボードを使ってみよう」と思い、約25年ぶりに新たなUS配列キーボードを導入した。
んで、今回購入したのは Realforce R3S Keyboard / R3SD13 です。
US配列、テンキーレス、30g荷重という条件を選んだ時点で1つに決まってしまった。R3 Keyboardは、Bluetooth接続をほぼ使わないので、追加の1万円の出費を避けることになった。
Realforce 89U以来の2台目Realforceなんだけど、まず最初にディップスイッチがないんだと驚いた。よくわからなかったのでRealfoceのサイトのマニュアルを読んでREALFORCE CONNECTをインストールしてファームウェアも更新した。
4年ぶりのRealforceに触れた感想としては、打鍵感がかなり軽い(スコスコ)と感じたが、すぐに慣れた。久しぶりに本物のUS配列なので、キー配置にも少し時間が必要だった。
日本語入力の基本方針
US配列で日本語入力する際は、左右のALTキーを単独で押した場合のみ変換(右ALT)・無変換(左ALT)キーとして機能させる方法を採用した。
Windows11のUS配列対応
キーボード設定を英語キーボード(101/102キー) に変更して再起動した後、
取り急ぎalt-ime-ahkを導入した。
普段コーディングを行っていないので、この設定で特に不自由は感じない。
Linux(X11)のUS配列対応
Waylandには移行していないので、X11向けの設定です。もともとキーボード設定をau(PC105)にしていたので、 xremap
の設定変更だけで完了した。
modmap: - name: SandS remap: Space: held: Shift_L alone: Space alone_timeout_millis: 400 application: not : [URxvt,/steam/] - name: AltIME remap: KEY_LEFTALT: held: LEFTALT alone: KEY_MUHENKAN alone_timeout_millis: 400 KEY_RIGHTALT: held: RIGHTALT alone: KEY_HENKAN alone_timeout_millis: 400 keymap: - name: Huge Track Ball # Left button: BTN_LEFT # Right button: BTN_RIGHT # Tilt Wheel: BTN_MIDDLE # Tilt left: unknown key 59987/59999 # Tilt Right: unknown key 59986/59998 # Forward button: BTN_EXTRA # Back button: BTN_SIDE # Fn1: BTN_FORWARD # Fn2: BTN_BACK # Fn3: BTN_TASK remap: BTN_TASK: CTRL-W device: only: ['ELECOM TrackBall Mouse HUGE TrackBall'] - name: Keyboard Volume Control remap: KEY_VOLUMEUP: launch: ["wpctl", "set-volume", "-l", "1.0", "@DEFAULT_AUDIO_SINK@", "0.1+"] KEY_VOLUMEDOWN: launch: ["wpctl", "set-volume", "-l", "1.0", "@DEFAULT_AUDIO_SINK@", "0.1-"] KEY_MUTE: launch: ["wpctl","set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"] - name: Caps2BS remap: CapsLock: KEY_BACKSPACE Shift-CapsLock: CapsLock
DvorakY改の見直し
日本語入力では、Mozcのローマ字テーブルをDvorakY改に設定しているため、特に見直しは不要だった。
ネット上ではDvorakYを常用している人はほとんどおらず、DvorakJPがデファクトスタンダードに近い状況です。DvorakYについて言及があるのが、https://ikatakos.com/pot/keyboard_layout/miscellaneous/20130128ぐらいしか残っておらずさみしい限り。個人的にはDvorakJPは極端すぎるので、そこまでするなら「かな入力」の練習をしたほうがいいと感じてる。
DvorakY改は上記サイトに記載されている欠点を持つこと対して、下記の変更を行っている。
- パ行
- (Dvorak配列の)「P」ではなく「L」に割り当て(QWERTYの「P」と位置が一致)
- 小書き文字(ゃゅょぁぃぅぇゎ)
- 「L」をパ行に使ってしまったので、「\」に割り当て(※使用頻度が低いため)
- 「'」キー
- (Dvorak配列の)「'」に「ん」を割り当て(AZIKの影響)
- 「P」キー
- (Dvorak配列の)「P」に「っ」を割り当て(AZIKの影響)
