Repository navigation
perf(recording): encode macOS studio audio with AudioToolbox AAC - #2626
Merged
richiemcilroy merged 4 commits intoOct 18, 2026
Merged
Conversation
FFmpeg's built-in AAC encoder costs about 7.5x the CPU of AudioToolbox for the same output bitrate. Fragmented studio audio now uses aac_at on macOS, trims the extra 1088 samples of encoder priming and the padded final frame so every sample decodes where it did before, and falls back to FFmpeg's encoder if AudioToolbox fails to open. CAP_RECORDING_FFMPEG_AAC=1 forces the old encoder.
Contributor
|
@hridaya423 is attempting to deploy a commit to the Cap Software Inc Team on Vercel. A member of the Team first needs to authorize it. |
… the opened encoder
Contributor
Author
|
@greptileai review |
Contributor
Author
|
@greptileai review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Referred from this post by @cap.
Summary
On macOS, studio recordings encode the microphone and system audio tracks with FFmpeg's built-in
aacencoder. In a profile of the GPUI app during a studio recording, that encoder was the largest unclaimed CPU cost in the recording engine. AudioToolbox's AAC encoder (aac_at, already in the bundled FFmpeg) does the same job for about 7.5x less CPU at the same output bitrate.This PR makes
FragmentedAudioFileuseaac_aton macOS. The decoded audio timeline stays sample-identical to the current output, so A/V sync does not change.Benchmarks
Encoder cost
cargo run --release -p cap-enc-ffmpeg --example recording-audio-benchmark -- <input>encodes the same input throughFragmentedAudioFilewith each encoder. Rounds alternate between the encoders. The table shows the median of 5 rounds. CPU is process user+sys time fromgetrusage. M4 Pro, 48 kHz input.75 s of speech:
aacaac_at60 s synthetic tone and noise (the default input without an argument):
aacaac_atIn the GPUI app
I ran the release
cap-gpuibinary with an isolated profile andCAP_GPUI_AUTO_RECORD=studio:…, recording with the microphone and system audio on. One build covers both cases.CAP_RECORDING_FFMPEG_AAC=1selects the old encoder./usr/bin/sampleran for 45 s in the middle of a 120 s recording. Samples insideFragmentedAudioMuxer::send_audio_framefell from 5,684 to 403. In the AudioToolbox run, the encoder itself (AudioConverterFillComplexBuffer) took 364 samples. In the FFmpeg run,avcodec_send_frametook 3,229.On this macOS build, system audio loopback captured only silence in both variants. Silence costs almost nothing with either encoder. The in-app saving therefore comes from the microphone track. With non-silent system audio, the stereo row above also applies.
What changed
All changes are in
crates/enc-ffmpeg.AACEncoder::init_for_recording. On macOS, this opensaac_atwithaac_at_mode=abr, at 192 kbps for mono and 256 kbps for stereo. These targets match what the current encoder actually writes. ABR keeps silence small; CBR would fill silence up to the full bitrate. Ifaac_atfails to open, the function logs a warning and usesAACEncoder::init.CAP_RECORDING_FFMPEG_AAC=1forces the old path. On other platforms, the function callsAACEncoder::init.AACEncoder::initis unchanged for every other caller (camera, DASH, segmented, Windows, export). It now delegates to a sharedinit_with_codec. That function selects the sample format and sample rate from what the codec supports, becauseaac_atreports no supported-rate list.aacprimes 1024 samples andaac_atprimes 2112. A direct switch would move all recorded audio about 22.7 ms later.AudioEncoderBasenow dropsinitial_padding - 1024samples (1088 foraac_at) before they reach the encoder. The value comes from the opened encoder, not a constant.aac_ataccepts only full frames, so libavcodec pads the last frame and the packet durations include the padding. AnEndTrimmedSinkdrops or shortens packets after the last input sample. This applies only to codecs withoutSMALL_LAST_FRAMEorVARIABLE_FRAME_SIZE, so FFmpeg'saacoutput does not change.FragmentedAudioFile::initusesinit_for_recording. A#[doc(hidden)] init_with_encoder_preferencelets tests and the benchmark select an encoder without environment variables.Correctness
New tests in
mux/fragmented_audio.rsencode through both encoders and decode the result:24_000 + 1024with both encoders, in mono and in stereo.output_pipeline/core.rs). A test copies that input shape: two clicks around a 1.5 s silence fill, with irregular frame sizes and the same nanosecond timestamp rounding. Both clicks decode at the same sample with both encoders. The decoded length and track end also match.FragmentedAudioFile::initselectsaac_at.The input that this PR does not cover is a raw timestamp jump that reaches the encoder without a silence fill. With such input, audio after the jump can decode up to one frame away from the FFmpeg position, because the leading trim moves the frame grid. The presentation timestamps still match. The recording pipeline does not produce this input today.
Trade-offs
audio/aac.rsandaudio/base.rs. I used the sameinit_with_codecandEndTrimmedSinknames so that the second PR to merge needs only a small rebase.Verification
cargo test -p cap-enc-ffmpeg: 157 passed.cargo clippy -p cap-enc-ffmpeg --all-targets -- -D warnings: clean.cargo check -p cap-recording: OK.audio-input.m4aandsystem_audio.m4afiles with matching durations, and the editor opened them.

Confidence Score: 5/5
The PR appears safe to merge, with no outstanding findings.Summary
The PR switches macOS studio audio to AudioToolbox AAC, with FFmpeg AAC as a fallback.
Reviews (3) · Last reviewed commit: "chore(enc-ffmpeg): drop descriptive head..." · Reviewed by Greptile