react-native-audio-api 0.13.2 → 0.13.4
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/RNAudioAPI.podspec +53 -16
- package/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt +1 -0
- package/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp +8 -2
- package/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.h +1 -0
- package/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp +2 -0
- package/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp +15 -1
- package/common/cpp/audioapi/core/sources/AudioFileSourceNode.h +19 -0
- package/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp +12 -1
- package/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +7 -0
- package/common/cpp/audioapi/core/utils/param/ParamQueueBase.hpp +1 -1
- package/common/cpp/audioapi/core/utils/param/ParamRenderQueue.cpp +35 -12
- package/common/cpp/audioapi/core/utils/param/ParamRenderQueue.h +9 -0
- package/common/cpp/audioapi/core/utils/worklets/SafeIncludes.h +3 -3
- package/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp +2 -2
- package/common/cpp/audioapi/events/AudioEvent.h +1 -0
- package/common/cpp/audioapi/events/AudioEventPayload.h +11 -0
- package/common/cpp/audioapi/events/AudioEventPayloadMapping.h +1 -0
- package/common/cpp/audioapi/utils/events/BufferingStateDispatcher.cpp +49 -0
- package/common/cpp/audioapi/utils/events/BufferingStateDispatcher.h +42 -0
- package/ios/audioapi/ios/core/IOSAudioRecorder.h +12 -1
- package/ios/audioapi/ios/core/IOSAudioRecorder.mm +179 -63
- package/ios/audioapi/ios/core/NativeAudioRecorder.h +2 -0
- package/ios/audioapi/ios/core/NativeAudioRecorder.m +37 -12
- package/ios/audioapi/ios/core/utils/IOSFileWriter.h +3 -0
- package/ios/audioapi/ios/core/utils/IOSFileWriter.mm +17 -0
- package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.h +3 -0
- package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm +21 -0
- package/ios/audioapi/ios/system/AudioEngine.h +2 -1
- package/ios/audioapi/ios/system/AudioEngine.mm +49 -23
- package/ios/audioapi/ios/system/SystemNotificationManager.mm +10 -0
- package/lib/commonjs/Audio/Audio.js +30 -3
- package/lib/commonjs/Audio/Audio.js.map +1 -1
- package/lib/commonjs/Audio/Audio.web.js +21 -3
- package/lib/commonjs/Audio/Audio.web.js.map +1 -1
- package/lib/commonjs/Audio/AudioFileSourceNode.js +20 -0
- package/lib/commonjs/Audio/AudioFileSourceNode.js.map +1 -1
- package/lib/commonjs/Audio/controls/AudioControls.js +90 -94
- package/lib/commonjs/Audio/controls/AudioControls.js.map +1 -1
- package/lib/commonjs/Audio/controls/audioControlUtils.js +48 -18
- package/lib/commonjs/Audio/controls/audioControlUtils.js.map +1 -1
- package/lib/commonjs/Audio/index.js +7 -0
- package/lib/commonjs/Audio/index.js.map +1 -1
- package/lib/commonjs/Audio/utils.js +16 -5
- package/lib/commonjs/Audio/utils.js.map +1 -1
- package/lib/commonjs/AudioAPIModule/AudioAPIModule.js +1 -1
- package/lib/commonjs/core/AudioDecoder.js +4 -1
- package/lib/commonjs/core/AudioDecoder.js.map +1 -1
- package/lib/commonjs/core/AudioParam.js +1 -0
- package/lib/commonjs/core/AudioParam.js.map +1 -1
- package/lib/module/Audio/Audio.js +30 -3
- package/lib/module/Audio/Audio.js.map +1 -1
- package/lib/module/Audio/Audio.web.js +21 -3
- package/lib/module/Audio/Audio.web.js.map +1 -1
- package/lib/module/Audio/AudioFileSourceNode.js +20 -0
- package/lib/module/Audio/AudioFileSourceNode.js.map +1 -1
- package/lib/module/Audio/controls/AudioControls.js +92 -96
- package/lib/module/Audio/controls/AudioControls.js.map +1 -1
- package/lib/module/Audio/controls/audioControlUtils.js +48 -19
- package/lib/module/Audio/controls/audioControlUtils.js.map +1 -1
- package/lib/module/Audio/index.js +1 -0
- package/lib/module/Audio/index.js.map +1 -1
- package/lib/module/Audio/utils.js +15 -5
- package/lib/module/Audio/utils.js.map +1 -1
- package/lib/module/AudioAPIModule/AudioAPIModule.js +1 -1
- package/lib/module/core/AudioDecoder.js +4 -1
- package/lib/module/core/AudioDecoder.js.map +1 -1
- package/lib/module/core/AudioParam.js +1 -0
- package/lib/module/core/AudioParam.js.map +1 -1
- package/lib/typescript/Audio/Audio.d.ts.map +1 -1
- package/lib/typescript/Audio/Audio.web.d.ts.map +1 -1
- package/lib/typescript/Audio/AudioFileSourceNode.d.ts +2 -0
- package/lib/typescript/Audio/AudioFileSourceNode.d.ts.map +1 -1
- package/lib/typescript/Audio/controls/AudioControls.d.ts.map +1 -1
- package/lib/typescript/Audio/controls/audioControlUtils.d.ts +11 -4
- package/lib/typescript/Audio/controls/audioControlUtils.d.ts.map +1 -1
- package/lib/typescript/Audio/index.d.ts +1 -0
- package/lib/typescript/Audio/index.d.ts.map +1 -1
- package/lib/typescript/Audio/types.d.ts +15 -1
- package/lib/typescript/Audio/types.d.ts.map +1 -1
- package/lib/typescript/Audio/utils.d.ts +2 -1
- package/lib/typescript/Audio/utils.d.ts.map +1 -1
- package/lib/typescript/core/AudioDecoder.d.ts.map +1 -1
- package/lib/typescript/core/AudioParam.d.ts.map +1 -1
- package/lib/typescript/events/types.d.ts +5 -0
- package/lib/typescript/events/types.d.ts.map +1 -1
- package/lib/typescript/jsi-interfaces.d.ts +1 -0
- package/lib/typescript/jsi-interfaces.d.ts.map +1 -1
- package/package.json +8 -8
- package/scripts/download-prebuilt-binaries.sh +94 -6
- package/scripts/validate-worklets-version.js +1 -1
- package/src/Audio/Audio.tsx +38 -2
- package/src/Audio/Audio.web.tsx +26 -2
- package/src/Audio/AudioFileSourceNode.ts +28 -0
- package/src/Audio/controls/AudioControls.tsx +117 -116
- package/src/Audio/controls/audioControlUtils.ts +63 -31
- package/src/Audio/index.ts +1 -0
- package/src/Audio/types.ts +15 -1
- package/src/Audio/utils.ts +20 -2
- package/src/AudioAPIModule/AudioAPIModule.ts +1 -1
- package/src/core/AudioDecoder.ts +5 -3
- package/src/core/AudioParam.ts +1 -0
- package/src/events/types.ts +6 -0
- package/src/jsi-interfaces.ts +1 -0
package/RNAudioAPI.podspec
CHANGED
|
@@ -18,7 +18,7 @@ worklets_preprocessor_flag = worklets_enabled ? '-DRN_AUDIO_API_ENABLE_WORKLETS=
|
|
|
18
18
|
ffmpeg_flag = $RN_AUDIO_API_FFMPEG_DISABLED ? '-DRN_AUDIO_API_FFMPEG_DISABLED=1' : ''
|
|
19
19
|
static_external_libs_flag = $RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED ? '-DRN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED=1 -DMA_NO_LIBOPUS=1 -DMA_NO_LIBVORBIS=1' : ''
|
|
20
20
|
skip_ffmpeg_argument = $RN_AUDIO_API_FFMPEG_DISABLED ? 'skipffmpeg' : ''
|
|
21
|
-
|
|
21
|
+
download_script_env_prefix = $RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED ? 'DISABLE_AUDIOAPI_STATIC_EXTERNAL_LIBS=1 ' : ''
|
|
22
22
|
|
|
23
23
|
Pod::Spec.new do |s|
|
|
24
24
|
s.name = "RNAudioAPI"
|
|
@@ -67,18 +67,60 @@ Pod::Spec.new do |s|
|
|
|
67
67
|
|
|
68
68
|
s.prepare_command = <<-CMD
|
|
69
69
|
chmod +x scripts/download-prebuilt-binaries.sh
|
|
70
|
-
#{
|
|
70
|
+
#{download_script_env_prefix}scripts/download-prebuilt-binaries.sh ios #{skip_ffmpeg_argument}
|
|
71
71
|
CMD
|
|
72
72
|
|
|
73
73
|
external_dir_relative = "common/cpp/audioapi/external"
|
|
74
74
|
lib_dir = "$(PODS_ROOT)/#{$audio_api_config[:dynamic_frameworks_audio_api_dir]}/#{external_dir_relative}/$(PLATFORM_NAME)"
|
|
75
|
+
ffmpeg_dir = "$(PODS_ROOT)/#{$audio_api_config[:dynamic_frameworks_audio_api_dir]}/#{external_dir_relative}/ffmpeg_ios"
|
|
76
|
+
|
|
77
|
+
ffmpeg_framework_names = %w[libavcodec libavformat libavutil libswresample]
|
|
78
|
+
static_external_libs_names = %w[libopusfile libopus libogg libvorbis libvorbisenc libvorbisfile]
|
|
79
|
+
|
|
80
|
+
# `prepare_command` hydrates binaries on a fresh `pod install` so CocoaPods can
|
|
81
|
+
# register all vendored `.xcframework`s in `[CP] Copy XCFrameworks`. When `Pods/`
|
|
82
|
+
# is restored from cache while `node_modules/` is reinstalled, `prepare_command`
|
|
83
|
+
# does not re-run — the build-time phase below re-hydrates on every build.
|
|
84
|
+
#
|
|
85
|
+
# It must run at `:before_headers` so it lands ahead of CocoaPods' own
|
|
86
|
+
# `[CP] Copy XCFrameworks` phase, which consumes the FFmpeg xcframeworks. If it
|
|
87
|
+
# ran later (e.g. `:before_compile`, which is after Copy XCFrameworks), Xcode
|
|
88
|
+
# would reject the build when the xcframeworks are missing before we download.
|
|
89
|
+
#
|
|
90
|
+
# `output_files` declares everything the linker (`-force_load` static libs in
|
|
91
|
+
# `s.xcconfig`) and Copy XCFrameworks (FFmpeg) consume, so Xcode's build graph
|
|
92
|
+
# knows this phase produces them.
|
|
93
|
+
unless $RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED && $RN_AUDIO_API_FFMPEG_DISABLED
|
|
94
|
+
download_output_files = []
|
|
95
|
+
|
|
96
|
+
unless $RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED
|
|
97
|
+
static_external_libs_names.each do |lib|
|
|
98
|
+
download_output_files << "#{lib_dir}/#{lib}.a"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
unless $RN_AUDIO_API_FFMPEG_DISABLED
|
|
103
|
+
ffmpeg_framework_names.each do |framework|
|
|
104
|
+
download_output_files << "#{ffmpeg_dir}/#{framework}.xcframework"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
75
107
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
s.script_phase = {
|
|
109
|
+
:name => 'Download RNAudioAPI prebuilt binaries',
|
|
110
|
+
:execution_position => :before_headers,
|
|
111
|
+
:always_out_of_date => '1',
|
|
112
|
+
:output_files => download_output_files,
|
|
113
|
+
:script => <<-SCRIPT
|
|
114
|
+
cd "$PODS_TARGET_SRCROOT"
|
|
115
|
+
chmod +x scripts/download-prebuilt-binaries.sh
|
|
116
|
+
#{download_script_env_prefix}scripts/download-prebuilt-binaries.sh ios #{skip_ffmpeg_argument}
|
|
117
|
+
SCRIPT
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
s.ios.vendored_frameworks = $RN_AUDIO_API_FFMPEG_DISABLED ? [] : ffmpeg_framework_names.map { |framework|
|
|
122
|
+
"common/cpp/audioapi/external/ffmpeg_ios/#{framework}.xcframework"
|
|
123
|
+
}
|
|
82
124
|
|
|
83
125
|
s.pod_target_xcconfig = {
|
|
84
126
|
"USE_HEADERMAP" => "YES",
|
|
@@ -133,14 +175,9 @@ Pod::Spec.new do |s|
|
|
|
133
175
|
'OTHER_LDFLAGS' => %W[
|
|
134
176
|
$(inherited)
|
|
135
177
|
]
|
|
136
|
-
.concat($RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED ? [] :
|
|
137
|
-
-force_load #{lib_dir}
|
|
138
|
-
|
|
139
|
-
-force_load #{lib_dir}/libogg.a
|
|
140
|
-
-force_load #{lib_dir}/libvorbis.a
|
|
141
|
-
-force_load #{lib_dir}/libvorbisenc.a
|
|
142
|
-
-force_load #{lib_dir}/libvorbisfile.a
|
|
143
|
-
])
|
|
178
|
+
.concat($RN_AUDIO_API_STATIC_EXTERNAL_LIBS_DISABLED ? [] : static_external_libs_names.flat_map { |lib|
|
|
179
|
+
['-force_load', "#{lib_dir}/#{lib}.a"]
|
|
180
|
+
})
|
|
144
181
|
.join(" "),
|
|
145
182
|
}
|
|
146
183
|
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
|
@@ -33,6 +33,7 @@ AudioFileSourceNodeHostObject::AudioFileSourceNodeHostObject(
|
|
|
33
33
|
JSI_EXPORT_PROPERTY_GETTER(AudioFileSourceNodeHostObject, routedThroughMediaElement));
|
|
34
34
|
addSetters(
|
|
35
35
|
JSI_EXPORT_PROPERTY_SETTER(AudioFileSourceNodeHostObject, onPositionChanged),
|
|
36
|
+
JSI_EXPORT_PROPERTY_SETTER(AudioFileSourceNodeHostObject, onBufferingStateChanged),
|
|
36
37
|
JSI_EXPORT_PROPERTY_SETTER(AudioFileSourceNodeHostObject, volume),
|
|
37
38
|
JSI_EXPORT_PROPERTY_SETTER(AudioFileSourceNodeHostObject, playbackRate),
|
|
38
39
|
JSI_EXPORT_PROPERTY_SETTER(AudioFileSourceNodeHostObject, preservesPitch),
|
|
@@ -45,8 +46,8 @@ AudioFileSourceNodeHostObject::AudioFileSourceNodeHostObject(
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
AudioFileSourceNodeHostObject::~AudioFileSourceNodeHostObject() {
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
getAudioFileSourceNode()->assignOnPositionChangedCallbackId(0);
|
|
50
|
+
getAudioFileSourceNode()->assignOnBufferingStateChangeCallbackId(0);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
JSI_PROPERTY_GETTER_IMPL(AudioFileSourceNodeHostObject, volume) {
|
|
@@ -149,4 +150,9 @@ JSI_PROPERTY_SETTER_IMPL(AudioFileSourceNodeHostObject, onPositionChanged) {
|
|
|
149
150
|
std::stoull(value.getString(runtime).utf8(runtime)));
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
JSI_PROPERTY_SETTER_IMPL(AudioFileSourceNodeHostObject, onBufferingStateChanged) {
|
|
154
|
+
getAudioFileSourceNode()->assignOnBufferingStateChangeCallbackId(
|
|
155
|
+
std::stoull(value.getString(runtime).utf8(runtime)));
|
|
156
|
+
}
|
|
157
|
+
|
|
152
158
|
} // namespace audioapi
|
|
@@ -31,6 +31,7 @@ class AudioFileSourceNodeHostObject : public AudioScheduledSourceNodeHostObject
|
|
|
31
31
|
JSI_PROPERTY_SETTER_DECL(preservesPitch);
|
|
32
32
|
JSI_PROPERTY_SETTER_DECL(loop);
|
|
33
33
|
JSI_PROPERTY_SETTER_DECL(onPositionChanged);
|
|
34
|
+
JSI_PROPERTY_SETTER_DECL(onBufferingStateChanged);
|
|
34
35
|
|
|
35
36
|
JSI_HOST_FUNCTION_DECL(pause);
|
|
36
37
|
JSI_HOST_FUNCTION_DECL(seekToStart);
|
|
@@ -148,6 +148,8 @@ AudioEvent audioEventFromString(const std::string &event) {
|
|
|
148
148
|
return AudioEvent::BUFFER_ENDED;
|
|
149
149
|
if (event == "recorderError")
|
|
150
150
|
return AudioEvent::RECORDER_ERROR;
|
|
151
|
+
if (event == "bufferingStateChanged")
|
|
152
|
+
return AudioEvent::BUFFERING_STATE_CHANGE;
|
|
151
153
|
|
|
152
154
|
throw std::invalid_argument("Unknown audio event: " + event);
|
|
153
155
|
}
|
|
@@ -37,7 +37,10 @@ AudioFileSourceNode::AudioFileSourceNode(
|
|
|
37
37
|
positionChanged_(
|
|
38
38
|
context->getAudioEventHandlerRegistry(),
|
|
39
39
|
static_cast<int>(context->getSampleRate() * ON_POSITION_CHANGED_INTERVAL),
|
|
40
|
-
true)
|
|
40
|
+
true),
|
|
41
|
+
bufferingStateDispatcher_(
|
|
42
|
+
context->getAudioEventHandlerRegistry(),
|
|
43
|
+
static_cast<int>(context->getSampleRate() * ON_BUFFERING_STATE_DEBOUNCE_INTERVAL)) {
|
|
41
44
|
decoderState_->playbackRate.store(options.playbackRate, std::memory_order_release);
|
|
42
45
|
decoderState_->preservesPitch.store(options.preservesPitch, std::memory_order_release);
|
|
43
46
|
|
|
@@ -90,6 +93,10 @@ void AudioFileSourceNode::assignOnPositionChangedCallbackId(uint64_t callbackId)
|
|
|
90
93
|
positionChanged_.assignCallbackId(callbackId);
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
void AudioFileSourceNode::assignOnBufferingStateChangeCallbackId(uint64_t callbackId) {
|
|
97
|
+
bufferingStateDispatcher_.assignCallbackId(callbackId);
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
bool AudioFileSourceNode::initDecoder(
|
|
94
101
|
const std::shared_ptr<BaseAudioContext> &context,
|
|
95
102
|
AudioFileSourceOptions &options) {
|
|
@@ -433,6 +440,9 @@ bool AudioFileSourceNode::isCurrentMediaElementSource(uint64_t bindingId) const
|
|
|
433
440
|
|
|
434
441
|
void AudioFileSourceNode::pause() {
|
|
435
442
|
filePaused_ = true;
|
|
443
|
+
// A deliberate pause is not a stall — don't leave a stale "buffering" state
|
|
444
|
+
// observed by JS once processDecodedOutput() stops being called below.
|
|
445
|
+
bufferingStateDispatcher_.advance(/* hasData */ true, 0);
|
|
436
446
|
}
|
|
437
447
|
|
|
438
448
|
void AudioFileSourceNode::disable() {
|
|
@@ -440,6 +450,7 @@ void AudioFileSourceNode::disable() {
|
|
|
440
450
|
filePaused_ = false;
|
|
441
451
|
endOfStreamStopPending_ = false;
|
|
442
452
|
endOfStreamDrainPending_ = false;
|
|
453
|
+
bufferingStateDispatcher_.advance(/* hasData */ true, 0);
|
|
443
454
|
|
|
444
455
|
AudioScheduledSourceNode::disable();
|
|
445
456
|
}
|
|
@@ -719,10 +730,13 @@ std::shared_ptr<DSPAudioBuffer> AudioFileSourceNode::processDecodedOutput(
|
|
|
719
730
|
const bool hasFreshChunk = needsFreshDecoderChunk && readNextFrameChunk(incoming);
|
|
720
731
|
|
|
721
732
|
if (!hasFreshChunk && pendingDecoderChunk_.size == 0) {
|
|
733
|
+
bufferingStateDispatcher_.advance(/* hasData */ false, framesToProcess);
|
|
722
734
|
processingBuffer->zero();
|
|
723
735
|
return processingBuffer;
|
|
724
736
|
}
|
|
725
737
|
|
|
738
|
+
bufferingStateDispatcher_.advance(/* hasData */ true, framesToProcess);
|
|
739
|
+
|
|
726
740
|
if (hasFreshChunk && incoming.state == StreamState::END_OF_STREAM) {
|
|
727
741
|
currentTime_.store(duration_, std::memory_order_release);
|
|
728
742
|
sendOnPositionChangedEvent(0);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include <audioapi/dsp/WsolaTimeStretcher.h>
|
|
7
7
|
#include <audioapi/libs/decoding/IncrementalAudioDecoder.h>
|
|
8
8
|
#include <audioapi/types/NodeOptions.h>
|
|
9
|
+
#include <audioapi/utils/events/BufferingStateDispatcher.h>
|
|
9
10
|
#include <audioapi/utils/events/PositionChangedDispatcher.h>
|
|
10
11
|
#include <cstddef>
|
|
11
12
|
#include <thread>
|
|
@@ -29,6 +30,9 @@ class MediaElementAudioSourceNode;
|
|
|
29
30
|
|
|
30
31
|
inline constexpr auto ON_POSITION_CHANGED_INTERVAL = 0.25f;
|
|
31
32
|
|
|
33
|
+
/// @brief Debounce interval for BufferingStateDispatcher — see its header.
|
|
34
|
+
inline constexpr auto ON_BUFFERING_STATE_DEBOUNCE_INTERVAL = 0.15f;
|
|
35
|
+
|
|
32
36
|
/// @brief Decodes a file or in-memory buffer and plays it as a scheduled source.
|
|
33
37
|
/// @note When routed through MediaElementAudioSourceNode, this node outputs silence and the media node pulls decoded audio.
|
|
34
38
|
class AudioFileSourceNode : public AudioScheduledSourceNode {
|
|
@@ -125,6 +129,17 @@ class AudioFileSourceNode : public AudioScheduledSourceNode {
|
|
|
125
129
|
|
|
126
130
|
void assignOnPositionChangedCallbackId(uint64_t callbackId);
|
|
127
131
|
|
|
132
|
+
/// @brief Registers the JS callback for buffering-state-change events.
|
|
133
|
+
/// Pass 0 to unregister.
|
|
134
|
+
void assignOnBufferingStateChangeCallbackId(uint64_t callbackId);
|
|
135
|
+
|
|
136
|
+
/// @brief True while the render thread has been unable to obtain a decoded
|
|
137
|
+
/// frame for longer than @ref ON_BUFFERING_STATE_DEBOUNCE_INTERVAL. Not
|
|
138
|
+
/// exposed to JS (event-driven there); kept for tests.
|
|
139
|
+
[[nodiscard]] bool isBuffering() const {
|
|
140
|
+
return bufferingStateDispatcher_.isBuffering();
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
protected:
|
|
129
144
|
/// @brief Outputs silence when media-routed; otherwise decodes into @p processingBuffer.
|
|
130
145
|
std::shared_ptr<DSPAudioBuffer> processNode(
|
|
@@ -183,6 +198,10 @@ class AudioFileSourceNode : public AudioScheduledSourceNode {
|
|
|
183
198
|
|
|
184
199
|
PositionChangedDispatcher positionChanged_;
|
|
185
200
|
|
|
201
|
+
/// @brief Owns the buffering-state debounce/dispatch logic; see
|
|
202
|
+
/// BufferingStateDispatcher.h for the policy.
|
|
203
|
+
BufferingStateDispatcher bufferingStateDispatcher_;
|
|
204
|
+
|
|
186
205
|
/// @brief Sets up SPSC channels, constructs the SeekDecoderDaemon, and initialises metadata from the opened decoder.
|
|
187
206
|
/// @return false if the source could not be opened; caller must not set isInitialized_.
|
|
188
207
|
[[nodiscard]] bool initDecoder(
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
#include <audioapi/core/sources/RecorderAdapterNode.h>
|
|
3
3
|
#include <audioapi/core/types/ChannelInterpretation.h>
|
|
4
4
|
#include <audioapi/core/utils/Constants.h>
|
|
5
|
+
#include <audioapi/core/utils/CurrentRenderScope.h>
|
|
5
6
|
#include <audioapi/utils/AudioArray.hpp>
|
|
6
7
|
|
|
7
8
|
#include <algorithm>
|
|
8
9
|
#include <cmath>
|
|
9
10
|
#include <cstring>
|
|
10
11
|
#include <memory>
|
|
12
|
+
#include <thread>
|
|
11
13
|
#include <vector>
|
|
12
14
|
|
|
13
15
|
namespace audioapi {
|
|
@@ -59,17 +61,26 @@ void RecorderAdapterNode::init(size_t bufferSize, int channelCount, float sample
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
void RecorderAdapterNode::adapterCleanup() {
|
|
64
|
+
isInitialized_.store(false, std::memory_order_release);
|
|
65
|
+
waitForProcessQuiescence();
|
|
66
|
+
|
|
62
67
|
needsResampling_ = false;
|
|
63
68
|
buff_.clear();
|
|
64
69
|
resampler_.reset();
|
|
65
70
|
overflowSize_ = 0;
|
|
71
|
+
}
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
void RecorderAdapterNode::waitForProcessQuiescence() const {
|
|
74
|
+
while (currentProcesses_.load(std::memory_order_acquire) != 0) {
|
|
75
|
+
std::this_thread::yield();
|
|
76
|
+
}
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
std::shared_ptr<DSPAudioBuffer> RecorderAdapterNode::processNode(
|
|
71
80
|
const std::shared_ptr<DSPAudioBuffer> &processingBuffer,
|
|
72
81
|
int framesToProcess) {
|
|
82
|
+
const CurrentRenderScope processScope(currentProcesses_);
|
|
83
|
+
|
|
73
84
|
if (!isInitialized_.load(std::memory_order_acquire)) {
|
|
74
85
|
processingBuffer->zero();
|
|
75
86
|
return processingBuffer;
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
#include <audioapi/dsp/r8brain/Resampler.hpp>
|
|
8
8
|
#include <audioapi/utils/AudioBuffer.hpp>
|
|
9
9
|
#include <audioapi/utils/CircularOverflowableAudioArray.h>
|
|
10
|
+
#include <atomic>
|
|
11
|
+
#include <cstdint>
|
|
10
12
|
#include <memory>
|
|
11
13
|
#include <vector>
|
|
12
14
|
|
|
@@ -41,6 +43,7 @@ class RecorderAdapterNode : public AudioNode {
|
|
|
41
43
|
private:
|
|
42
44
|
void readFrames(AudioBuffer &target, size_t framesToRead);
|
|
43
45
|
void processResampled(int framesToProcess);
|
|
46
|
+
void waitForProcessQuiescence() const;
|
|
44
47
|
|
|
45
48
|
std::unique_ptr<r8b::MultiChannelResampler> resampler_;
|
|
46
49
|
bool needsResampling_ = false;
|
|
@@ -53,6 +56,10 @@ class RecorderAdapterNode : public AudioNode {
|
|
|
53
56
|
// Accumulates resampled output across calls
|
|
54
57
|
AudioBuffer overflowBuffer_;
|
|
55
58
|
size_t overflowSize_ = 0;
|
|
59
|
+
|
|
60
|
+
std::atomic<bool> isInitialized_{false};
|
|
61
|
+
/// Incremented around each processNode() call; adapterCleanup waits for quiescence.
|
|
62
|
+
std::atomic<uint32_t> currentProcesses_{0};
|
|
56
63
|
};
|
|
57
64
|
|
|
58
65
|
} // namespace audioapi
|
|
@@ -20,7 +20,7 @@ class ParamQueueBase {
|
|
|
20
20
|
|
|
21
21
|
/// @brief Cancel scheduled parameter changes at or after the given time.
|
|
22
22
|
/// @param cancelTime The time at which to cancel scheduled changes.
|
|
23
|
-
void cancelScheduledValues(double cancelTime) {
|
|
23
|
+
virtual void cancelScheduledValues(double cancelTime) {
|
|
24
24
|
eventQueue_.erase(eventQueue_.lowerBound(cancelTime), eventQueue_.end());
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
namespace audioapi {
|
|
10
10
|
|
|
11
11
|
std::optional<float> ParamRenderQueue::computeValueAtTime(double time) {
|
|
12
|
-
while (
|
|
13
|
-
!eventQueue_.isEmpty() &&
|
|
12
|
+
while (!eventQueue_.isEmpty() &&
|
|
14
13
|
(!currentEvent_ ||
|
|
15
|
-
|
|
14
|
+
(time >= currentEvent_->getEndTime() &&
|
|
15
|
+
eventQueue_.peekFront().getStartTime() <= time))) {
|
|
16
16
|
RenderParamEvent next;
|
|
17
17
|
eventQueue_.pop(next);
|
|
18
18
|
currentEvent_ = std::move(next);
|
|
@@ -93,8 +93,38 @@ float ParamRenderQueue::getValueOfPreviousEventAt(const RenderParamEvent &event,
|
|
|
93
93
|
return event.getEndValue();
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
void ParamRenderQueue::cancelScheduledValues(double cancelTime) {
|
|
97
|
+
ParamQueueBase::cancelScheduledValues(cancelTime);
|
|
98
|
+
|
|
99
|
+
// An event may already have been promoted out of the queue; the erase above cannot see it
|
|
100
|
+
if (currentEvent_ && currentEvent_->getAutomationTime() >= cancelTime) {
|
|
101
|
+
// restore value from before currentEvent_ and discard it
|
|
102
|
+
currentEvent_ = ParamRenderEventFactory::createSetValueEvent(
|
|
103
|
+
currentEvent_->getStartValue(), currentEvent_->getStartTime());
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
void ParamRenderQueue::truncateCurrentEventAt(double holdTime) {
|
|
108
|
+
float holdValue = currentEvent_->getCalculateValue()(
|
|
109
|
+
currentEvent_->getStartTime(),
|
|
110
|
+
currentEvent_->getEndTime(),
|
|
111
|
+
currentEvent_->getStartValue(),
|
|
112
|
+
currentEvent_->getEndValue(),
|
|
113
|
+
holdTime);
|
|
114
|
+
currentEvent_->setEndTime(holdTime);
|
|
115
|
+
currentEvent_->setEndValue(holdValue);
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
void ParamRenderQueue::cancelAndHoldAtTime(double cancelTime) {
|
|
97
|
-
// E2:
|
|
119
|
+
// E2: handle the case with currentEvent_ first, since it is no longer in queue
|
|
120
|
+
if (currentEvent_ && currentEvent_->isRampType() && cancelTime < currentEvent_->getEndTime()) {
|
|
121
|
+
truncateCurrentEventAt(cancelTime);
|
|
122
|
+
// Step 5: remove everything strictly after cancelTime
|
|
123
|
+
eventQueue_.erase(eventQueue_.upperBound(cancelTime), eventQueue_.end());
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// E2: find the first event with automationTime > cancelTime
|
|
98
128
|
auto e2It = eventQueue_.upperBound(cancelTime);
|
|
99
129
|
|
|
100
130
|
if (e2It != eventQueue_.end() && e2It->isRampType()) {
|
|
@@ -154,14 +184,7 @@ void ParamRenderQueue::cancelAndHoldAtTime(double cancelTime) {
|
|
|
154
184
|
|
|
155
185
|
if (currentEvent_->getType() == ParamEventType::SET_VALUE_CURVE &&
|
|
156
186
|
cancelTime <= currentEvent_->getEndTime()) {
|
|
157
|
-
|
|
158
|
-
currentEvent_->getStartTime(),
|
|
159
|
-
currentEvent_->getEndTime(),
|
|
160
|
-
currentEvent_->getStartValue(),
|
|
161
|
-
currentEvent_->getEndValue(),
|
|
162
|
-
cancelTime);
|
|
163
|
-
currentEvent_->setEndTime(cancelTime);
|
|
164
|
-
currentEvent_->setEndValue(holdValue);
|
|
187
|
+
truncateCurrentEventAt(cancelTime);
|
|
165
188
|
// fall through to step 5
|
|
166
189
|
}
|
|
167
190
|
}
|
|
@@ -22,6 +22,12 @@ class ParamRenderQueue : public ParamQueueBase<RenderParamEvent> {
|
|
|
22
22
|
/// @return True if the event was successfully added, false if the queue is full.
|
|
23
23
|
bool push(RenderParamEvent &&event) override;
|
|
24
24
|
|
|
25
|
+
/// @brief Cancel scheduled parameter changes at or after the given time.
|
|
26
|
+
/// Also cancels the already-promoted @c currentEvent_ when its automation time is at or after
|
|
27
|
+
/// @p cancelTime (an in-flight ramp), restoring the value from before that automation as the spec requires.
|
|
28
|
+
/// @param cancelTime The time at which to cancel scheduled changes.
|
|
29
|
+
void cancelScheduledValues(double cancelTime) override;
|
|
30
|
+
|
|
25
31
|
/// @brief Cancel scheduled parameter changes and hold the current value at the given time.
|
|
26
32
|
/// @param cancelTime The time at which to cancel scheduled changes.
|
|
27
33
|
void cancelAndHoldAtTime(double cancelTime);
|
|
@@ -29,6 +35,9 @@ class ParamRenderQueue : public ParamQueueBase<RenderParamEvent> {
|
|
|
29
35
|
private:
|
|
30
36
|
float defaultValue_;
|
|
31
37
|
|
|
38
|
+
/// @brief Truncate @c currentEvent_ at @p holdTime, freezing it at its value there.
|
|
39
|
+
void truncateCurrentEventAt(double holdTime);
|
|
40
|
+
|
|
32
41
|
/// @brief Resolve new event's startValue and startTime based on the previous event in the queue,
|
|
33
42
|
/// and adjust neighboring events to maintain the invariant of non-overlapping events in the queue.
|
|
34
43
|
/// @param event The new event to resolve around.
|
|
@@ -47,13 +47,13 @@ class WorkletRuntime {
|
|
|
47
47
|
[[nodiscard]] jsi::Runtime &getJSIRuntime() const {
|
|
48
48
|
throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR
|
|
49
49
|
}
|
|
50
|
-
[[nodiscard]] jsi::Value
|
|
50
|
+
[[nodiscard]] jsi::Value runSync(jsi::Runtime &rt, const jsi::Value &worklet) const {
|
|
51
51
|
throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR
|
|
52
52
|
}
|
|
53
|
-
[[nodiscard]] jsi::Value
|
|
53
|
+
[[nodiscard]] jsi::Value runSync(std::function<jsi::Value(jsi::Runtime &)> &&job) const {
|
|
54
54
|
// NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
|
|
55
55
|
throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR} jsi::Value
|
|
56
|
-
|
|
56
|
+
runSync(const std::function<jsi::Value(jsi::Runtime &)> &job) const {
|
|
57
57
|
throw RN_AUDIO_API_WORKLETS_DISABLED_ERROR
|
|
58
58
|
}
|
|
59
59
|
};
|
|
@@ -15,7 +15,7 @@ WorkletsRunner::WorkletsRunner(
|
|
|
15
15
|
}
|
|
16
16
|
#if RN_AUDIO_API_ENABLE_WORKLETS
|
|
17
17
|
unsafeRuntimePtr = &strongRuntime->getJSIRuntime();
|
|
18
|
-
strongRuntime->
|
|
18
|
+
strongRuntime->runSync([this, shareableWorklet](jsi::Runtime &rt) -> jsi::Value {
|
|
19
19
|
/// Placement new to avoid dynamic memory allocation
|
|
20
20
|
new (reinterpret_cast<jsi::Function *>(&unsafeWorklet))
|
|
21
21
|
jsi::Function(shareableWorklet->toJSValue(*unsafeRuntimePtr)
|
|
@@ -62,7 +62,7 @@ std::optional<jsi::Value> WorkletsRunner::executeOnRuntimeGuarded(
|
|
|
62
62
|
return std::nullopt;
|
|
63
63
|
}
|
|
64
64
|
#if RN_AUDIO_API_ENABLE_WORKLETS
|
|
65
|
-
return strongRuntime->
|
|
65
|
+
return strongRuntime->runSync(std::move(job));
|
|
66
66
|
#else
|
|
67
67
|
return std::nullopt;
|
|
68
68
|
#endif
|
|
@@ -27,6 +27,16 @@ struct DoubleValuePayload {
|
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
struct BoolValuePayload {
|
|
31
|
+
bool value;
|
|
32
|
+
|
|
33
|
+
facebook::jsi::Object toJsiObject(facebook::jsi::Runtime &rt) const {
|
|
34
|
+
facebook::jsi::Object obj(rt);
|
|
35
|
+
obj.setProperty(rt, "value", value);
|
|
36
|
+
return obj;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
30
40
|
struct InterruptionPayload {
|
|
31
41
|
std::string type;
|
|
32
42
|
bool shouldResume;
|
|
@@ -90,6 +100,7 @@ struct AudioReadyPayload {
|
|
|
90
100
|
using AudioEventPayload = std::variant<
|
|
91
101
|
EmptyPayload,
|
|
92
102
|
DoubleValuePayload,
|
|
103
|
+
BoolValuePayload,
|
|
93
104
|
InterruptionPayload,
|
|
94
105
|
StringPayload,
|
|
95
106
|
BufferEndedPayload,
|
|
@@ -40,6 +40,7 @@ AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::AUDIO_READY, AudioReadyPayload);
|
|
|
40
40
|
AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::POSITION_CHANGED, DoubleValuePayload);
|
|
41
41
|
AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::BUFFER_ENDED, BufferEndedPayload);
|
|
42
42
|
AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::RECORDER_ERROR, StringPayload);
|
|
43
|
+
AUDIOAPI_DEFINE_EVENT_PAYLOAD(AudioEvent::BUFFERING_STATE_CHANGE, BoolValuePayload);
|
|
43
44
|
|
|
44
45
|
#undef AUDIOAPI_DEFINE_EVENT_PAYLOAD
|
|
45
46
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#include <audioapi/utils/events/BufferingStateDispatcher.h>
|
|
2
|
+
|
|
3
|
+
#include <memory>
|
|
4
|
+
|
|
5
|
+
namespace audioapi {
|
|
6
|
+
|
|
7
|
+
BufferingStateDispatcher::BufferingStateDispatcher(
|
|
8
|
+
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry,
|
|
9
|
+
int startThresholdFrames)
|
|
10
|
+
: bufferingStateChangeEvent_(audioEventHandlerRegistry),
|
|
11
|
+
startThresholdFrames_(startThresholdFrames) {}
|
|
12
|
+
|
|
13
|
+
void BufferingStateDispatcher::assignCallbackId(uint64_t callbackId) noexcept {
|
|
14
|
+
bufferingStateChangeEvent_.assignCallbackId(callbackId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
uint64_t BufferingStateDispatcher::getCallbackId() const noexcept {
|
|
18
|
+
return bufferingStateChangeEvent_.getCallbackId();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
bool BufferingStateDispatcher::hasCallback() const noexcept {
|
|
22
|
+
return bufferingStateChangeEvent_.hasCallback();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
bool BufferingStateDispatcher::isBuffering() const noexcept {
|
|
26
|
+
return isBuffering_.load(std::memory_order_acquire);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void BufferingStateDispatcher::advance(bool hasData, int framesToProcess) {
|
|
30
|
+
if (!bufferingStateChangeEvent_.hasCallback()) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (hasData) {
|
|
35
|
+
starvedFrames_ = 0;
|
|
36
|
+
if (isBuffering_.exchange(false, std::memory_order_acq_rel)) {
|
|
37
|
+
bufferingStateChangeEvent_.dispatchFromAudioThread(BoolValuePayload{.value = false});
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
starvedFrames_ += framesToProcess;
|
|
43
|
+
if (!isBuffering_.load(std::memory_order_acquire) && starvedFrames_ >= startThresholdFrames_) {
|
|
44
|
+
isBuffering_.store(true, std::memory_order_release);
|
|
45
|
+
bufferingStateChangeEvent_.dispatchFromAudioThread(BoolValuePayload{.value = true});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
} // namespace audioapi
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <audioapi/events/EventCaller.hpp>
|
|
4
|
+
|
|
5
|
+
#include <atomic>
|
|
6
|
+
#include <cstdint>
|
|
7
|
+
#include <memory>
|
|
8
|
+
|
|
9
|
+
namespace audioapi {
|
|
10
|
+
|
|
11
|
+
/// @brief Debounces render-thread frame starvation into a buffering-state
|
|
12
|
+
/// event. A single starved render quantum is normal decode-ahead jitter, not
|
|
13
|
+
/// a real stall — only sustained starvation past @p startThresholdFrames is
|
|
14
|
+
/// reported. Recovery is reported immediately, with no symmetric debounce.
|
|
15
|
+
class BufferingStateDispatcher {
|
|
16
|
+
public:
|
|
17
|
+
BufferingStateDispatcher(
|
|
18
|
+
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry,
|
|
19
|
+
int startThresholdFrames);
|
|
20
|
+
|
|
21
|
+
void assignCallbackId(uint64_t callbackId) noexcept;
|
|
22
|
+
[[nodiscard]] uint64_t getCallbackId() const noexcept;
|
|
23
|
+
[[nodiscard]] bool hasCallback() const noexcept;
|
|
24
|
+
|
|
25
|
+
/// @brief True once sustained starvation has been reported and no
|
|
26
|
+
/// recovery has fired yet.
|
|
27
|
+
[[nodiscard]] bool isBuffering() const noexcept;
|
|
28
|
+
|
|
29
|
+
/// @brief Call once per render quantum. @p hasData is true when the
|
|
30
|
+
/// quantum had a decoded chunk available (fresh or previously stashed) to
|
|
31
|
+
/// play; false when the decoder daemon has nothing ready yet.
|
|
32
|
+
/// @note Audio thread only.
|
|
33
|
+
void advance(bool hasData, int framesToProcess);
|
|
34
|
+
|
|
35
|
+
private:
|
|
36
|
+
EventCaller<AudioEvent::BUFFERING_STATE_CHANGE> bufferingStateChangeEvent_;
|
|
37
|
+
std::atomic<bool> isBuffering_{false};
|
|
38
|
+
int starvedFrames_ = 0;
|
|
39
|
+
const int startThresholdFrames_;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
} // namespace audioapi
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#ifdef __OBJC__ // when compiled as Objective-C
|
|
4
|
-
#import <NativeAudioRecorder.h>
|
|
4
|
+
#import <audioapi/ios/core/NativeAudioRecorder.h>
|
|
5
5
|
#else
|
|
6
6
|
typedef struct objc_object NSURL;
|
|
7
7
|
typedef struct objc_object AVAudioFile;
|
|
8
8
|
typedef struct objc_object AudioBufferList;
|
|
9
9
|
typedef struct objc_object NativeAudioRecorder;
|
|
10
|
+
typedef struct objc_object AVAudioFormat;
|
|
10
11
|
#endif // __OBJC__
|
|
11
12
|
|
|
12
13
|
#include <audioapi/core/inputs/AudioRecorder.h>
|
|
@@ -59,6 +60,16 @@ class IOSAudioRecorder : public AudioRecorder {
|
|
|
59
60
|
const std::shared_ptr<AudioFileProperties> &properties,
|
|
60
61
|
const std::string &fileNameOverride = "");
|
|
61
62
|
void rollbackFailedStart();
|
|
63
|
+
Result<NoneType, std::string> reprepareForLiveInput();
|
|
64
|
+
void handleInputConfigurationChange();
|
|
65
|
+
Result<NoneType, std::string> reprepareFileWriter(
|
|
66
|
+
AVAudioFormat *inputFormat,
|
|
67
|
+
int maxInputBufferLength);
|
|
68
|
+
Result<NoneType, std::string> reprepareCallback(
|
|
69
|
+
AVAudioFormat *inputFormat,
|
|
70
|
+
int maxInputBufferLength);
|
|
71
|
+
void reprepareAdapter(AVAudioFormat *inputFormat, int maxInputBufferLength);
|
|
72
|
+
void runSideEffects(const AudioBufferList *inputBuffer, int numFrames);
|
|
62
73
|
|
|
63
74
|
std::vector<std::string> recordingSegmentPaths_;
|
|
64
75
|
};
|