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); + } + }); } } diff --git a/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java b/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java index 3d9467b..941c19b 100644 --- a/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java +++ b/android/src/main/java/xyz/canardoux/TauEngine/FlautoPlayerEngine.java @@ -116,21 +116,27 @@ class FeedThread extends Thread { } public void run() { - 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 = audioTrack.write(ff, 0, mData.length/4, AudioTrack.WRITE_BLOCKING); - ln = 4 * ln; - } else - { - ln = audioTrack.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); } } @@ -142,24 +148,30 @@ class FeedInt16Thread extends Thread { } public void run() { - 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 = audioTrack.write(interleavedData, 0, ln, AudioTrack.WRITE_BLOCKING); - mSession.needSomeFood(1); } } @@ -171,20 +183,26 @@ class FeedFloat32Thread extends Thread { } public void run() { - 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 = audioTrack.write(r, 0, r.length, AudioTrack.WRITE_BLOCKING); - mSession.needSomeFood(1); } } 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); + } + } } } }; 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 3a3ecd4..cab95c7 100644 --- a/ios/Classes/FlautoRecorderEngine.mm +++ b/ios/Classes/FlautoRecorderEngine.mm @@ -81,8 +81,13 @@ [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]]; + + 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) @@ -127,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]; + } }); } @@ -161,21 +168,24 @@ } 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) + } }]; }