From 3e39fcb30f03c32746e8c2908633f06e79e5cf89 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 5 May 2026 23:06:54 +0100 Subject: [PATCH] fix(ios): configure AVAudioSession before engine init to prevent buffer mismatch iOS defaults to AVAudioSessionCategorySoloAmbient with a system-chosen buffer size (~4096 frames on device). Elementary's runtime is initialized with a fixed block size of 512, causing a mismatch that produces garbage audio (audible as a low-frequency square wave) immediately on app launch. Fix: set AVAudioSessionCategoryPlayback with MixWithOthers and AllowBluetoothA2DP, request a 512-frame preferred buffer duration, activate the session before creating AVAudioEngine, then derive the actual block size from session.IOBufferDuration so the runtime always matches what the engine delivers per callback. --- ios/Elementary.mm | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/ios/Elementary.mm b/ios/Elementary.mm index 6a2b501..0de3332 100644 --- a/ios/Elementary.mm +++ b/ios/Elementary.mm @@ -20,6 +20,27 @@ - (instancetype)init _sharedInstance = self; self.loadedResources = [[NSMutableSet alloc] init]; + // Configure session before engine: iOS default (SoloAmbient, ~4096-frame buffers) + // mismatches Elementary's block size and produces garbage audio on launch. + NSError *sessionError; + AVAudioSession *session = [AVAudioSession sharedInstance]; + [session setCategory:AVAudioSessionCategoryPlayback + mode:AVAudioSessionModeDefault + options:AVAudioSessionCategoryOptionMixWithOthers | + AVAudioSessionCategoryOptionAllowBluetoothA2DP + error:&sessionError]; + if (sessionError) { + NSLog(@"[Elementary] Failed to set audio session category: %@", sessionError); + } + [session setPreferredIOBufferDuration:(512.0 / 48000.0) error:&sessionError]; + if (sessionError) { + NSLog(@"[Elementary] Failed to set preferred IO buffer duration: %@", sessionError); + } + [session setActive:YES error:&sessionError]; + if (sessionError) { + NSLog(@"[Elementary] Failed to activate audio session: %@", sessionError); + } + self.audioEngine = [[AVAudioEngine alloc] init]; AVAudioFormat *outputFormat = [self.audioEngine.outputNode outputFormatForBus:0]; @@ -30,7 +51,8 @@ - (instancetype)init const float **inputBuffer = (const float **)calloc(numOutputChannels, sizeof(float *)); float **outputBuffer = (float **)malloc(numOutputChannels * sizeof(float *)); - NSLog(@"[Elementary] Init: %d output channels, sampleRate=%.0f", numOutputChannels, outputFormat.sampleRate); + NSLog(@"[Elementary] Init: %d output channels, sampleRate=%.0f, IOBufferDuration=%.4fs", + numOutputChannels, outputFormat.sampleRate, session.IOBufferDuration); AVAudioSourceNode *sourceNode = [[AVAudioSourceNode alloc] initWithRenderBlock:^OSStatus( BOOL * _Nonnull isSilence, @@ -82,7 +104,10 @@ - (instancetype)init return nil; } - int bufferSize = 512; + // Use granted IOBufferDuration — iOS may not honor the preferred value exactly. + int bufferSize = (int)round(outputFormat.sampleRate * session.IOBufferDuration); + if (bufferSize <= 0 || bufferSize > 4096) bufferSize = 512; // safety fallback + NSLog(@"[Elementary] Runtime block size: %d frames", bufferSize); self.runtime = std::make_shared>(outputFormat.sampleRate, bufferSize); [[NSNotificationCenter defaultCenter] addObserver:self