https://github.com/svtlabs/screencapturekit-rs/pull/13 pull-request を出しただけで、まだマージされてないんだけど。
最近のMacでは、ScreenCaptureKit というのがあって、任意のウィンドウの画面をキャプチャしたり、音声をキャプチャしたりできる。昔は Mac では BlackHole とか SoundFlower とか Loopback とかを入れないと、アプリの音声をキャプチャすることはできなかったのだが、これを使うと、簡単にキャプチャできるし、リアルタイムでの音声ミックスを BlackHole とかでやってると coreaudiod とかでCPU食いまくるってのもあるんで、そういうのを避けたいというのもある。
実際、ScreenCaptureKit 使うと CPU 負荷めっちゃ低くて良い。OBS でも ScreenCaptureKit を処理することで CPU 負荷を減らしたという話がある。 https://applech2.com/archives/20220613-obs-studio-for-mac-next-level-with-macos-13-ventura-screencapturekit.html
ScreenCaptureKit は mac の機能なので、これを使ったアプリを作るなら、 Swift を使うのが王道だとは思いつつ、Swift に慣れてないのと、XCode に慣れてないので、可能なら Rust とかでやりたいなと思っていると、そのものずばりの crate が存在している。 https://crates.io/crates/screencapturekit
よしこれで、音声ファイルをキャプチャするぞ、と思ったのだが、音声を取得する機能がそもそも実装されていないことに気づいた。
(この時点で、xcode で書く方向性も今一度考えたが、ちょっと試してみたらうまく動かなくて諦めたのであった。。結局、XCode で Swift で書いてても、権限周りとかではまるのと、XCode に慣れてなさすぎることと、そもそも rust だろうが swift だろうが、ScreenCaptureKit は新参なので情報が薄いというのもあって、RustRover で Rust で頑張ったほうが楽という結論に。。)
ScreenCaptureKit で、CMSamleBuffer に音声データが取れるので、これをそのまま raw PCM として保存したら、wave に変換して音声ファイルになるやろ、っておもってやってみたら全然そうならなかった。なんかノイズがすごい、ちょっと高い音のデータができた。このバイナリデータは、AudioBufferList という構造体になっていて、サイズ情報とかが混じってるので、ちゃんと AudioBufferList としてデシリアライズしてやらないと使えない。 AudioBufferList には、チャネルごとにバイナリデータが入ってるので、これをうまいこと取り出せば使える。 https://developer.apple.com/documentation/coremedia/cmsamplebuffer-u71
let mut buffer_size = 0;
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
self,
&mut buffer_size,
null_mut(),
0,
null_mut(),
null_mut(),
0,
&mut null_mut(),
);
とかして、必要なバッファサイズを計測する。
let mut block_buffer_ref = CMSampleBufferGetDataBuffer(self);
let layout = alloc::Layout::from_size_align(buffer_size, 16).unwrap();
let audio_buffer_list_ptr = alloc::alloc(layout);
let result = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
self,
null_mut(),
audio_buffer_list_ptr as _,
buffer_size,
null_mut(),
null_mut(),
kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
&mut block_buffer_ref,
);
CFRelease(block_buffer_ref as _);
if result != 0 {
panic!()
}
let audio_buffer_list_ptr = audio_buffer_list_ptr as *mut AudioBufferList;
あとは、こんなふうにして、CMSmpleBufferGetDataBuffer で、データバッファを取り出して、maloc した領域にデータを取り出す。
let audio_buffer_list = unsafe { *audio_buffer_list_ptr };
let number_buffers = audio_buffer_list.number_buffers;
let mut buffers = Vec::new();
for i in 0..audio_buffer_list.number_buffers {
let audio_buffer = audio_buffer_list.buffers[i as usize];
buffers.push(CopiedAudioBuffer {
number_channels: number_buffers,
data: unsafe {
std::slice::from_raw_parts(audio_buffer.data, audio_buffer.data_bytes_size as usize)
}.to_vec(),
});
}
buffers
データをコピーするとちょっとオーバーヘッドあるけど、、そんなにシビアな用途でもないので、コピーしちゃう。
あとは、この PCM データをチャネルごとに、データを普通にファイルに書いていけばいい。 複数チャネルを一気にファイルに書いていくのは、今回の僕の用途では必要なかったので、、、raw PCM をゴリゴリ書いていき、sox コマンドで、wave ヘッダをつける。 このへん、rust のライブラリもありそうだが、結局 sox コマンド使ったほうが情報多くて楽そうだった。メモリたくさん使う、オーディオ関連の処理は、別プロセスでやったほうが、メモリリークの心配とかがなくて楽というのもある。
そんなこんなで、raw pcm の取得部分まで実装し、サンプルコードを書いて pull-request を出した。 ScreenCaptureKit には夢が詰まってる気がしていて、ScreenCaptureKit で適当に音声をキャプチャした上で、whisper.cpp 使って音声認識するとかすると結構遊べるなと思っている。 初心者にも書きやすい rust でかければ、誰でも簡単に ScreenCaptureKit で遊べるので、オススメ。