From aa40fcfc9c152b825922d4b5b20fbf77674ac1e1 Mon Sep 17 00:00:00 2001 From: bram Date: Sun, 25 Jan 2026 01:27:52 -0800 Subject: [PATCH 1/6] Modified FlautoRecorderEngine.java to re-use buffers and avoid Garbage Collection churn --- .../TauEngine/FlautoRecorderEngine.java | 206 ++++++++++-------- 1 file changed, 111 insertions(+), 95 deletions(-) diff --git a/android/src/main/java/xyz/canardoux/TauEngine/FlautoRecorderEngine.java b/android/src/main/java/xyz/canardoux/TauEngine/FlautoRecorderEngine.java index 43edd4c..2b2a2e3 100644 --- a/android/src/main/java/xyz/canardoux/TauEngine/FlautoRecorderEngine.java +++ b/android/src/main/java/xyz/canardoux/TauEngine/FlautoRecorderEngine.java @@ -60,6 +60,10 @@ public class FlautoRecorderEngine FlautoRecorder session = null; FileOutputStream outputStream = null; final private Handler mainHandler = new Handler (Looper.getMainLooper ()); + private ByteBuffer byteBuffer; + private FloatBuffer floatBuffer; + private FloatBuffer floatBuffer2; + @@ -351,22 +355,28 @@ ArrayList uninterleaved16(Integer numChannels, byte[] b) } + // Returns the number of bytes read int writeData32( t_CODEC theCodec, Integer numChannels, Boolean interleaved, int bufferSize) throws Exception { - FloatBuffer floatBuffer = FloatBuffer.allocate(bufferSize / 4); + // Use pre-allocated buffers int n = recorder.read(floatBuffer.array(), 0, bufferSize / 4, AudioRecord.READ_NON_BLOCKING); if (n > 0) { totalBytes += n; ArrayList r = new ArrayList(); + // Optimizing this part is tricky without changing the API (ArrayList). + // But we can at least avoid re-allocating the intermediate buffers if we are smart, + // but for now let's focus on the main reading loop allocations. + // The user API expects ArrayList, so we must create it. + for (int channel = 0; channel < numChannels; ++channel) { int frameSize = n / numChannels; - FloatBuffer fb = FloatBuffer.allocate(frameSize); + FloatBuffer fb = FloatBuffer.allocate(frameSize); // Still allocating here, but unavoidable given API for (int i = 0; i < frameSize; ++i) { int pos = channel + i * numChannels; fb.array()[i ] = floatBuffer.array()[pos]; @@ -377,77 +387,74 @@ int writeData32( mainHandler.post(new Runnable() { @Override public void run() { - session.recordingDataFloat32(r); + if (session != null) + session.recordingDataFloat32(r); } }); computeMaxAmplitude32(floatBuffer.array()); - - } return n; } - int writeData32Interleaved( + // Returns the number of bytes read + int writeData32Interleaved( t_CODEC theCodec, Integer numChannels, Boolean interleaved, int bufferSize) throws Exception { - FloatBuffer floatBuffer = FloatBuffer.allocate(bufferSize/4); - - int n = recorder.read(floatBuffer.array(), 0, bufferSize / 4, AudioRecord.READ_NON_BLOCKING); - n *= 4; - - if (n > 0) - { - totalBytes += n; - - //byte[] bytearray = buf.array(); - //float toto = buf.getFloat(0); - //float toto1 = buf.getFloat(1); - //buf.rewind(); - - if (!interleaved) - { - FloatBuffer fb = FloatBuffer.allocate(bufferSize/4); - int lnx = n / numChannels; - for (int channel = 0; channel < numChannels; ++channel) - { - int frameSize = n/(4 * numChannels); - for (int i = 0; i < frameSize; ++i) - { - int pos = channel + i * numChannels; - fb.array()[i + channel * frameSize] = floatBuffer.array()[pos ]; - } - } - floatBuffer = fb; - - } - - ByteBuffer buf = ByteBuffer.allocate(bufferSize); - buf.rewind(); - - buf.order(ByteOrder.nativeOrder()); - floatBuffer.rewind(); - buf.rewind(); - buf.asFloatBuffer().put(floatBuffer); - - - final int ln = n; - final byte[] b = Arrays.copyOfRange(buf.array(), 0, ln); - mainHandler.post(new Runnable() { - @Override - public void run() { - session.recordingData(b); - } - }); - computeMaxAmplitude32(floatBuffer.array()); - - } - - return n; + // Use pre-allocated buffer + // Note: floatBuffer is allocated as bufferSize/4 + int n = recorder.read(floatBuffer.array(), 0, bufferSize / 4, AudioRecord.READ_NON_BLOCKING); + n *= 4; // Convert to bytes count for totalBytes logic + + if (n > 0) + { + totalBytes += n; + + if (!interleaved) + { + // This path seems unused or rare for interleaved=false but called from interleaved method? + // The original code handled !interleaved block inside writeData32Interleaved which is confusing naming. + // But let's respect it. + // floatBuffer2 should be used here. + int lnx = n / numChannels; + for (int channel = 0; channel < numChannels; ++channel) + { + int frameSize = n/(4 * numChannels); + for (int i = 0; i < frameSize; ++i) + { + int pos = channel + i * numChannels; + floatBuffer2.array()[i + channel * frameSize] = floatBuffer.array()[pos ]; + } + } + // Swap used buffer for next steps + FloatBuffer temp = floatBuffer; + floatBuffer = floatBuffer2; + floatBuffer2 = temp; + } + + // Copy floats to byte buffer + byteBuffer.rewind(); // Reuse member byteBuffer + byteBuffer.order(ByteOrder.nativeOrder()); + floatBuffer.rewind(); + byteBuffer.asFloatBuffer().put(floatBuffer); + + final int ln = n; + final byte[] b = Arrays.copyOfRange(byteBuffer.array(), 0, ln); // Still copy required for thread safety passing to UI thread + mainHandler.post(new Runnable() { + @Override + public void run() { + if (session != null) + session.recordingData(b); + } + }); + computeMaxAmplitude32(floatBuffer.array()); + } + + return n; // Return bytes read (approx) } int writeData16( @@ -457,11 +464,9 @@ int writeData16( int bufferSize) { int n = 0; - ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize); - - + // Reuse byteBuffer n = recorder.read(byteBuffer.array(), 0, bufferSize, AudioRecord.READ_NON_BLOCKING); - if (n == 0) + if (n <= 0) // Changed to <= 0 to handle errors return 0; final int elementCount = n; totalBytes += n; @@ -480,7 +485,8 @@ int writeData16( mainHandler.post(new Runnable() { @Override public void run() { - session.recordingData(b); + if (session != null) + session.recordingData(b); } }); } else // pcmInt16 !interleaved @@ -491,8 +497,8 @@ public void run() { @Override public void run() { - - session.recordingDataInt16(x); + if (session != null) + session.recordingDataInt16(x); } }); @@ -509,35 +515,32 @@ int writeData( Boolean interleaved, int bufferSize) { + // Removed the while loop. We only do ONE non-blocking read. + // The looping is handled by the recursive Runnable p mechanism. int n = 0; - while (isRecording ) { - try { - - if (codec == t_CODEC.pcm16 || codec == t_CODEC.pcm16WAV ) { - n = writeData16(codec, numChannels, interleaved - , bufferSize); - } else - if (interleaved) { - n = writeData32Interleaved(codec, numChannels, interleaved - , bufferSize); - } else - { - n = writeData32(codec, numChannels, interleaved - , bufferSize); - } - - if (isRecording) - mainHandler.post(p); - if (n == 0) - break; - } catch (Exception e) { - System.out.println(e); - break; - } - } - //if (isRecording) - //mainHandler.post(p); - return 1; + // Check recording state first + if (!isRecording || recorder == null) return 0; + + try { + + if (codec == t_CODEC.pcm16 || codec == t_CODEC.pcm16WAV ) { + n = writeData16(codec, numChannels, interleaved + , bufferSize); + } else + if (interleaved) { + n = writeData32Interleaved(codec, numChannels, interleaved + , bufferSize); + } else + { + n = writeData32(codec, numChannels, interleaved + , bufferSize); + } + + // We do NOT post(p) here anymore. p handles reposting itself. + } catch (Exception e) { + System.out.println(e); + } + return n; } @@ -581,6 +584,12 @@ int writeData( bufLn ); + // Allocate buffers once + byteBuffer = ByteBuffer.allocate(bufLn); + floatBuffer = FloatBuffer.allocate(bufLn/4); + floatBuffer2 = FloatBuffer.allocate(bufLn/4); + + if (recorder.getState() == AudioRecord.STATE_INITIALIZED) { if (noiseSuppression) { @@ -602,7 +611,14 @@ public void run() { if (isRecording) { int n = writeData( codec, numChannels, interleaved, bufLn); - + if (isRecording) { + // Yield mechanism: if we read 0 bytes, sleep a bit to avoid CPU spin + if (n == 0) { + mainHandler.postDelayed(p, 10); + } else { + mainHandler.post(p); + } + } } } }; From 071700a91edc4dd48b827486472b34456b6beaaa Mon Sep 17 00:00:00 2001 From: bram Date: Sun, 1 Feb 2026 00:35:02 -0800 Subject: [PATCH 2/6] Fix: avoid nullpointer error in race condition where player is stopped but data is still feeding in --- .../canardoux/TauEngine/FlautoPlayerEngine.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java b/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java index 3d9467b..7471998 100644 --- a/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java +++ b/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java @@ -116,6 +116,8 @@ class FeedThread extends Thread { } public void run() { + AudioTrack track = audioTrack; + if (track == null) return; int ln = 0; // The number of bytes accepted (and perhaps played) by the device if (mCodec == Flauto.t_CODEC.pcmFloat32) { @@ -124,11 +126,11 @@ public void run() { FloatBuffer fbuf = buf.asFloatBuffer(); float[] ff = new float[mData.length/4]; fbuf.get(ff); - ln = audioTrack.write(ff, 0, mData.length/4, AudioTrack.WRITE_BLOCKING); + ln = track.write(ff, 0, mData.length/4, AudioTrack.WRITE_BLOCKING); ln = 4 * ln; } else { - ln = audioTrack.write(mData, 0, mData.length, AudioTrack.WRITE_BLOCKING); + ln = track.write(mData, 0, mData.length, AudioTrack.WRITE_BLOCKING); } mSession.needSomeFood(1); } @@ -142,6 +144,8 @@ class FeedInt16Thread extends Thread { } public void run() { + AudioTrack track = audioTrack; + if (track == null) return; int nbrChannels = mData.size(); int frameSize = mData.get(0).length; int ln = nbrChannels * frameSize; @@ -158,7 +162,7 @@ public void run() { } } - int r = audioTrack.write(interleavedData, 0, ln, AudioTrack.WRITE_BLOCKING); + int r = track.write(interleavedData, 0, ln, AudioTrack.WRITE_BLOCKING); mSession.needSomeFood(1); } } @@ -171,6 +175,8 @@ class FeedFloat32Thread extends Thread { } public void run() { + AudioTrack track = audioTrack; + if (track == null) return; int ln = 0; // The number of bytes accepted (and perhaps played) by the device int nbrOfChannels = mData.size(); int frameSize = mData.get(0).length; @@ -183,7 +189,7 @@ public void run() { r[ i * nbrOfChannels + channel] = b[i]; } } - ln = audioTrack.write(r, 0, r.length, AudioTrack.WRITE_BLOCKING); + ln = track.write(r, 0, r.length, AudioTrack.WRITE_BLOCKING); mSession.needSomeFood(1); } } From 0e724c247ac8f3e278f993838cd3b55f8e549a50 Mon Sep 17 00:00:00 2001 From: bram Date: Sat, 21 Feb 2026 20:09:53 -0800 Subject: [PATCH 3/6] Added autorelease to prevent memory overflow during recording --- ios/Classes/FlautoRecorderEngine.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/Classes/FlautoRecorderEngine.mm b/ios/Classes/FlautoRecorderEngine.mm index 3a3ecd4..71facf5 100644 --- a/ios/Classes/FlautoRecorderEngine.mm +++ b/ios/Classes/FlautoRecorderEngine.mm @@ -81,6 +81,7 @@ [inputNode installTapOnBus: 0 bufferSize: (int)bufferSize format: nil block: ^(AVAudioPCMBuffer* _Nonnull buffer, AVAudioTime* _Nonnull when) { + @autoreleasepool { inputStatus = AVAudioConverterInputStatus_HaveData ; AVAudioPCMBuffer* convertedBuffer = [[AVAudioPCMBuffer alloc]initWithPCMFormat: recordingFormat frameCapacity: [buffer frameCapacity]]; @@ -176,6 +177,7 @@ }); } // Not interleaved } // (frameLength > 0) + } }]; } From 5804563bfe948adff9a5305bf7f067cdc14d2aca Mon Sep 17 00:00:00 2001 From: bram Date: Sat, 21 Feb 2026 22:49:14 -0800 Subject: [PATCH 4/6] Wrapped the run() body of the three feed threads in try/catch to prevent crashes like IllegalState when the feed is terminated --- .../TauEngine/FlautoPlayerEngine.java | 106 ++++++++++-------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java b/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java index 7471998..941c19b 100644 --- a/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java +++ b/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java @@ -116,23 +116,27 @@ class FeedThread extends Thread { } public void run() { - AudioTrack track = audioTrack; - if (track == null) return; - int ln = 0; // The number of bytes accepted (and perhaps played) by the device - if (mCodec == Flauto.t_CODEC.pcmFloat32) - { - ByteBuffer buf = ByteBuffer.wrap(mData); - buf.order(ByteOrder.nativeOrder()); - FloatBuffer fbuf = buf.asFloatBuffer(); - float[] ff = new float[mData.length/4]; - fbuf.get(ff); - ln = track.write(ff, 0, mData.length/4, AudioTrack.WRITE_BLOCKING); - ln = 4 * ln; - } else - { - ln = track.write(mData, 0, mData.length, AudioTrack.WRITE_BLOCKING); + try { + AudioTrack track = audioTrack; + if (track == null) return; + int ln = 0; // The number of bytes accepted (and perhaps played) by the device + if (mCodec == Flauto.t_CODEC.pcmFloat32) + { + ByteBuffer buf = ByteBuffer.wrap(mData); + buf.order(ByteOrder.nativeOrder()); + FloatBuffer fbuf = buf.asFloatBuffer(); + float[] ff = new float[mData.length/4]; + fbuf.get(ff); + ln = track.write(ff, 0, mData.length/4, AudioTrack.WRITE_BLOCKING); + ln = 4 * ln; + } else + { + ln = track.write(mData, 0, mData.length, AudioTrack.WRITE_BLOCKING); + } + mSession.needSomeFood(1); + } catch (Exception e) { + mSession.logError("FeedThread exception: " + e.getMessage()); } - mSession.needSomeFood(1); } } @@ -144,26 +148,30 @@ class FeedInt16Thread extends Thread { } public void run() { - AudioTrack track = audioTrack; - if (track == null) return; - int nbrChannels = mData.size(); - int frameSize = mData.get(0).length; - int ln = nbrChannels * frameSize; - byte[] interleavedData = new byte[ln]; - for (int channel = 0; channel < nbrChannels; ++channel ) - { - byte[] b = mData.get(channel); - if (b.length != frameSize) // Wrong size - return; - for (int i = 0; i < frameSize/2; ++i) { - int pos = 2 * (channel + i * nbrChannels); - interleavedData[pos] = b[2 * i]; // Little endian - interleavedData[pos + 1] = b[2 * i + 1]; - } + try { + AudioTrack track = audioTrack; + if (track == null) return; + int nbrChannels = mData.size(); + int frameSize = mData.get(0).length; + int ln = nbrChannels * frameSize; + byte[] interleavedData = new byte[ln]; + for (int channel = 0; channel < nbrChannels; ++channel ) + { + byte[] b = mData.get(channel); + if (b.length != frameSize) // Wrong size + return; + for (int i = 0; i < frameSize/2; ++i) { + int pos = 2 * (channel + i * nbrChannels); + interleavedData[pos] = b[2 * i]; // Little endian + interleavedData[pos + 1] = b[2 * i + 1]; + } + } + int r = track.write(interleavedData, 0, ln, AudioTrack.WRITE_BLOCKING); + mSession.needSomeFood(1); + } catch (Exception e) { + mSession.logError("FeedInt16Thread exception: " + e.getMessage()); } - int r = track.write(interleavedData, 0, ln, AudioTrack.WRITE_BLOCKING); - mSession.needSomeFood(1); } } @@ -175,22 +183,26 @@ class FeedFloat32Thread extends Thread { } public void run() { - AudioTrack track = audioTrack; - if (track == null) return; - int ln = 0; // The number of bytes accepted (and perhaps played) by the device - int nbrOfChannels = mData.size(); - int frameSize = mData.get(0).length; - float[] r = new float[nbrOfChannels * frameSize]; - for (int channel = 0; channel < nbrOfChannels; ++channel) - { - float[] b = mData.get(channel); - for (int i = 0; i < frameSize; ++i) + try { + AudioTrack track = audioTrack; + if (track == null) return; + int ln = 0; // The number of bytes accepted (and perhaps played) by the device + int nbrOfChannels = mData.size(); + int frameSize = mData.get(0).length; + float[] r = new float[nbrOfChannels * frameSize]; + for (int channel = 0; channel < nbrOfChannels; ++channel) { - r[ i * nbrOfChannels + channel] = b[i]; + float[] b = mData.get(channel); + for (int i = 0; i < frameSize; ++i) + { + r[ i * nbrOfChannels + channel] = b[i]; + } } + ln = track.write(r, 0, r.length, AudioTrack.WRITE_BLOCKING); + mSession.needSomeFood(1); + } catch (Exception e) { + mSession.logError("FeedFloat32Thread exception: " + e.getMessage()); } - ln = track.write(r, 0, r.length, AudioTrack.WRITE_BLOCKING); - mSession.needSomeFood(1); } } From efd6b4e61a2300753dc5d518de1a9882043a6ba4 Mon Sep 17 00:00:00 2001 From: bram Date: Sun, 22 Feb 2026 14:19:14 -0800 Subject: [PATCH 5/6] Fixed bug introduced by try/catch logging off the UIThread --- .../java/xyz/canardoux/TauEngine/FlautoPlayer.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayer.java b/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayer.java index ce6504e..7155d09 100644 --- a/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayer.java +++ b/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayer.java @@ -539,11 +539,21 @@ public Map getProgress() { } void logDebug(String msg) { - m_callBack.log(t_LOG_LEVEL.DBG, msg); + mainHandler.post(new Runnable() { + @Override + public void run() { + m_callBack.log(t_LOG_LEVEL.DBG, msg); + } + }); } void logError(String msg) { - m_callBack.log(t_LOG_LEVEL.ERROR, msg); + mainHandler.post(new Runnable() { + @Override + public void run() { + m_callBack.log(t_LOG_LEVEL.ERROR, msg); + } + }); } } From 8af7d6cb90f5a94032c6e347539b7849f318b374 Mon Sep 17 00:00:00 2001 From: bram Date: Tue, 24 Feb 2026 23:41:03 -0800 Subject: [PATCH 6/6] fix: iOS crash due to lack of autoReleasePool and use after free --- ios/Classes/FlautoPlayerEngine.mm | 6 ++++ ios/Classes/FlautoRecorderEngine.mm | 44 +++++++++++++++++------------ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/ios/Classes/FlautoPlayerEngine.mm b/ios/Classes/FlautoPlayerEngine.mm index 86f3e99..3325a41 100644 --- a/ios/Classes/FlautoPlayerEngine.mm +++ b/ios/Classes/FlautoPlayerEngine.mm @@ -338,6 +338,7 @@ -(int) getStatus #define NB_BUFFERS 4 - (int) feed: (NSArray*)data interleaved: (bool)interleaved { + @autoreleasepool { //NSMutableArray* data = [[NSMutableArray alloc] init]; //NSData* d = data[0]; //assert (audioData.count > 0); // Something wrong @@ -423,8 +424,10 @@ - (int) feed: (NSArray*)data interleaved: (bool)interleaved [playerNode scheduleBuffer: thePCMOutputBuffer completionHandler: ^(void) { + @autoreleasepool { dispatch_async(dispatch_get_main_queue(), ^{ + @autoreleasepool { --ready; // The Device has sent its packet. One less to send. assert(ready < NB_BUFFERS || !interleaved); if (self ->waitingBlock != nil) @@ -441,7 +444,9 @@ - (int) feed: (NSArray*)data interleaved: (bool)interleaved [self ->flutterSoundPlayer audioPlayerDidFinishPlaying: true]; } + } }); + } }]; return ln; @@ -453,6 +458,7 @@ - (int) feed: (NSArray*)data interleaved: (bool)interleaved waitingBlock = data; return 0; } + } } diff --git a/ios/Classes/FlautoRecorderEngine.mm b/ios/Classes/FlautoRecorderEngine.mm index 71facf5..cab95c7 100644 --- a/ios/Classes/FlautoRecorderEngine.mm +++ b/ios/Classes/FlautoRecorderEngine.mm @@ -83,7 +83,11 @@ { @autoreleasepool { inputStatus = AVAudioConverterInputStatus_HaveData ; - AVAudioPCMBuffer* convertedBuffer = [[AVAudioPCMBuffer alloc]initWithPCMFormat: recordingFormat frameCapacity: [buffer frameCapacity]]; + + double ratio = recordingFormat.sampleRate / inputFormat.sampleRate; + AVAudioFrameCount requiredCapacity = (AVAudioFrameCount)((double)[buffer frameCapacity] * ratio) + 1024; + + AVAudioPCMBuffer* convertedBuffer = [[AVAudioPCMBuffer alloc]initWithPCMFormat: recordingFormat frameCapacity: requiredCapacity]; AVAudioConverterInputBlock inputBlock = ^AVAudioBuffer*(AVAudioPacketCount inNumberOfPackets, AVAudioConverterInputStatus *outStatus) @@ -128,11 +132,13 @@ { dispatch_async(dispatch_get_main_queue(), ^{ - if (flautoRecorder == nil || status == 0) // something bad in the recorder : skip the callback - { - return; - } - [flautoRecorder recordingData: data]; + @autoreleasepool { + if (flautoRecorder == nil || status == 0) // something bad in the recorder : skip the callback + { + return; + } + [flautoRecorder recordingData: data]; + } }); } @@ -162,18 +168,20 @@ } dispatch_async(dispatch_get_main_queue(), ^{ - if (flautoRecorder == nil || getStatus() == 0) // something bad - { - return; - } - if (coder == pcmFloat32) - { - [flautoRecorder recordingDataFloat32: recdata]; - } else - if (coder == pcm16) - { - [flautoRecorder recordingDataInt16: recdata]; - } + @autoreleasepool { + if (flautoRecorder == nil || getStatus() == 0) // something bad + { + return; + } + if (coder == pcmFloat32) + { + [flautoRecorder recordingDataFloat32: recdata]; + } else + if (coder == pcm16) + { + [flautoRecorder recordingDataInt16: recdata]; + } + } }); } // Not interleaved } // (frameLength > 0)