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.
- package/android/src/main/cpp/audioapi/CMakeLists.txt +13 -0
- package/android/src/main/cpp/audioapi/android/AndroidDecoding.cpp +366 -53
- package/android/src/main/cpp/audioapi/android/AndroidDecoding.h +6 -1
- package/android/src/main/cpp/audioapi/android/ThroughputHintSession.cpp +51 -0
- package/android/src/main/cpp/audioapi/android/ThroughputHintSession.h +40 -0
- package/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +3 -0
- package/common/cpp/audioapi/decoding/AudioDecoding.cpp +47 -13
- package/common/cpp/audioapi/decoding/backends/AudioDecoderBackend.h +4 -0
- package/common/cpp/audioapi/jsi/RuntimeInstanceCache.hpp +20 -14
- package/common/cpp/audioapi/jsi/RuntimeObserver.cpp +23 -11
- package/common/cpp/audioapi/jsi/RuntimeObserver.h +2 -0
- package/common/cpp/audioapi/libs/miniaudio/decoders/libvorbis/miniaudio_libvorbis.c +5 -2
- package/common/cpp/audioapi/utils/AudioArray.hpp +11 -0
- package/common/cpp/audioapi/utils/AudioBuffer.hpp +11 -0
- package/ios/audioapi/ios/core/utils/IOSDecoding.mm +157 -0
- package/ios/audioapi/ios/system/AudioEngine.mm +4 -0
- package/ios/audioapi/ios/system/AudioSessionManager.mm +1 -1
- package/lib/commonjs/hooks/useAudioInput.js +2 -0
- package/lib/commonjs/hooks/useAudioInput.js.map +1 -1
- package/lib/module/hooks/useAudioInput.js +2 -0
- package/lib/module/hooks/useAudioInput.js.map +1 -1
- package/lib/typescript/commonjs/hooks/useAudioInput.d.ts.map +1 -1
- package/lib/typescript/module/hooks/useAudioInput.d.ts.map +1 -1
- package/package.json +1 -1
- 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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 (
|
|
209
|
-
state.nativeSampleRate =
|
|
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
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 (
|
|
100
|
+
if (framesDecoded == 0) {
|
|
72
101
|
return Err("Failed to decode any frames");
|
|
73
102
|
}
|
|
74
103
|
|
|
75
|
-
const size_t
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
28
|
+
std::scoped_lock lock(runtimeCachesMutex_);
|
|
28
29
|
runtimeCaches_.erase(rt);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
~RuntimeInstanceCache() override {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
/*
|
|
486
|
-
|
|
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;
|
|
@@ -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;
|
|
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;
|
|
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
|
|
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',
|