react-native-audio-api 1.0.0-nightly-6fd4c68-20261102 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/android/src/main/cpp/audioapi/CMakeLists.txt +13 -0
  2. package/android/src/main/cpp/audioapi/android/AndroidDecoding.cpp +366 -53
  3. package/android/src/main/cpp/audioapi/android/AndroidDecoding.h +6 -1
  4. package/android/src/main/cpp/audioapi/android/ThroughputHintSession.cpp +51 -0
  5. package/android/src/main/cpp/audioapi/android/ThroughputHintSession.h +40 -0
  6. package/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +3 -0
  7. package/common/cpp/audioapi/decoding/AudioDecoding.cpp +47 -13
  8. package/common/cpp/audioapi/decoding/backends/AudioDecoderBackend.h +4 -0
  9. package/common/cpp/audioapi/jsi/RuntimeInstanceCache.hpp +20 -14
  10. package/common/cpp/audioapi/jsi/RuntimeObserver.cpp +23 -11
  11. package/common/cpp/audioapi/jsi/RuntimeObserver.h +2 -0
  12. package/common/cpp/audioapi/libs/miniaudio/decoders/libvorbis/miniaudio_libvorbis.c +5 -2
  13. package/common/cpp/audioapi/utils/AudioArray.hpp +11 -0
  14. package/common/cpp/audioapi/utils/AudioBuffer.hpp +11 -0
  15. package/ios/audioapi/ios/core/utils/IOSDecoding.mm +157 -0
  16. package/ios/audioapi/ios/system/AudioEngine.mm +4 -0
  17. package/ios/audioapi/ios/system/AudioSessionManager.mm +1 -1
  18. package/lib/commonjs/hooks/useAudioInput.js +2 -0
  19. package/lib/commonjs/hooks/useAudioInput.js.map +1 -1
  20. package/lib/module/hooks/useAudioInput.js +2 -0
  21. package/lib/module/hooks/useAudioInput.js.map +1 -1
  22. package/lib/typescript/commonjs/hooks/useAudioInput.d.ts.map +1 -1
  23. package/lib/typescript/module/hooks/useAudioInput.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/src/hooks/useAudioInput.ts +2 -0
@@ -30,6 +30,19 @@ set_source_files_properties(
30
30
  COMPILE_FLAGS "-O3"
31
31
  )
32
32
 
33
+ set_source_files_properties(
34
+ "${ANDROID_CPP_DIR}/audioapi/android/AndroidDecoding.cpp"
35
+ PROPERTIES
36
+ COMPILE_FLAGS "-O3 -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability"
37
+ )
38
+
39
+ # APerformanceHint (API 33) is weak-linked and guarded the same way.
40
+ set_source_files_properties(
41
+ "${ANDROID_CPP_DIR}/audioapi/android/ThroughputHintSession.cpp"
42
+ PROPERTIES
43
+ COMPILE_FLAGS "-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability"
44
+ )
45
+
33
46
  if(CMAKE_C_COMPILER_ID MATCHES "Clang")
34
47
  set_source_files_properties(
35
48
  "${COMMON_CPP_DIR}/audioapi/dsp/r8brain/fft/pffft_double.c"
@@ -1,5 +1,6 @@
1
1
  #include <audioapi/android/AndroidDecoding.h>
2
2
  #include <audioapi/android/AndroidDecodingDataSource.h>
3
+ #include <audioapi/android/ThroughputHintSession.h>
3
4
  #include <audioapi/android/core/utils/AndroidMediaFormat.h>
4
5
 
5
6
  #include <audioapi/dsp/r8brain/Resampler.hpp>
@@ -10,18 +11,24 @@
10
11
  #include <media/NdkMediaCodec.h>
11
12
  #include <media/NdkMediaExtractor.h>
12
13
  #include <media/NdkMediaFormat.h>
14
+ #include <chrono>
13
15
 
14
16
  #include <fcntl.h>
15
17
  #include <sys/stat.h>
16
18
  #include <unistd.h>
17
19
 
18
20
  #include <algorithm>
21
+ #include <atomic>
19
22
  #include <cerrno>
20
23
  #include <cmath>
24
+ #include <condition_variable>
21
25
  #include <cstdint>
22
26
  #include <cstdio>
23
27
  #include <cstring>
28
+ #include <deque>
24
29
  #include <memory>
30
+ #include <mutex>
31
+ #include <optional>
25
32
  #include <string>
26
33
  #include <utility>
27
34
  #include <vector>
@@ -35,6 +42,11 @@ constexpr int ENCODING_PCM_8 = 3;
35
42
  constexpr int ENCODING_PCM_FLOAT = 4;
36
43
  constexpr int RESAMPLE_MAX_IN_FRAMES = 4096;
37
44
  constexpr int64_t CODEC_TIMEOUT_US = 5000;
45
+ constexpr auto CODEC_TIMEOUT = std::chrono::microseconds(CODEC_TIMEOUT_US);
46
+ // Track mime the extractor uses for already-decoded PCM (e.g. WAV).
47
+ constexpr const char *MIME_AUDIO_RAW = "audio/raw";
48
+ constexpr size_t RAW_PACKET_INITIAL_BYTES = size_t{64} * 1024;
49
+ constexpr size_t RAW_PACKET_MAX_BYTES = size_t{16} * 1024 * 1024;
38
50
  // Bail out of the drain loop if MediaCodec keeps saying "try again" after EOS
39
51
  // input without ever flagging EOS output (defensive against a stuck codec).
40
52
  constexpr int MAX_TRY_AGAIN_AFTER_EOS = 200;
@@ -61,6 +73,104 @@ using MediaExtractorPtr = std::unique_ptr<AMediaExtractor, FnDeleter<AMediaExtra
61
73
  using MediaCodecPtr = std::unique_ptr<AMediaCodec, MediaCodecDeleter>;
62
74
  using FilePtr = std::unique_ptr<FILE, FnDeleter<fclose>>;
63
75
 
76
+ // Fields of a codec output format; 0 marks a key the format did not carry.
77
+ struct CodecOutputFormat {
78
+ int sampleRate = 0;
79
+ int channels = 0;
80
+ int pcmEncoding = ENCODING_PCM_16;
81
+ };
82
+
83
+ CodecOutputFormat readCodecOutputFormat(AMediaFormat *format) {
84
+ CodecOutputFormat fields;
85
+ AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, &fields.sampleRate);
86
+ AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &fields.channels);
87
+ if (!AMediaFormat_getInt32(format, "pcm-encoding", &fields.pcmEncoding)) {
88
+ fields.pcmEncoding = ENCODING_PCM_16;
89
+ }
90
+ return fields;
91
+ }
92
+
93
+ // A filled output buffer, or (when outputFormat is set) a format change that
94
+ // must apply before the buffers queued after it.
95
+ struct AsyncOutputEvent {
96
+ int32_t index = -1;
97
+ AMediaCodecBufferInfo info{};
98
+ std::optional<CodecOutputFormat> outputFormat;
99
+ };
100
+
101
+ // Buffer indices handed over by the codec's callback thread in async mode.
102
+ // After a flush, callbacks already in flight may still deliver indices from
103
+ // before it; those buffers are no longer owned by the client, so their
104
+ // getInputBuffer / getOutputBuffer lookup fails and the event is dropped.
105
+ struct AsyncCodecEvents {
106
+ std::mutex mutex;
107
+ std::condition_variable ready;
108
+ std::deque<int32_t> inputs;
109
+ std::deque<AsyncOutputEvent> outputs;
110
+ bool failed = false;
111
+ // The NDK delivers every callback on one thread of its own.
112
+ std::atomic<pid_t> callbackThreadId{0};
113
+
114
+ void clear() {
115
+ std::scoped_lock lock(mutex);
116
+ inputs.clear();
117
+ outputs.clear();
118
+ failed = false;
119
+ }
120
+ };
121
+
122
+ void onAsyncInputAvailable(AMediaCodec * /*codec*/, void *userdata, int32_t index) {
123
+ auto &events = *static_cast<AsyncCodecEvents *>(userdata);
124
+ events.callbackThreadId.store(gettid(), std::memory_order_relaxed);
125
+ {
126
+ std::scoped_lock lock(events.mutex);
127
+ // A stale pre-flush callback and a fresh one can name the same buffer.
128
+ if (std::find(events.inputs.begin(), events.inputs.end(), index) == events.inputs.end()) {
129
+ events.inputs.push_back(index);
130
+ }
131
+ }
132
+ events.ready.notify_one();
133
+ }
134
+
135
+ void onAsyncOutputAvailable(
136
+ AMediaCodec * /*codec*/,
137
+ void *userdata,
138
+ int32_t index,
139
+ AMediaCodecBufferInfo *info) {
140
+ auto &events = *static_cast<AsyncCodecEvents *>(userdata);
141
+ {
142
+ std::scoped_lock lock(events.mutex);
143
+ events.outputs.push_back(AsyncOutputEvent{.index = index, .info = *info});
144
+ }
145
+ events.ready.notify_one();
146
+ }
147
+
148
+ void onAsyncFormatChanged(AMediaCodec * /*codec*/, void *userdata, AMediaFormat *format) {
149
+ auto &events = *static_cast<AsyncCodecEvents *>(userdata);
150
+ const CodecOutputFormat fields = readCodecOutputFormat(format);
151
+ // The NDK hands the callback a copy it never frees.
152
+ AMediaFormat_delete(format);
153
+ {
154
+ std::scoped_lock lock(events.mutex);
155
+ events.outputs.push_back(AsyncOutputEvent{.outputFormat = fields});
156
+ }
157
+ events.ready.notify_one();
158
+ }
159
+
160
+ void onAsyncError(
161
+ AMediaCodec * /*codec*/,
162
+ void *userdata,
163
+ media_status_t /*error*/,
164
+ int32_t /*actionCode*/,
165
+ const char * /*detail*/) {
166
+ auto &events = *static_cast<AsyncCodecEvents *>(userdata);
167
+ {
168
+ std::scoped_lock lock(events.mutex);
169
+ events.failed = true;
170
+ }
171
+ events.ready.notify_one();
172
+ }
173
+
64
174
  decoding::DecoderResult attachMemoryExtractorViaTempFile(
65
175
  AMediaExtractor *extractor,
66
176
  FilePtr &tempFile,
@@ -94,7 +204,12 @@ struct AndroidDecoderState {
94
204
  FilePtr tempFile;
95
205
  AndroidMemoryDataSource dataSource;
96
206
  MediaExtractorPtr extractor;
207
+ // Outlives the codec, whose callbacks write into it until AMediaCodec_delete.
208
+ std::unique_ptr<AsyncCodecEvents> asyncEvents;
97
209
  MediaCodecPtr codec;
210
+ // The track is already PCM: packets are converted directly and no codec runs.
211
+ bool rawPcmTrack = false;
212
+ std::vector<uint8_t> rawPacket;
98
213
 
99
214
  int audioTrackIndex = -1;
100
215
  std::string codecMime;
@@ -122,6 +237,13 @@ struct AndroidDecoderState {
122
237
  std::unique_ptr<r8b::MultiChannelResampler> resampler;
123
238
  std::unique_ptr<AudioBuffer> resampleIn;
124
239
  std::unique_ptr<AudioBuffer> resampleOut;
240
+
241
+ // Decoding is a chain of short bursts on threads in two processes; none of
242
+ // them stays busy long enough for the CPU governor to raise clocks, which
243
+ // roughly halves decoding speed. The session asks for those clocks on our
244
+ // side of the chain: the decoding thread and the codec callback thread.
245
+ std::unique_ptr<ThroughputHintSession> throughputHint;
246
+ bool throughputHintRequested = false;
125
247
  };
126
248
 
127
249
  namespace {
@@ -141,22 +263,29 @@ void compactConsumed(std::vector<float> &buffer, size_t &cursor) {
141
263
  void appendNativePcm(AndroidDecoderState &state, const uint8_t *bytes, size_t byteCount) {
142
264
  compactConsumed(state.nativeLeftover, state.nativeCursor);
143
265
  auto &pcm = state.nativeLeftover;
266
+ const size_t base = pcm.size();
144
267
  if (state.pcmEncoding == ENCODING_PCM_FLOAT) {
145
268
  const size_t sampleCount = byteCount / sizeof(float);
146
- const auto *samples = reinterpret_cast<const float *>(bytes);
147
- pcm.insert(pcm.end(), samples, samples + sampleCount);
269
+ pcm.resize(base + sampleCount);
270
+ std::memcpy(pcm.data() + base, bytes, sampleCount * sizeof(float));
148
271
  return;
149
272
  }
150
273
  if (state.pcmEncoding == ENCODING_PCM_8) {
274
+ pcm.resize(base + byteCount);
275
+ float *out = pcm.data() + base;
151
276
  for (size_t i = 0; i < byteCount; ++i) {
152
- pcm.push_back((static_cast<float>(bytes[i]) - 128.0f) / 128.0f);
277
+ out[i] = (static_cast<float>(bytes[i]) - 128.0f) / 128.0f;
153
278
  }
154
279
  return;
155
280
  }
156
281
  const size_t sampleCount = byteCount / sizeof(int16_t);
157
- const auto *pcm16 = reinterpret_cast<const int16_t *>(bytes);
282
+ pcm.resize(base + sampleCount);
283
+ float *out = pcm.data() + base;
284
+ // memcpy per sample: codec output buffers carry no int16 alignment guarantee.
158
285
  for (size_t i = 0; i < sampleCount; ++i) {
159
- pcm.push_back(static_cast<float>(pcm16[i]) / 32768.0f);
286
+ int16_t sample = 0;
287
+ std::memcpy(&sample, bytes + i * sizeof(int16_t), sizeof(sample));
288
+ out[i] = static_cast<float>(sample) / 32768.0f;
160
289
  }
161
290
  }
162
291
 
@@ -193,20 +322,20 @@ void setupResamplerIfNeeded(AndroidDecoderState &state) {
193
322
  // Applies MediaCodec output format. When rate/channels change vs track metadata
194
323
  // (common with AAC), rebuilds the resampler and drops PCM produced under the
195
324
  // previous layout so leftover samples are not misinterpreted.
196
- void applyCodecOutputFormat(AndroidDecoderState &state, AMediaFormat *outFormat) {
325
+ void applyCodecOutputFormat(AndroidDecoderState &state, const CodecOutputFormat &format) {
197
326
  const int previousChannels = state.channels;
198
327
  const int previousNativeRate = state.nativeSampleRate;
199
328
 
200
- AMediaFormat_getInt32(outFormat, AMEDIAFORMAT_KEY_SAMPLE_RATE, &state.nativeSampleRate);
201
- AMediaFormat_getInt32(outFormat, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &state.channels);
202
- if (!AMediaFormat_getInt32(outFormat, "pcm-encoding", &state.pcmEncoding)) {
203
- state.pcmEncoding = ENCODING_PCM_16;
204
- }
205
- if (state.channels <= 0) {
206
- state.channels = previousChannels > 0 ? previousChannels : 1;
329
+ state.pcmEncoding = format.pcmEncoding;
330
+ if (format.channels > 0) {
331
+ state.channels = format.channels;
332
+ } else if (state.channels <= 0) {
333
+ state.channels = 1;
207
334
  }
208
- if (state.nativeSampleRate <= 0) {
209
- state.nativeSampleRate = previousNativeRate > 0 ? previousNativeRate : 44100;
335
+ if (format.sampleRate > 0) {
336
+ state.nativeSampleRate = format.sampleRate;
337
+ } else if (state.nativeSampleRate <= 0) {
338
+ state.nativeSampleRate = 44100;
210
339
  }
211
340
 
212
341
  if (state.channels == previousChannels && state.nativeSampleRate == previousNativeRate) {
@@ -220,62 +349,173 @@ void applyCodecOutputFormat(AndroidDecoderState &state, AMediaFormat *outFormat)
220
349
  setupResamplerIfNeeded(state);
221
350
  }
222
351
 
223
- // Advances the codec one step: feeds at most one input packet and drains at
224
- // most one output buffer into nativeLeftover. Sets outputEnded at EOS.
225
- void pumpCodec(AndroidDecoderState &state) {
226
- if (state.outputEnded) {
352
+ // Fills the codec input buffer at `index` with the next extractor packet, or
353
+ // with end-of-stream once the extractor runs dry. Returns false when the
354
+ // buffer is not ours (a stale async index) and nothing was queued.
355
+ bool queueNextPacket(AndroidDecoderState &state, size_t index) {
356
+ auto *codec = state.codec.get();
357
+ auto *extractor = state.extractor.get();
358
+
359
+ size_t bufSize = 0;
360
+ uint8_t *buf = AMediaCodec_getInputBuffer(codec, index, &bufSize);
361
+ if (buf == nullptr) {
362
+ return false;
363
+ }
364
+
365
+ const ssize_t sampleSize = AMediaExtractor_readSampleData(extractor, buf, bufSize);
366
+ if (sampleSize < 0) {
367
+ AMediaCodec_queueInputBuffer(codec, index, 0, 0, 0, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);
368
+ state.inputEnded = true;
369
+ } else {
370
+ const int64_t pts = AMediaExtractor_getSampleTime(extractor);
371
+ AMediaCodec_queueInputBuffer(codec, index, 0, static_cast<size_t>(sampleSize), pts, 0);
372
+ AMediaExtractor_advance(extractor);
373
+ }
374
+ return true;
375
+ }
376
+
377
+ // Appends the PCM of the codec output buffer at `index` to nativeLeftover and
378
+ // hands the buffer back. Sets outputEnded at EOS. A buffer that is not ours
379
+ // (a stale async index) is ignored, EOS flag included.
380
+ void drainOutputBuffer(
381
+ AndroidDecoderState &state,
382
+ size_t index,
383
+ const AMediaCodecBufferInfo &info) {
384
+ auto *codec = state.codec.get();
385
+
386
+ size_t outBufSize = 0;
387
+ uint8_t *outBuf = AMediaCodec_getOutputBuffer(codec, index, &outBufSize);
388
+ if (outBuf == nullptr) {
227
389
  return;
228
390
  }
229
391
 
392
+ state.tryAgainAfterEos = 0;
393
+ const auto offset = std::min(static_cast<size_t>(std::max(info.offset, 0)), outBufSize);
394
+ const auto size = std::min(static_cast<size_t>(std::max(info.size, 0)), outBufSize - offset);
395
+ if (size > 0) {
396
+ appendNativePcm(state, outBuf + offset, size);
397
+ }
398
+ AMediaCodec_releaseOutputBuffer(codec, index, false);
399
+ if ((info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != 0) {
400
+ state.outputEnded = true;
401
+ }
402
+ }
403
+
404
+ void countTryAgainAfterEos(AndroidDecoderState &state) {
405
+ if (state.inputEnded && ++state.tryAgainAfterEos > MAX_TRY_AGAIN_AFTER_EOS) {
406
+ state.outputEnded = true;
407
+ }
408
+ }
409
+
410
+ // Sync mode: feeds at most one input packet and drains at most one output
411
+ // buffer, each behind a blocking dequeue call.
412
+ void pumpCodecSync(AndroidDecoderState &state) {
230
413
  auto *codec = state.codec.get();
231
- auto *extractor = state.extractor.get();
232
414
 
233
415
  if (!state.inputEnded) {
234
416
  const ssize_t inIndex = AMediaCodec_dequeueInputBuffer(codec, CODEC_TIMEOUT_US);
235
- if (inIndex >= 0) {
236
- size_t bufSize = 0;
237
- uint8_t *buf = AMediaCodec_getInputBuffer(codec, static_cast<size_t>(inIndex), &bufSize);
238
- const ssize_t sampleSize =
239
- buf != nullptr ? AMediaExtractor_readSampleData(extractor, buf, bufSize) : -1;
240
- if (sampleSize < 0) {
241
- AMediaCodec_queueInputBuffer(
242
- codec, static_cast<size_t>(inIndex), 0, 0, 0, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);
243
- state.inputEnded = true;
244
- } else {
245
- const int64_t pts = AMediaExtractor_getSampleTime(extractor);
246
- AMediaCodec_queueInputBuffer(
247
- codec, static_cast<size_t>(inIndex), 0, static_cast<size_t>(sampleSize), pts, 0);
248
- AMediaExtractor_advance(extractor);
249
- }
417
+ if (inIndex >= 0 && !queueNextPacket(state, static_cast<size_t>(inIndex))) {
418
+ state.inputEnded = true;
250
419
  }
251
420
  }
252
421
 
253
422
  AMediaCodecBufferInfo info{};
254
423
  const ssize_t outIndex = AMediaCodec_dequeueOutputBuffer(codec, &info, CODEC_TIMEOUT_US);
255
424
  if (outIndex >= 0) {
256
- state.tryAgainAfterEos = 0;
257
- if (info.size > 0) {
258
- size_t outBufSize = 0;
259
- uint8_t *outBuf =
260
- AMediaCodec_getOutputBuffer(codec, static_cast<size_t>(outIndex), &outBufSize);
261
- if (outBuf != nullptr) {
262
- appendNativePcm(state, outBuf + info.offset, static_cast<size_t>(info.size));
263
- }
264
- }
265
- AMediaCodec_releaseOutputBuffer(codec, static_cast<size_t>(outIndex), false);
266
- if ((info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != 0) {
267
- state.outputEnded = true;
268
- }
425
+ drainOutputBuffer(state, static_cast<size_t>(outIndex), info);
269
426
  } else if (outIndex == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
270
427
  AMediaFormat *outFormat = AMediaCodec_getOutputFormat(codec);
271
428
  if (outFormat != nullptr) {
272
- applyCodecOutputFormat(state, outFormat);
429
+ applyCodecOutputFormat(state, readCodecOutputFormat(outFormat));
273
430
  AMediaFormat_delete(outFormat);
274
431
  }
275
432
  } else if (outIndex == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
276
- if (state.inputEnded && ++state.tryAgainAfterEos > MAX_TRY_AGAIN_AFTER_EOS) {
277
- state.outputEnded = true;
433
+ countTryAgainAfterEos(state);
434
+ }
435
+ }
436
+
437
+ // Async mode: waits for the codec callbacks, then feeds every input buffer and
438
+ // drains every output buffer they announced. Skipping the dequeue calls saves
439
+ // two of the six blocking codec round trips each packet costs in sync mode.
440
+ void pumpCodecAsync(AndroidDecoderState &state) {
441
+ auto &events = *state.asyncEvents;
442
+ std::deque<int32_t> inputs;
443
+ std::deque<AsyncOutputEvent> outputs;
444
+ bool failed = false;
445
+ {
446
+ std::unique_lock lock(events.mutex);
447
+ events.ready.wait_for(lock, CODEC_TIMEOUT, [&] {
448
+ return events.failed || !events.outputs.empty() ||
449
+ (!state.inputEnded && !events.inputs.empty());
450
+ });
451
+ failed = events.failed;
452
+ if (!state.inputEnded) {
453
+ inputs.swap(events.inputs);
278
454
  }
455
+ outputs.swap(events.outputs);
456
+ }
457
+
458
+ if (failed) {
459
+ state.outputEnded = true;
460
+ return;
461
+ }
462
+ if (inputs.empty() && outputs.empty()) {
463
+ countTryAgainAfterEos(state);
464
+ return;
465
+ }
466
+
467
+ for (const int32_t index : inputs) {
468
+ if (state.inputEnded) {
469
+ break;
470
+ }
471
+ queueNextPacket(state, static_cast<size_t>(index));
472
+ }
473
+ for (const auto &event : outputs) {
474
+ if (state.outputEnded) {
475
+ break;
476
+ }
477
+ if (event.outputFormat.has_value()) {
478
+ applyCodecOutputFormat(state, *event.outputFormat);
479
+ } else {
480
+ drainOutputBuffer(state, static_cast<size_t>(event.index), event.info);
481
+ }
482
+ }
483
+ }
484
+
485
+ // Raw PCM track: converts the next extractor packet without a codec round trip.
486
+ void pumpRawPcm(AndroidDecoderState &state) {
487
+ auto *extractor = state.extractor.get();
488
+ ssize_t packetBytes =
489
+ AMediaExtractor_readSampleData(extractor, state.rawPacket.data(), state.rawPacket.size());
490
+ // readSampleData also fails when the packet outgrows the buffer; a packet
491
+ // still has a timestamp then, while the end of the track has none.
492
+ while (packetBytes < 0 && AMediaExtractor_getSampleTime(extractor) >= 0 &&
493
+ state.rawPacket.size() < RAW_PACKET_MAX_BYTES) {
494
+ state.rawPacket.resize(state.rawPacket.size() * 2);
495
+ packetBytes =
496
+ AMediaExtractor_readSampleData(extractor, state.rawPacket.data(), state.rawPacket.size());
497
+ }
498
+ if (packetBytes < 0) {
499
+ state.inputEnded = true;
500
+ state.outputEnded = true;
501
+ return;
502
+ }
503
+ appendNativePcm(state, state.rawPacket.data(), static_cast<size_t>(packetBytes));
504
+ AMediaExtractor_advance(extractor);
505
+ }
506
+
507
+ // Advances the codec, appending whatever PCM it produced to nativeLeftover.
508
+ // Sets outputEnded at EOS.
509
+ void pumpCodec(AndroidDecoderState &state) {
510
+ if (state.outputEnded) {
511
+ return;
512
+ }
513
+ if (state.rawPcmTrack) {
514
+ pumpRawPcm(state);
515
+ } else if (state.asyncEvents != nullptr) {
516
+ pumpCodecAsync(state);
517
+ } else {
518
+ pumpCodecSync(state);
279
519
  }
280
520
  }
281
521
 
@@ -374,6 +614,14 @@ decoding::DecoderResult configureExtractorMetadata(
374
614
  state.codecMime = mime != nullptr ? mime : "audio/mp4a-latm";
375
615
  AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, &state.nativeSampleRate);
376
616
  AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, &state.channels);
617
+ state.rawPcmTrack = state.codecMime == MIME_AUDIO_RAW;
618
+ if (state.rawPcmTrack) {
619
+ state.pcmEncoding = readCodecOutputFormat(format).pcmEncoding;
620
+ int32_t maxPacketBytes = 0;
621
+ AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_MAX_INPUT_SIZE, &maxPacketBytes);
622
+ state.rawPacket.resize(
623
+ std::max(RAW_PACKET_INITIAL_BYTES, static_cast<size_t>(std::max(maxPacketBytes, 0))));
624
+ }
377
625
  int64_t durationUs = 0;
378
626
  AMediaFormat_getInt64(format, AMEDIAFORMAT_KEY_DURATION, &durationUs);
379
627
  AMediaFormat_delete(format);
@@ -384,8 +632,43 @@ decoding::DecoderResult configureExtractorMetadata(
384
632
  return Ok(None);
385
633
  }
386
634
 
635
+ // Routes codec buffer notifications into state.asyncEvents. Must run before
636
+ // configure; returns false, leaving the codec in sync mode, below API 28.
637
+ bool enableAsyncCallbacks(AndroidDecoderState &state, AMediaCodec *codec) {
638
+ if (__builtin_available(android 28, *)) {
639
+ auto events = std::make_unique<AsyncCodecEvents>();
640
+ const AMediaCodecOnAsyncNotifyCallback callbacks{
641
+ .onAsyncInputAvailable = onAsyncInputAvailable,
642
+ .onAsyncOutputAvailable = onAsyncOutputAvailable,
643
+ .onAsyncFormatChanged = onAsyncFormatChanged,
644
+ .onAsyncError = onAsyncError,
645
+ };
646
+ if (AMediaCodec_setAsyncNotifyCallback(codec, callbacks, events.get()) != AMEDIA_OK) {
647
+ return false;
648
+ }
649
+ state.asyncEvents = std::move(events);
650
+ return true;
651
+ }
652
+ return false;
653
+ }
654
+
655
+ // Joins the calling (decoding) thread and the codec callback thread into one
656
+ // performance hint session. Tried once per decoder; without support it is a no-op.
657
+ void startThroughputHint(AndroidDecoderState &state) {
658
+ state.throughputHintRequested = true;
659
+ std::vector<int32_t> threadIds{gettid()};
660
+ if (state.asyncEvents != nullptr) {
661
+ const pid_t callbackThreadId =
662
+ state.asyncEvents->callbackThreadId.load(std::memory_order_relaxed);
663
+ if (callbackThreadId != 0 && callbackThreadId != threadIds.front()) {
664
+ threadIds.push_back(callbackThreadId);
665
+ }
666
+ }
667
+ state.throughputHint = ThroughputHintSession::create(threadIds);
668
+ }
669
+
387
670
  decoding::DecoderResult ensureCodecStarted(AndroidDecoderState &state) {
388
- if (state.codec != nullptr) {
671
+ if (state.codec != nullptr || state.rawPcmTrack) {
389
672
  return Ok(None);
390
673
  }
391
674
  if (state.extractor == nullptr || state.audioTrackIndex < 0) {
@@ -403,8 +686,11 @@ decoding::DecoderResult ensureCodecStarted(AndroidDecoderState &state) {
403
686
  AMediaFormat_delete(format);
404
687
  return Err("AndroidDecoder: createDecoderByType failed");
405
688
  }
689
+ state.asyncEvents.reset();
690
+ enableAsyncCallbacks(state, codec);
406
691
  if (AMediaCodec_configure(codec, format, nullptr, nullptr, 0) != AMEDIA_OK) {
407
692
  AMediaCodec_delete(codec);
693
+ state.asyncEvents.reset();
408
694
  AMediaFormat_delete(format);
409
695
  return Err("AndroidDecoder: configure failed");
410
696
  }
@@ -412,6 +698,7 @@ decoding::DecoderResult ensureCodecStarted(AndroidDecoderState &state) {
412
698
 
413
699
  if (AMediaCodec_start(codec) != AMEDIA_OK) {
414
700
  AMediaCodec_delete(codec);
701
+ state.asyncEvents.reset();
415
702
  return Err("AndroidDecoder: start failed");
416
703
  }
417
704
 
@@ -555,6 +842,10 @@ size_t AndroidDecoder::readPcmFrames(float *outInterleaved, size_t frameCount) {
555
842
  if (ensureCodecStarted(state).is_err()) {
556
843
  return 0;
557
844
  }
845
+ if (preferThroughput_ && !state.throughputHintRequested) {
846
+ startThroughputHint(state);
847
+ }
848
+ const auto workStart = std::chrono::steady_clock::now();
558
849
 
559
850
  size_t filled = 0; // frames
560
851
  // The caller sized outInterleaved from outputChannels(), so this call must
@@ -598,6 +889,10 @@ size_t AndroidDecoder::readPcmFrames(float *outInterleaved, size_t frameCount) {
598
889
  }
599
890
  }
600
891
 
892
+ if (state.throughputHint != nullptr) {
893
+ state.throughputHint->reportWork(std::chrono::steady_clock::now() - workStart);
894
+ }
895
+
601
896
  framePosition_ += static_cast<int64_t>(filled);
602
897
  return filled;
603
898
  }
@@ -620,7 +915,16 @@ decoding::DecoderResult AndroidDecoder::seekToTime(double seconds) {
620
915
  AMEDIA_OK) {
621
916
  return Err("AndroidDecoder::seekToTime: AMediaExtractor_seekTo failed");
622
917
  }
623
- AMediaCodec_flush(state.codec.get());
918
+ if (state.codec != nullptr) {
919
+ AMediaCodec_flush(state.codec.get());
920
+ }
921
+ if (state.asyncEvents != nullptr) {
922
+ // A flushed async codec stays silent until restarted.
923
+ state.asyncEvents->clear();
924
+ if (AMediaCodec_start(state.codec.get()) != AMEDIA_OK) {
925
+ return Err("AndroidDecoder::seekToTime: restarting the codec after flush failed");
926
+ }
927
+ }
624
928
 
625
929
  state.nativeLeftover.clear();
626
930
  state.nativeCursor = 0;
@@ -640,6 +944,15 @@ decoding::DecoderResult AndroidDecoder::seekToTime(double seconds) {
640
944
  return Ok(None);
641
945
  }
642
946
 
947
+ void AndroidDecoder::setPreferThroughput(bool preferThroughput) {
948
+ preferThroughput_ = preferThroughput;
949
+ if (!preferThroughput && impl_ != nullptr) {
950
+ // Closing the session lets the clocks drop; re-enabling starts a new one.
951
+ impl_->throughputHint.reset();
952
+ impl_->throughputHintRequested = false;
953
+ }
954
+ }
955
+
643
956
  void AndroidDecoder::releaseImpl() {
644
957
  impl_.reset();
645
958
  }
@@ -16,7 +16,8 @@ struct AndroidDecoderState;
16
16
  *
17
17
  * Opens local files or in-memory buffers and pulls interleaved float PCM on
18
18
  * demand. Container metadata (including duration) is read at open time; the
19
- * MediaCodec pipeline starts lazily on the first read or seek. In-memory
19
+ * MediaCodec pipeline starts lazily on the first read or seek, in async
20
+ * (callback) mode on API 28+ and sync mode below. In-memory
20
21
  * sources use AMediaDataSource on API 28+ (runtime check); API 21–27 falls
21
22
  * back to a temp file + fd. Remote URL / HLS decoding stays on FFmpeg.
22
23
  */
@@ -30,6 +31,9 @@ class AndroidDecoder : public decoding::OsDecoderBase {
30
31
  [[nodiscard]] decoding::DecoderResult open(const decoding::EncodedMemorySource &source);
31
32
  [[nodiscard]] size_t readPcmFrames(float *outInterleaved, size_t frameCount) override;
32
33
  [[nodiscard]] decoding::DecoderResult seekToTime(double seconds) override;
34
+ /// While on, boosts CPU clocks for the decoding threads through an ADPF
35
+ /// performance hint session on API 33+; does nothing below that.
36
+ void setPreferThroughput(bool preferThroughput) override;
33
37
 
34
38
  private:
35
39
  [[nodiscard]] decoding::DecoderResult finishOpeningDecoder(
@@ -40,6 +44,7 @@ class AndroidDecoder : public decoding::OsDecoderBase {
40
44
 
41
45
  // Opaque NDK state (defined in AndroidDecoding.cpp).
42
46
  std::unique_ptr<AndroidDecoderState> impl_;
47
+ bool preferThroughput_ = false;
43
48
  };
44
49
 
45
50
  } // namespace audioapi::android::decoder
@@ -0,0 +1,51 @@
1
+ #include <audioapi/android/ThroughputHintSession.h>
2
+
3
+ #include <android/performance_hint.h>
4
+
5
+ #include <algorithm>
6
+ #include <memory>
7
+ #include <utility>
8
+ #include <vector>
9
+
10
+ namespace audioapi::android {
11
+ namespace {
12
+
13
+ // Shorter than any burst on purpose: every report then asks for more CPU,
14
+ // keeping the clocks raised for as long as the session lives.
15
+ constexpr int64_t THROUGHPUT_HINT_TARGET_NS = 1'000'000;
16
+
17
+ } // namespace
18
+
19
+ std::unique_ptr<ThroughputHintSession> ThroughputHintSession::create(
20
+ const std::vector<int32_t> &threadIds) {
21
+ if (__builtin_available(android 33, *)) {
22
+ APerformanceHintManager *manager = APerformanceHint_getManager();
23
+ if (manager == nullptr) {
24
+ return nullptr;
25
+ }
26
+ SessionPtr session(APerformanceHint_createSession(
27
+ manager, threadIds.data(), threadIds.size(), THROUGHPUT_HINT_TARGET_NS));
28
+ if (session == nullptr) {
29
+ return nullptr;
30
+ }
31
+ return std::unique_ptr<ThroughputHintSession>(new ThroughputHintSession(std::move(session)));
32
+ }
33
+ return nullptr;
34
+ }
35
+
36
+ ThroughputHintSession::ThroughputHintSession(SessionPtr session) : session_(std::move(session)) {}
37
+
38
+ void ThroughputHintSession::SessionCloser::operator()(APerformanceHintSession *session) const {
39
+ if (__builtin_available(android 33, *)) {
40
+ APerformanceHint_closeSession(session);
41
+ }
42
+ }
43
+
44
+ void ThroughputHintSession::reportWork(std::chrono::nanoseconds duration) {
45
+ if (__builtin_available(android 33, *)) {
46
+ APerformanceHint_reportActualWorkDuration(
47
+ session_.get(), std::max<int64_t>(duration.count(), 1));
48
+ }
49
+ }
50
+
51
+ } // namespace audioapi::android
@@ -0,0 +1,40 @@
1
+ #pragma once
2
+
3
+ #include <audioapi/utils/Macros.h>
4
+ #include <chrono>
5
+ #include <cstdint>
6
+ #include <memory>
7
+ #include <vector>
8
+
9
+ struct APerformanceHintSession;
10
+
11
+ namespace audioapi::android {
12
+
13
+ /// ADPF performance hint session that keeps CPU clocks raised for a group of
14
+ /// threads doing bursty work which would otherwise never look busy enough to
15
+ /// the governor. Requires API 33; ThroughputHintSession.cpp weak-links the NDK
16
+ /// symbols and guards every call.
17
+ class ThroughputHintSession {
18
+ public:
19
+ /// Returns null below API 33 or when the device offers no hint sessions.
20
+ [[nodiscard]] static std::unique_ptr<ThroughputHintSession> create(
21
+ const std::vector<int32_t> &threadIds);
22
+
23
+ ~ThroughputHintSession() = default;
24
+ DELETE_COPY_AND_MOVE(ThroughputHintSession);
25
+
26
+ /// Reports how long one burst of work took on the session's threads.
27
+ void reportWork(std::chrono::nanoseconds duration);
28
+
29
+ private:
30
+ struct SessionCloser {
31
+ void operator()(APerformanceHintSession *session) const;
32
+ };
33
+ using SessionPtr = std::unique_ptr<APerformanceHintSession, SessionCloser>;
34
+
35
+ explicit ThroughputHintSession(SessionPtr session);
36
+
37
+ SessionPtr session_;
38
+ };
39
+
40
+ } // namespace audioapi::android
@@ -17,6 +17,7 @@ import com.facebook.react.turbomodule.core.CallInvokerHolderImpl
17
17
  import com.swmansion.audioapi.system.ForegroundServiceManager
18
18
  import com.swmansion.audioapi.system.MediaSessionManager
19
19
  import com.swmansion.audioapi.system.NativeFileInfo
20
+ import com.swmansion.audioapi.system.NativeRecorderControl
20
21
  import com.swmansion.audioapi.system.PermissionRequestListener
21
22
  import java.lang.ref.WeakReference
22
23
  import kotlin.concurrent.thread
@@ -92,6 +93,8 @@ class AudioAPIModule(
92
93
  }
93
94
 
94
95
  override fun invalidate() {
96
+ // Before the cleanups below pull the foreground service and input routing from under it.
97
+ NativeRecorderControl.stop()
95
98
  reactContext.get()?.removeLifecycleEventListener(this)
96
99
  ForegroundServiceManager.cleanup()
97
100
  MediaSessionManager.cleanup()
@@ -48,33 +48,67 @@ makeEncodedMemorySource(const void *data, size_t size, int sampleRate) {
48
48
  .sampleRate = sampleRate};
49
49
  }
50
50
 
51
+ // Container durations are estimates, trusted only up to this length so a
52
+ // corrupt header cannot force a huge allocation. Longer files grow past it.
53
+ constexpr float MAX_TRUSTED_ESTIMATE_SECONDS = 30.0F * 60.0F;
54
+ // Headroom over the estimate (1/64 ≈ 1.6%) for encoder padding and priming.
55
+ constexpr size_t ESTIMATE_HEADROOM_DIVISOR = 64;
56
+ // Capacity growth once the estimate is exceeded: x1.5.
57
+ constexpr size_t GROWTH_NUMERATOR = 3;
58
+ constexpr size_t GROWTH_DENOMINATOR = 2;
59
+ // Unused capacity up to 1/8 of the decoded length is trimmed in place (the
60
+ // allocation stays); more than that is worth an exact-size copy.
61
+ constexpr size_t IN_PLACE_TRIM_SLACK_DIVISOR = 8;
62
+
63
+ size_t initialDecodeCapacity(size_t estimatedFrames, float sampleRate, size_t chunkFrames) {
64
+ const auto maxTrustedEstimate =
65
+ static_cast<size_t>(std::max(1.0F, sampleRate) * MAX_TRUSTED_ESTIMATE_SECONDS);
66
+ const size_t trustedEstimate = std::min(estimatedFrames, maxTrustedEstimate);
67
+ return trustedEstimate + trustedEstimate / ESTIMATE_HEADROOM_DIVISOR + chunkFrames;
68
+ }
69
+
70
+ size_t grownDecodeCapacity(size_t currentCapacity, size_t requiredFrames) {
71
+ return std::max(requiredFrames, currentCapacity * GROWTH_NUMERATOR / GROWTH_DENOMINATOR);
72
+ }
73
+
74
+ // Decodes straight into the returned planar buffer, sized up front from the
75
+ // decoder's frame estimate, so peak memory stays near one copy of the PCM.
51
76
  AudioBufferResult decodeAll(decoding::AudioDecoderBackend &decoder) {
77
+ constexpr size_t chunkFrames = decoding::AudioDecoderBackend::CHUNK_SIZE;
78
+ decoder.setPreferThroughput(true);
52
79
  const int channels = std::max(1, decoder.outputChannels());
53
80
  const auto outputSampleRate = static_cast<float>(decoder.outputSampleRate());
54
81
 
55
- std::vector<float> interleaved;
56
- std::vector<float> chunk(
57
- decoding::AudioDecoderBackend::CHUNK_SIZE * static_cast<size_t>(channels));
82
+ const size_t initialCapacity =
83
+ initialDecodeCapacity(decoder.getTotalPcmFrameCount(), outputSampleRate, chunkFrames);
84
+ auto audioBuffer = std::make_shared<AudioBuffer>(initialCapacity, channels, outputSampleRate);
58
85
 
86
+ std::vector<float> chunk(chunkFrames * static_cast<size_t>(channels));
87
+ size_t framesDecoded = 0;
59
88
  while (true) {
60
- const size_t framesRead =
61
- decoder.readPcmFrames(chunk.data(), decoding::AudioDecoderBackend::CHUNK_SIZE);
89
+ const size_t framesRead = decoder.readPcmFrames(chunk.data(), chunkFrames);
62
90
  if (framesRead == 0) {
63
91
  break;
64
92
  }
65
- interleaved.insert(
66
- interleaved.end(),
67
- chunk.begin(),
68
- chunk.begin() + static_cast<std::ptrdiff_t>(framesRead * static_cast<size_t>(channels)));
93
+ if (framesDecoded + framesRead > audioBuffer->getSize()) {
94
+ audioBuffer->resize(grownDecodeCapacity(audioBuffer->getSize(), framesDecoded + framesRead));
95
+ }
96
+ audioBuffer->deinterleaveFrom(chunk.data(), framesDecoded, framesRead);
97
+ framesDecoded += framesRead;
69
98
  }
70
99
 
71
- if (interleaved.empty()) {
100
+ if (framesDecoded == 0) {
72
101
  return Err("Failed to decode any frames");
73
102
  }
74
103
 
75
- const size_t outputFrames = interleaved.size() / static_cast<size_t>(channels);
76
- auto audioBuffer = std::make_shared<AudioBuffer>(outputFrames, channels, outputSampleRate);
77
- audioBuffer->deinterleaveFrom(interleaved.data(), outputFrames);
104
+ const size_t capacity = audioBuffer->getSize();
105
+ if (capacity - framesDecoded <= framesDecoded / IN_PLACE_TRIM_SLACK_DIVISOR) {
106
+ audioBuffer->resize(framesDecoded);
107
+ } else {
108
+ auto exact = std::make_shared<AudioBuffer>(framesDecoded, channels, outputSampleRate);
109
+ exact->copy(*audioBuffer, 0, 0, framesDecoded);
110
+ audioBuffer = std::move(exact);
111
+ }
78
112
  return Ok(std::move(audioBuffer));
79
113
  }
80
114
 
@@ -19,6 +19,10 @@ class AudioDecoderBackend {
19
19
 
20
20
  [[nodiscard]] virtual size_t readPcmFrames(float *outInterleaved, size_t frameCount) = 0;
21
21
  [[nodiscard]] virtual DecoderResult seekToTime(double seconds) = 0;
22
+ /// Whether the platform should favour decoding speed over power: on when
23
+ /// decoding a whole file at once rather than streaming it in real time.
24
+ /// Off initially; a no-op by default.
25
+ virtual void setPreferThroughput(bool /*preferThroughput*/) {}
22
26
  virtual void close() = 0;
23
27
  [[nodiscard]] virtual bool isOpen() const = 0;
24
28
 
@@ -3,6 +3,7 @@
3
3
  #include <audioapi/jsi/RuntimeObserver.h>
4
4
 
5
5
  #include <jsi/jsi.h>
6
+ #include <mutex>
6
7
  #include <unordered_map>
7
8
  #include <utility>
8
9
 
@@ -24,31 +25,36 @@ template <typename T>
24
25
  class RuntimeInstanceCache : public RuntimeObserverListener {
25
26
  public:
26
27
  void onRuntimeDestroyed(jsi::Runtime *rt) override {
27
- // A runtime has been destroyed, so destroy the related cache.
28
+ std::scoped_lock lock(runtimeCachesMutex_);
28
29
  runtimeCaches_.erase(rt);
29
30
  }
30
31
 
31
32
  ~RuntimeInstanceCache() override {
32
- for (auto &cache : runtimeCaches_) {
33
- // remove all `onRuntimeDestroyed` listeners.
34
- RuntimeObserver::removeListener(*cache.first, this);
35
- }
33
+ // Unregister under the observer's lock rather than walking `runtimeCaches_`, which a runtime
34
+ // being torn down on another thread may be erasing from at the same time.
35
+ RuntimeObserver::removeListener(this);
36
36
  }
37
37
 
38
38
  T &get(jsi::Runtime &rt) {
39
- if (runtimeCaches_.count(&rt) == 0) {
40
- // This is the first time this Runtime has been accessed.
41
- // We set up a `onRuntimeDestroyed` listener for it and
42
- // initialize the cache map.
43
- RuntimeObserver::addListener(rt, this);
44
-
45
- T cache;
46
- runtimeCaches_.emplace(&rt, std::move(cache));
39
+ {
40
+ std::scoped_lock lock(runtimeCachesMutex_);
41
+ if (auto cache = runtimeCaches_.find(&rt); cache != runtimeCaches_.end()) {
42
+ return cache->second;
43
+ }
47
44
  }
48
- return runtimeCaches_.at(&rt);
45
+ // Registered without holding `runtimeCachesMutex_`: teardown takes the observer's lock and
46
+ // then this one, so taking them in the opposite order here could deadlock.
47
+ RuntimeObserver::addListener(rt, this);
48
+
49
+ std::scoped_lock lock(runtimeCachesMutex_);
50
+ // first time this runtime has seen this cache, create it and return it.
51
+ return runtimeCaches_.emplace(&rt, T{}).first->second;
49
52
  }
50
53
 
51
54
  private:
55
+ // Guards the map structure: `get` runs on each runtime's JS thread while `onRuntimeDestroyed`
56
+ // runs on whichever thread tears another runtime down.
57
+ std::mutex runtimeCachesMutex_;
52
58
  std::unordered_map<jsi::Runtime *, T> runtimeCaches_;
53
59
  };
54
60
 
@@ -8,25 +8,30 @@
8
8
  namespace audioapi {
9
9
 
10
10
  static std::unordered_map<jsi::Runtime *, std::unordered_set<RuntimeObserverListener *>> listeners;
11
- static std::mutex listenersMutex;
11
+ // Host objects are destroyed on whichever thread drops their last reference, and runtimes can be
12
+ // torn down off the JS thread. Recursive because notifying a listener can destroy other host
13
+ // objects, which remove themselves on the same thread.
14
+ static std::recursive_mutex listenersMutex;
12
15
 
13
16
  struct RuntimeObserverObject : public jsi::HostObject {
14
17
  jsi::Runtime *rt_;
15
18
  explicit RuntimeObserverObject(jsi::Runtime *rt) : rt_(rt) {}
16
19
  ~RuntimeObserverObject() override {
17
- std::unordered_set<RuntimeObserverListener *> toNotify;
18
- {
19
- std::scoped_lock lock(listenersMutex);
20
- auto listenersSet = listeners.find(rt_);
21
- if (listenersSet != listeners.end()) {
22
- toNotify = std::move(listenersSet->second);
23
- listeners.erase(listenersSet);
24
- }
20
+ // Notify while holding the lock, so a listener being destroyed on another thread cannot be
21
+ // freed between leaving the set and being notified. Take one listener at a time: a notified
22
+ // listener can remove others from this set.
23
+ std::scoped_lock lock(listenersMutex);
24
+ auto listenersSet = listeners.find(rt_);
25
+ if (listenersSet == listeners.end()) {
26
+ return;
25
27
  }
26
- // Notify outside the lock — listener callbacks may re-enter add/remove.
27
- for (auto *listener : toNotify) {
28
+ auto &runtimeListeners = listenersSet->second;
29
+ while (!runtimeListeners.empty()) {
30
+ auto *listener = *runtimeListeners.begin();
31
+ runtimeListeners.erase(runtimeListeners.begin());
28
32
  listener->onRuntimeDestroyed(rt_);
29
33
  }
34
+ listeners.erase(rt_);
30
35
  }
31
36
  };
32
37
 
@@ -60,4 +65,11 @@ void RuntimeObserver::removeListener(jsi::Runtime &rt, RuntimeObserverListener *
60
65
  }
61
66
  }
62
67
 
68
+ void RuntimeObserver::removeListener(RuntimeObserverListener *listener) {
69
+ std::scoped_lock lock(listenersMutex);
70
+ for (auto &[rt, runtimeListeners] : listeners) {
71
+ runtimeListeners.erase(listener);
72
+ }
73
+ }
74
+
63
75
  } // namespace audioapi
@@ -24,6 +24,8 @@ struct RuntimeObserverListener {
24
24
  struct RuntimeObserver {
25
25
  static void addListener(jsi::Runtime &rt, RuntimeObserverListener *listener);
26
26
  static void removeListener(jsi::Runtime &rt, RuntimeObserverListener *listener);
27
+ /** Removes the listener from every runtime it is registered with. */
28
+ static void removeListener(RuntimeObserverListener *listener);
27
29
  };
28
30
 
29
31
  } // namespace audioapi
@@ -482,8 +482,11 @@ MA_API ma_result ma_libvorbis_get_length_in_pcm_frames(ma_libvorbis* pVorbis, ma
482
482
 
483
483
  #if !defined(MA_NO_LIBVORBIS)
484
484
  {
485
- /* I don't know how to reliably retrieve the length in frames using libvorbis, so returning 0 for now. */
486
- *pLength = 0;
485
+ /* ov_pcm_total() needs a seekable stream; an unseekable one keeps the length unknown (0). */
486
+ ogg_int64_t length = ov_pcm_total((OggVorbis_File*)pVorbis->vf, -1);
487
+ if (length > 0) {
488
+ *pLength = (ma_uint64)length;
489
+ }
487
490
 
488
491
  return MA_SUCCESS;
489
492
  }
@@ -67,6 +67,17 @@ class AlignedAudioArray {
67
67
  return size_;
68
68
  }
69
69
 
70
+ /// @brief Changes the length in place, keeping existing samples and zero-filling growth.
71
+ /// @note Shrinking keeps the allocation; growing reallocates to exactly `size`,
72
+ /// so the caller owns the growth policy.
73
+ void resize(size_t size) {
74
+ if (size > data_.capacity()) {
75
+ data_.reserve(size);
76
+ }
77
+ data_.resize(size, 0.0f);
78
+ size_ = size;
79
+ }
80
+
70
81
  float &operator[](size_t index) noexcept {
71
82
  return data_[index];
72
83
  }
@@ -109,6 +109,17 @@ class AlignedAudioBuffer {
109
109
  [[nodiscard]] size_t getSize() const noexcept {
110
110
  return size_;
111
111
  }
112
+
113
+ /// @brief Changes the frame count of every channel in place (see AlignedAudioArray::resize).
114
+ /// @note Only for a buffer nothing else references yet: channels shared through
115
+ /// shareChannels() or exposed to JS would change length under their holders.
116
+ void resize(size_t size) {
117
+ for (const auto &channel : channels_) {
118
+ channel->resize(size);
119
+ }
120
+ size_ = size;
121
+ }
122
+
112
123
  [[nodiscard]] double getDuration() const noexcept {
113
124
  return static_cast<double>(size_) / static_cast<double>(getSampleRate());
114
125
  }
@@ -10,6 +10,8 @@
10
10
  #include <cstdint>
11
11
  #include <cstring>
12
12
  #include <memory>
13
+ #include <optional>
14
+ #include <span>
13
15
  #include <string>
14
16
  #include <type_traits>
15
17
  #include <utility>
@@ -116,6 +118,159 @@ AudioFileTypeID guessFileTypeHint(const void *data, size_t size)
116
118
  }
117
119
  // NOLINTEND(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers)
118
120
 
121
+ using ChannelLayoutStorage = std::vector<std::byte>;
122
+
123
+ AudioChannelLayout *asChannelLayout(ChannelLayoutStorage &storage)
124
+ {
125
+ return reinterpret_cast<AudioChannelLayout *>(storage.data());
126
+ }
127
+
128
+ // Reads a variable-size AudioChannelLayout property through a GetPropertyInfo / GetProperty pair.
129
+ template <typename GetInfo, typename Get>
130
+ std::optional<ChannelLayoutStorage> readChannelLayout(GetInfo &&getInfo, Get &&get)
131
+ {
132
+ UInt32 size = 0;
133
+ if (getInfo(&size) != noErr || size < sizeof(AudioChannelLayout)) {
134
+ return std::nullopt;
135
+ }
136
+ ChannelLayoutStorage storage(size);
137
+ if (get(&size, storage.data()) != noErr) {
138
+ return std::nullopt;
139
+ }
140
+ return storage;
141
+ }
142
+
143
+ // The layout is not always published by ExtAudioFile itself: for MP4/AAC it lives on
144
+ // the wrapped AudioFile or only on the codec converter ExtAudioFile builds internally.
145
+ std::optional<ChannelLayoutStorage> readFileChannelLayout(ExtAudioFileRef extFile)
146
+ {
147
+ if (auto layout = readChannelLayout(
148
+ [&](UInt32 *size) {
149
+ return ExtAudioFileGetPropertyInfo(
150
+ extFile, kExtAudioFileProperty_FileChannelLayout, size, nullptr);
151
+ },
152
+ [&](UInt32 *size, void *data) {
153
+ return ExtAudioFileGetProperty(
154
+ extFile, kExtAudioFileProperty_FileChannelLayout, size, data);
155
+ })) {
156
+ return layout;
157
+ }
158
+
159
+ AudioFileID audioFile = nullptr;
160
+ UInt32 audioFileSize = sizeof(audioFile);
161
+ if (ExtAudioFileGetProperty(
162
+ extFile, kExtAudioFileProperty_AudioFile, &audioFileSize, &audioFile) == noErr &&
163
+ audioFile != nullptr) {
164
+ if (auto layout = readChannelLayout(
165
+ [&](UInt32 *size) {
166
+ return AudioFileGetPropertyInfo(
167
+ audioFile, kAudioFilePropertyChannelLayout, size, nullptr);
168
+ },
169
+ [&](UInt32 *size, void *data) {
170
+ return AudioFileGetProperty(audioFile, kAudioFilePropertyChannelLayout, size, data);
171
+ })) {
172
+ return layout;
173
+ }
174
+ }
175
+
176
+ AudioConverterRef converter = nullptr;
177
+ UInt32 converterSize = sizeof(converter);
178
+ if (ExtAudioFileGetProperty(
179
+ extFile, kExtAudioFileProperty_AudioConverter, &converterSize, &converter) == noErr &&
180
+ converter != nullptr) {
181
+ if (auto layout = readChannelLayout(
182
+ [&](UInt32 *size) {
183
+ return AudioConverterGetPropertyInfo(
184
+ converter, kAudioConverterInputChannelLayout, size, nullptr);
185
+ },
186
+ [&](UInt32 *size, void *data) {
187
+ return AudioConverterGetProperty(
188
+ converter, kAudioConverterInputChannelLayout, size, data);
189
+ })) {
190
+ return layout;
191
+ }
192
+ }
193
+ return std::nullopt;
194
+ }
195
+
196
+ // Expands a tag- or bitmap-described layout into explicit per-channel descriptions.
197
+ std::optional<ChannelLayoutStorage> expandToChannelDescriptions(ChannelLayoutStorage layout)
198
+ {
199
+ const AudioChannelLayout *source = asChannelLayout(layout);
200
+ if (source->mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelDescriptions) {
201
+ return layout;
202
+ }
203
+
204
+ const bool usesBitmap = source->mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelBitmap;
205
+ const AudioFormatPropertyID property = usesBitmap ? kAudioFormatProperty_ChannelLayoutForBitmap
206
+ : kAudioFormatProperty_ChannelLayoutForTag;
207
+ const void *specifier = usesBitmap ? static_cast<const void *>(&source->mChannelBitmap)
208
+ : static_cast<const void *>(&source->mChannelLayoutTag);
209
+ const UInt32 specifierSize =
210
+ usesBitmap ? sizeof(source->mChannelBitmap) : sizeof(source->mChannelLayoutTag);
211
+
212
+ UInt32 size = 0;
213
+ if (AudioFormatGetPropertyInfo(property, specifierSize, specifier, &size) != noErr ||
214
+ size < sizeof(AudioChannelLayout)) {
215
+ return std::nullopt;
216
+ }
217
+ ChannelLayoutStorage expanded(size);
218
+ if (AudioFormatGetProperty(property, specifierSize, specifier, &size, expanded.data()) != noErr) {
219
+ return std::nullopt;
220
+ }
221
+ return expanded;
222
+ }
223
+
224
+ bool isSpeakerLabel(AudioChannelLabel label)
225
+ {
226
+ return label != kAudioChannelLabel_Unknown && label != kAudioChannelLabel_Unused &&
227
+ label < kAudioChannelLabel_Discrete;
228
+ }
229
+
230
+ // Decoded channels must follow the WAVE / Web Audio speaker order
231
+ // (L R C LFE Ls Rs …), but AudioToolbox emits them in the file's native order —
232
+ // C L R Ls Rs LFE for 5.1 AAC. Core Audio numbers its speaker labels in WAVE
233
+ // order, so sorting the file's channels by label yields the target layout, and
234
+ // ExtAudioFile reorders to it. Layouts without speaker positions stay in file order.
235
+ void requestWebAudioChannelOrder(ExtAudioFileRef extFile, int channels)
236
+ {
237
+ if (channels <= 2) {
238
+ return;
239
+ }
240
+ auto fileLayout = readFileChannelLayout(extFile);
241
+ if (!fileLayout) {
242
+ return;
243
+ }
244
+ auto described = expandToChannelDescriptions(std::move(*fileLayout));
245
+ if (!described) {
246
+ return;
247
+ }
248
+
249
+ AudioChannelLayout *layout = asChannelLayout(*described);
250
+ if (layout->mNumberChannelDescriptions != static_cast<UInt32>(channels)) {
251
+ return;
252
+ }
253
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
254
+ std::span descriptions(layout->mChannelDescriptions, layout->mNumberChannelDescriptions);
255
+ const bool allSpeakers = std::ranges::all_of(descriptions, [](const auto &description) {
256
+ return isSpeakerLabel(description.mChannelLabel);
257
+ });
258
+ if (!allSpeakers ||
259
+ std::ranges::is_sorted(descriptions, {}, &AudioChannelDescription::mChannelLabel)) {
260
+ return;
261
+ }
262
+
263
+ std::ranges::sort(descriptions, {}, &AudioChannelDescription::mChannelLabel);
264
+ layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
265
+ layout->mChannelBitmap = 0;
266
+ // Best effort: on failure the channels simply keep the file order.
267
+ ExtAudioFileSetProperty(
268
+ extFile,
269
+ kExtAudioFileProperty_ClientChannelLayout,
270
+ static_cast<UInt32>(described->size()),
271
+ described->data());
272
+ }
273
+
119
274
  } // namespace
120
275
 
121
276
  IOSDecoder::IOSDecoder() = default;
@@ -165,6 +320,8 @@ decoding::DecoderResult IOSDecoder::finishOpeningDecoder(
165
320
  return Err(
166
321
  "IOSDecoder: ExtAudioFileSetProperty(ClientDataFormat) failed: " + std::to_string(status));
167
322
  }
323
+ // After ClientDataFormat: setting the client format resets the client channel layout.
324
+ requestWebAudioChannelOrder(extFile, channels);
168
325
 
169
326
  SInt64 fileLengthFrames = 0;
170
327
  propSize = sizeof(fileLengthFrames);
@@ -1,6 +1,7 @@
1
1
  #import <audioapi/ios/system/AudioEngine.h>
2
2
  #import <audioapi/ios/system/AudioSessionManager.h>
3
3
 
4
+ #include <audioapi/core/inputs/ActiveRecorderHandle.h>
4
5
  #include <mutex>
5
6
 
6
7
  @interface AudioEngineSourceRegistration : NSObject
@@ -149,6 +150,9 @@ static AudioEngine *_sharedInstance = nil;
149
150
 
150
151
  - (void)cleanup
151
152
  {
153
+ // outside of lock because it locks on its own
154
+ (void)audioapi::ActiveRecorderHandle::global().stopAndReturnInfo();
155
+
152
156
  std::scoped_lock lock(_engineLock);
153
157
  [self destroyAudioEngine];
154
158
  self.state = AudioEngineState::AudioEngineStateIdle;
@@ -70,7 +70,7 @@ static AudioSessionManager *_sharedInstance = nil;
70
70
 
71
71
  - (void)setPreferredIOBufferFrames:(int)frames
72
72
  {
73
- if (!self.shouldManageSession) {
73
+ if (!self.shouldManageSession || self.audioSession == nil) {
74
74
  return;
75
75
  }
76
76
 
@@ -7,6 +7,8 @@ exports.default = useAudioInput;
7
7
  var _react = require("react");
8
8
  var _api = require("../api");
9
9
  const meaningfulReasons = ['NewDeviceAvailable', 'OldDeviceUnavailable',
10
+ // iOS reports a disconnected Bluetooth headset that was the current input this way not old device unavailable
11
+ 'Override',
10
12
  // e.g. system picks a different device as current one is not suitable for the new configuration
11
13
  'CategoryChange', 'ConfigurationChange'];
12
14
 
@@ -1 +1 @@
1
- {"version":3,"names":["_react","require","_api","meaningfulReasons","useAudioInput","availableInputs","setAvailableInputs","useState","currentInput","setCurrentInput","selectInput","useCallback","device","selection","id","AudioManager","setInputDevice","devicesInfo","getDevicesInfo","useEffect","fetchAvailableInputs","audioDevices","currentDeviceId","currentInputs","length","handleRouteChange","event","includes","reason","sub","addSystemEventListener","remove","useMemo","find","d"],"sourceRoot":"../../../src","sources":["hooks/useAudioInput.ts"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAEA,IAAAC,IAAA,GAAAD,OAAA;AAQA,MAAME,iBAAsC,GAAG,CAC7C,oBAAoB,EACpB,sBAAsB;AACtB;AACA,gBAAgB,EAChB,qBAAqB,CACtB;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAASC,aAAaA,CAAA,EAAG;EACtC,MAAM,CAACC,eAAe,EAAEC,kBAAkB,CAAC,GAAG,IAAAC,eAAQ,EAAkB,EAAE,CAAC;EAC3E,MAAM,CAACC,YAAY,EAAEC,eAAe,CAAC,GAAG,IAAAF,eAAQ,EAAgB,IAAI,CAAC;EAErE,MAAMG,WAAW,GAAG,IAAAC,kBAAW,EAC7B,MAAOC,MAAmC,IAAK;IAC7C,MAAMC,SAAS,GAAGD,MAAM,KAAK,SAAS,GAAG,SAAS,GAAGA,MAAM,CAACE,EAAE;IAE9D,MAAMC,iBAAY,CAACC,cAAc,CAACH,SAAS,CAAC;IAC5CJ,eAAe,CAACI,SAAS,KAAK,SAAS,GAAG,IAAI,GAAGA,SAAS,CAAC;IAE3D,MAAMI,WAA6B,GAAG,MAAMF,iBAAY,CAACG,cAAc,CAAC,CAAC;IACzEZ,kBAAkB,CAACW,WAAW,CAACZ,eAAe,CAAC;EACjD,CAAC,EACD,EACF,CAAC;EAED,IAAAc,gBAAS,EAAC,MAAM;IACd,eAAeC,oBAAoBA,CAAA,EAAG;MACpC,MAAMC,YAAY,GAAG,MAAMN,iBAAY,CAACG,cAAc,CAAC,CAAC;MACxD,MAAMI,eAAe,GAAGD,YAAY,CAACE,aAAa,CAACC,MAAM,GACrDH,YAAY,CAACE,aAAa,CAAC,CAAC,CAAC,CAACT,EAAE,GAChC,IAAI;MAERR,kBAAkB,CAACe,YAAY,CAAChB,eAAe,CAAC;MAChDI,eAAe,CAACa,eAAe,CAAC;IAClC;IAEA,eAAeG,iBAAiBA,CAACC,KAA6B,EAAE;MAC9D,IAAI,CAACvB,iBAAiB,CAACwB,QAAQ,CAACD,KAAK,CAACE,MAAM,CAAC,EAAE;QAC7C;MACF;MAEA,MAAMR,oBAAoB,CAAC,CAAC;IAC9B;IAEA,MAAMS,GAAG,GAAGd,iBAAY,CAACe,sBAAsB,CAC7C,aAAa,EACbL,iBACF,CAAC;IAEDL,oBAAoB,CAAC,CAAC;IACtB,OAAO,MAAM;MACXS,GAAG,EAAEE,MAAM,CAAC,CAAC;IACf,CAAC;EACH,CAAC,EAAE,EAAE,CAAC;EAEN,OAAO,IAAAC,cAAO,EACZ,OAAO;IACL;AACN;AACA;AACA;IACM3B,eAAe;IACf;AACN;AACA;AACA;IACMG,YAAY,EAAEH,eAAe,CAAC4B,IAAI,CAAEC,CAAC,IAAKA,CAAC,CAACpB,EAAE,KAAKN,YAAY,CAAC,IAAI,IAAI;IACxE;AACN;AACA;AACA;AACA;IACME;EACF,CAAC,CAAC,EACF,CAACL,eAAe,EAAEG,YAAY,EAAEE,WAAW,CAC7C,CAAC;AACH","ignoreList":[]}
1
+ {"version":3,"names":["_react","require","_api","meaningfulReasons","useAudioInput","availableInputs","setAvailableInputs","useState","currentInput","setCurrentInput","selectInput","useCallback","device","selection","id","AudioManager","setInputDevice","devicesInfo","getDevicesInfo","useEffect","fetchAvailableInputs","audioDevices","currentDeviceId","currentInputs","length","handleRouteChange","event","includes","reason","sub","addSystemEventListener","remove","useMemo","find","d"],"sourceRoot":"../../../src","sources":["hooks/useAudioInput.ts"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAEA,IAAAC,IAAA,GAAAD,OAAA;AAQA,MAAME,iBAAsC,GAAG,CAC7C,oBAAoB,EACpB,sBAAsB;AACtB;AACA,UAAU;AACV;AACA,gBAAgB,EAChB,qBAAqB,CACtB;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAASC,aAAaA,CAAA,EAAG;EACtC,MAAM,CAACC,eAAe,EAAEC,kBAAkB,CAAC,GAAG,IAAAC,eAAQ,EAAkB,EAAE,CAAC;EAC3E,MAAM,CAACC,YAAY,EAAEC,eAAe,CAAC,GAAG,IAAAF,eAAQ,EAAgB,IAAI,CAAC;EAErE,MAAMG,WAAW,GAAG,IAAAC,kBAAW,EAC7B,MAAOC,MAAmC,IAAK;IAC7C,MAAMC,SAAS,GAAGD,MAAM,KAAK,SAAS,GAAG,SAAS,GAAGA,MAAM,CAACE,EAAE;IAE9D,MAAMC,iBAAY,CAACC,cAAc,CAACH,SAAS,CAAC;IAC5CJ,eAAe,CAACI,SAAS,KAAK,SAAS,GAAG,IAAI,GAAGA,SAAS,CAAC;IAE3D,MAAMI,WAA6B,GAAG,MAAMF,iBAAY,CAACG,cAAc,CAAC,CAAC;IACzEZ,kBAAkB,CAACW,WAAW,CAACZ,eAAe,CAAC;EACjD,CAAC,EACD,EACF,CAAC;EAED,IAAAc,gBAAS,EAAC,MAAM;IACd,eAAeC,oBAAoBA,CAAA,EAAG;MACpC,MAAMC,YAAY,GAAG,MAAMN,iBAAY,CAACG,cAAc,CAAC,CAAC;MACxD,MAAMI,eAAe,GAAGD,YAAY,CAACE,aAAa,CAACC,MAAM,GACrDH,YAAY,CAACE,aAAa,CAAC,CAAC,CAAC,CAACT,EAAE,GAChC,IAAI;MAERR,kBAAkB,CAACe,YAAY,CAAChB,eAAe,CAAC;MAChDI,eAAe,CAACa,eAAe,CAAC;IAClC;IAEA,eAAeG,iBAAiBA,CAACC,KAA6B,EAAE;MAC9D,IAAI,CAACvB,iBAAiB,CAACwB,QAAQ,CAACD,KAAK,CAACE,MAAM,CAAC,EAAE;QAC7C;MACF;MAEA,MAAMR,oBAAoB,CAAC,CAAC;IAC9B;IAEA,MAAMS,GAAG,GAAGd,iBAAY,CAACe,sBAAsB,CAC7C,aAAa,EACbL,iBACF,CAAC;IAEDL,oBAAoB,CAAC,CAAC;IACtB,OAAO,MAAM;MACXS,GAAG,EAAEE,MAAM,CAAC,CAAC;IACf,CAAC;EACH,CAAC,EAAE,EAAE,CAAC;EAEN,OAAO,IAAAC,cAAO,EACZ,OAAO;IACL;AACN;AACA;AACA;IACM3B,eAAe;IACf;AACN;AACA;AACA;IACMG,YAAY,EAAEH,eAAe,CAAC4B,IAAI,CAAEC,CAAC,IAAKA,CAAC,CAACpB,EAAE,KAAKN,YAAY,CAAC,IAAI,IAAI;IACxE;AACN;AACA;AACA;AACA;IACME;EACF,CAAC,CAAC,EACF,CAACL,eAAe,EAAEG,YAAY,EAAEE,WAAW,CAC7C,CAAC;AACH","ignoreList":[]}
@@ -3,6 +3,8 @@
3
3
  import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import { AudioManager } from '../api';
5
5
  const meaningfulReasons = ['NewDeviceAvailable', 'OldDeviceUnavailable',
6
+ // iOS reports a disconnected Bluetooth headset that was the current input this way not old device unavailable
7
+ 'Override',
6
8
  // e.g. system picks a different device as current one is not suitable for the new configuration
7
9
  'CategoryChange', 'ConfigurationChange'];
8
10
 
@@ -1 +1 @@
1
- {"version":3,"names":["useCallback","useEffect","useMemo","useState","AudioManager","meaningfulReasons","useAudioInput","availableInputs","setAvailableInputs","currentInput","setCurrentInput","selectInput","device","selection","id","setInputDevice","devicesInfo","getDevicesInfo","fetchAvailableInputs","audioDevices","currentDeviceId","currentInputs","length","handleRouteChange","event","includes","reason","sub","addSystemEventListener","remove","find","d"],"sourceRoot":"../../../src","sources":["hooks/useAudioInput.ts"],"mappings":";;AAAA,SAASA,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,OAAO;AAEjE,SAASC,YAAY,QAAQ,QAAQ;AAQrC,MAAMC,iBAAsC,GAAG,CAC7C,oBAAoB,EACpB,sBAAsB;AACtB;AACA,gBAAgB,EAChB,qBAAqB,CACtB;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAe,SAASC,aAAaA,CAAA,EAAG;EACtC,MAAM,CAACC,eAAe,EAAEC,kBAAkB,CAAC,GAAGL,QAAQ,CAAkB,EAAE,CAAC;EAC3E,MAAM,CAACM,YAAY,EAAEC,eAAe,CAAC,GAAGP,QAAQ,CAAgB,IAAI,CAAC;EAErE,MAAMQ,WAAW,GAAGX,WAAW,CAC7B,MAAOY,MAAmC,IAAK;IAC7C,MAAMC,SAAS,GAAGD,MAAM,KAAK,SAAS,GAAG,SAAS,GAAGA,MAAM,CAACE,EAAE;IAE9D,MAAMV,YAAY,CAACW,cAAc,CAACF,SAAS,CAAC;IAC5CH,eAAe,CAACG,SAAS,KAAK,SAAS,GAAG,IAAI,GAAGA,SAAS,CAAC;IAE3D,MAAMG,WAA6B,GAAG,MAAMZ,YAAY,CAACa,cAAc,CAAC,CAAC;IACzET,kBAAkB,CAACQ,WAAW,CAACT,eAAe,CAAC;EACjD,CAAC,EACD,EACF,CAAC;EAEDN,SAAS,CAAC,MAAM;IACd,eAAeiB,oBAAoBA,CAAA,EAAG;MACpC,MAAMC,YAAY,GAAG,MAAMf,YAAY,CAACa,cAAc,CAAC,CAAC;MACxD,MAAMG,eAAe,GAAGD,YAAY,CAACE,aAAa,CAACC,MAAM,GACrDH,YAAY,CAACE,aAAa,CAAC,CAAC,CAAC,CAACP,EAAE,GAChC,IAAI;MAERN,kBAAkB,CAACW,YAAY,CAACZ,eAAe,CAAC;MAChDG,eAAe,CAACU,eAAe,CAAC;IAClC;IAEA,eAAeG,iBAAiBA,CAACC,KAA6B,EAAE;MAC9D,IAAI,CAACnB,iBAAiB,CAACoB,QAAQ,CAACD,KAAK,CAACE,MAAM,CAAC,EAAE;QAC7C;MACF;MAEA,MAAMR,oBAAoB,CAAC,CAAC;IAC9B;IAEA,MAAMS,GAAG,GAAGvB,YAAY,CAACwB,sBAAsB,CAC7C,aAAa,EACbL,iBACF,CAAC;IAEDL,oBAAoB,CAAC,CAAC;IACtB,OAAO,MAAM;MACXS,GAAG,EAAEE,MAAM,CAAC,CAAC;IACf,CAAC;EACH,CAAC,EAAE,EAAE,CAAC;EAEN,OAAO3B,OAAO,CACZ,OAAO;IACL;AACN;AACA;AACA;IACMK,eAAe;IACf;AACN;AACA;AACA;IACME,YAAY,EAAEF,eAAe,CAACuB,IAAI,CAAEC,CAAC,IAAKA,CAAC,CAACjB,EAAE,KAAKL,YAAY,CAAC,IAAI,IAAI;IACxE;AACN;AACA;AACA;AACA;IACME;EACF,CAAC,CAAC,EACF,CAACJ,eAAe,EAAEE,YAAY,EAAEE,WAAW,CAC7C,CAAC;AACH","ignoreList":[]}
1
+ {"version":3,"names":["useCallback","useEffect","useMemo","useState","AudioManager","meaningfulReasons","useAudioInput","availableInputs","setAvailableInputs","currentInput","setCurrentInput","selectInput","device","selection","id","setInputDevice","devicesInfo","getDevicesInfo","fetchAvailableInputs","audioDevices","currentDeviceId","currentInputs","length","handleRouteChange","event","includes","reason","sub","addSystemEventListener","remove","find","d"],"sourceRoot":"../../../src","sources":["hooks/useAudioInput.ts"],"mappings":";;AAAA,SAASA,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,OAAO;AAEjE,SAASC,YAAY,QAAQ,QAAQ;AAQrC,MAAMC,iBAAsC,GAAG,CAC7C,oBAAoB,EACpB,sBAAsB;AACtB;AACA,UAAU;AACV;AACA,gBAAgB,EAChB,qBAAqB,CACtB;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAe,SAASC,aAAaA,CAAA,EAAG;EACtC,MAAM,CAACC,eAAe,EAAEC,kBAAkB,CAAC,GAAGL,QAAQ,CAAkB,EAAE,CAAC;EAC3E,MAAM,CAACM,YAAY,EAAEC,eAAe,CAAC,GAAGP,QAAQ,CAAgB,IAAI,CAAC;EAErE,MAAMQ,WAAW,GAAGX,WAAW,CAC7B,MAAOY,MAAmC,IAAK;IAC7C,MAAMC,SAAS,GAAGD,MAAM,KAAK,SAAS,GAAG,SAAS,GAAGA,MAAM,CAACE,EAAE;IAE9D,MAAMV,YAAY,CAACW,cAAc,CAACF,SAAS,CAAC;IAC5CH,eAAe,CAACG,SAAS,KAAK,SAAS,GAAG,IAAI,GAAGA,SAAS,CAAC;IAE3D,MAAMG,WAA6B,GAAG,MAAMZ,YAAY,CAACa,cAAc,CAAC,CAAC;IACzET,kBAAkB,CAACQ,WAAW,CAACT,eAAe,CAAC;EACjD,CAAC,EACD,EACF,CAAC;EAEDN,SAAS,CAAC,MAAM;IACd,eAAeiB,oBAAoBA,CAAA,EAAG;MACpC,MAAMC,YAAY,GAAG,MAAMf,YAAY,CAACa,cAAc,CAAC,CAAC;MACxD,MAAMG,eAAe,GAAGD,YAAY,CAACE,aAAa,CAACC,MAAM,GACrDH,YAAY,CAACE,aAAa,CAAC,CAAC,CAAC,CAACP,EAAE,GAChC,IAAI;MAERN,kBAAkB,CAACW,YAAY,CAACZ,eAAe,CAAC;MAChDG,eAAe,CAACU,eAAe,CAAC;IAClC;IAEA,eAAeG,iBAAiBA,CAACC,KAA6B,EAAE;MAC9D,IAAI,CAACnB,iBAAiB,CAACoB,QAAQ,CAACD,KAAK,CAACE,MAAM,CAAC,EAAE;QAC7C;MACF;MAEA,MAAMR,oBAAoB,CAAC,CAAC;IAC9B;IAEA,MAAMS,GAAG,GAAGvB,YAAY,CAACwB,sBAAsB,CAC7C,aAAa,EACbL,iBACF,CAAC;IAEDL,oBAAoB,CAAC,CAAC;IACtB,OAAO,MAAM;MACXS,GAAG,EAAEE,MAAM,CAAC,CAAC;IACf,CAAC;EACH,CAAC,EAAE,EAAE,CAAC;EAEN,OAAO3B,OAAO,CACZ,OAAO;IACL;AACN;AACA;AACA;IACMK,eAAe;IACf;AACN;AACA;AACA;IACME,YAAY,EAAEF,eAAe,CAACuB,IAAI,CAAEC,CAAC,IAAKA,CAAC,CAACjB,EAAE,KAAKL,YAAY,CAAC,IAAI,IAAI;IACxE;AACN;AACA;AACA;AACA;IACME;EACF,CAAC,CAAC,EACF,CAACJ,eAAe,EAAEE,YAAY,EAAEE,WAAW,CAC7C,CAAC;AACH","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"useAudioInput.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useAudioInput.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,eAAe,EAEhB,MAAM,iBAAiB,CAAC;AAUzB;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa;IAiD/B;;;OAGG;;IAEH;;;OAGG;;IAEH;;;;OAIG;0BA1DU,eAAe,GAAG,SAAS;EA+D7C"}
1
+ {"version":3,"file":"useAudioInput.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useAudioInput.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,eAAe,EAEhB,MAAM,iBAAiB,CAAC;AAYzB;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa;IAiD/B;;;OAGG;;IAEH;;;OAGG;;IAEH;;;;OAIG;0BA1DU,eAAe,GAAG,SAAS;EA+D7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"useAudioInput.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useAudioInput.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,eAAe,EAEhB,MAAM,iBAAiB,CAAC;AAUzB;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa;IAiD/B;;;OAGG;;IAEH;;;OAGG;;IAEH;;;;OAIG;0BA1DU,eAAe,GAAG,SAAS;EA+D7C"}
1
+ {"version":3,"file":"useAudioInput.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useAudioInput.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,eAAe,EAEhB,MAAM,iBAAiB,CAAC;AAYzB;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa;IAiD/B;;;OAGG;;IAEH;;;OAGG;;IAEH;;;;OAIG;0BA1DU,eAAe,GAAG,SAAS;EA+D7C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-audio-api",
3
- "version": "1.0.0-nightly-6fd4c68-20261102",
3
+ "version": "1.0.0",
4
4
  "description": "react-native-audio-api provides system for controlling audio in React Native environment compatible with Web Audio API specification",
5
5
  "bin": {
6
6
  "setup-rn-audio-api-web": "./scripts/setup-rn-audio-api-web.js"
@@ -11,6 +11,8 @@ import {
11
11
  const meaningfulReasons: RouteChangeReason[] = [
12
12
  'NewDeviceAvailable',
13
13
  'OldDeviceUnavailable',
14
+ // iOS reports a disconnected Bluetooth headset that was the current input this way not old device unavailable
15
+ 'Override',
14
16
  // e.g. system picks a different device as current one is not suitable for the new configuration
15
17
  'CategoryChange',
16
18
  'ConfigurationChange',