react-native-audio-api 0.13.3 → 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.
Files changed (95) hide show
  1. package/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt +1 -0
  2. package/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp +8 -2
  3. package/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.h +1 -0
  4. package/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp +2 -0
  5. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp +15 -1
  6. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.h +19 -0
  7. package/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp +12 -1
  8. package/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +7 -0
  9. package/common/cpp/audioapi/core/utils/param/ParamQueueBase.hpp +1 -1
  10. package/common/cpp/audioapi/core/utils/param/ParamRenderQueue.cpp +35 -12
  11. package/common/cpp/audioapi/core/utils/param/ParamRenderQueue.h +9 -0
  12. package/common/cpp/audioapi/core/utils/worklets/SafeIncludes.h +3 -3
  13. package/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp +2 -2
  14. package/common/cpp/audioapi/events/AudioEvent.h +1 -0
  15. package/common/cpp/audioapi/events/AudioEventPayload.h +11 -0
  16. package/common/cpp/audioapi/events/AudioEventPayloadMapping.h +1 -0
  17. package/common/cpp/audioapi/utils/events/BufferingStateDispatcher.cpp +49 -0
  18. package/common/cpp/audioapi/utils/events/BufferingStateDispatcher.h +42 -0
  19. package/ios/audioapi/ios/core/IOSAudioRecorder.h +12 -1
  20. package/ios/audioapi/ios/core/IOSAudioRecorder.mm +179 -63
  21. package/ios/audioapi/ios/core/NativeAudioRecorder.h +2 -0
  22. package/ios/audioapi/ios/core/NativeAudioRecorder.m +37 -12
  23. package/ios/audioapi/ios/core/utils/IOSFileWriter.h +3 -0
  24. package/ios/audioapi/ios/core/utils/IOSFileWriter.mm +17 -0
  25. package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.h +3 -0
  26. package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm +21 -0
  27. package/ios/audioapi/ios/system/AudioEngine.h +2 -1
  28. package/ios/audioapi/ios/system/AudioEngine.mm +49 -23
  29. package/ios/audioapi/ios/system/SystemNotificationManager.mm +10 -0
  30. package/lib/commonjs/Audio/Audio.js +30 -3
  31. package/lib/commonjs/Audio/Audio.js.map +1 -1
  32. package/lib/commonjs/Audio/Audio.web.js +21 -3
  33. package/lib/commonjs/Audio/Audio.web.js.map +1 -1
  34. package/lib/commonjs/Audio/AudioFileSourceNode.js +20 -0
  35. package/lib/commonjs/Audio/AudioFileSourceNode.js.map +1 -1
  36. package/lib/commonjs/Audio/controls/AudioControls.js +90 -94
  37. package/lib/commonjs/Audio/controls/AudioControls.js.map +1 -1
  38. package/lib/commonjs/Audio/controls/audioControlUtils.js +48 -18
  39. package/lib/commonjs/Audio/controls/audioControlUtils.js.map +1 -1
  40. package/lib/commonjs/Audio/index.js +7 -0
  41. package/lib/commonjs/Audio/index.js.map +1 -1
  42. package/lib/commonjs/Audio/utils.js +16 -5
  43. package/lib/commonjs/Audio/utils.js.map +1 -1
  44. package/lib/commonjs/AudioAPIModule/AudioAPIModule.js +1 -1
  45. package/lib/commonjs/core/AudioDecoder.js +4 -1
  46. package/lib/commonjs/core/AudioDecoder.js.map +1 -1
  47. package/lib/module/Audio/Audio.js +30 -3
  48. package/lib/module/Audio/Audio.js.map +1 -1
  49. package/lib/module/Audio/Audio.web.js +21 -3
  50. package/lib/module/Audio/Audio.web.js.map +1 -1
  51. package/lib/module/Audio/AudioFileSourceNode.js +20 -0
  52. package/lib/module/Audio/AudioFileSourceNode.js.map +1 -1
  53. package/lib/module/Audio/controls/AudioControls.js +92 -96
  54. package/lib/module/Audio/controls/AudioControls.js.map +1 -1
  55. package/lib/module/Audio/controls/audioControlUtils.js +48 -19
  56. package/lib/module/Audio/controls/audioControlUtils.js.map +1 -1
  57. package/lib/module/Audio/index.js +1 -0
  58. package/lib/module/Audio/index.js.map +1 -1
  59. package/lib/module/Audio/utils.js +15 -5
  60. package/lib/module/Audio/utils.js.map +1 -1
  61. package/lib/module/AudioAPIModule/AudioAPIModule.js +1 -1
  62. package/lib/module/core/AudioDecoder.js +4 -1
  63. package/lib/module/core/AudioDecoder.js.map +1 -1
  64. package/lib/typescript/Audio/Audio.d.ts.map +1 -1
  65. package/lib/typescript/Audio/Audio.web.d.ts.map +1 -1
  66. package/lib/typescript/Audio/AudioFileSourceNode.d.ts +2 -0
  67. package/lib/typescript/Audio/AudioFileSourceNode.d.ts.map +1 -1
  68. package/lib/typescript/Audio/controls/AudioControls.d.ts.map +1 -1
  69. package/lib/typescript/Audio/controls/audioControlUtils.d.ts +11 -4
  70. package/lib/typescript/Audio/controls/audioControlUtils.d.ts.map +1 -1
  71. package/lib/typescript/Audio/index.d.ts +1 -0
  72. package/lib/typescript/Audio/index.d.ts.map +1 -1
  73. package/lib/typescript/Audio/types.d.ts +15 -1
  74. package/lib/typescript/Audio/types.d.ts.map +1 -1
  75. package/lib/typescript/Audio/utils.d.ts +2 -1
  76. package/lib/typescript/Audio/utils.d.ts.map +1 -1
  77. package/lib/typescript/core/AudioDecoder.d.ts.map +1 -1
  78. package/lib/typescript/events/types.d.ts +5 -0
  79. package/lib/typescript/events/types.d.ts.map +1 -1
  80. package/lib/typescript/jsi-interfaces.d.ts +1 -0
  81. package/lib/typescript/jsi-interfaces.d.ts.map +1 -1
  82. package/package.json +8 -8
  83. package/scripts/validate-worklets-version.js +1 -1
  84. package/src/Audio/Audio.tsx +38 -2
  85. package/src/Audio/Audio.web.tsx +26 -2
  86. package/src/Audio/AudioFileSourceNode.ts +28 -0
  87. package/src/Audio/controls/AudioControls.tsx +117 -116
  88. package/src/Audio/controls/audioControlUtils.ts +63 -31
  89. package/src/Audio/index.ts +1 -0
  90. package/src/Audio/types.ts +15 -1
  91. package/src/Audio/utils.ts +20 -2
  92. package/src/AudioAPIModule/AudioAPIModule.ts +1 -1
  93. package/src/core/AudioDecoder.ts +5 -3
  94. package/src/events/types.ts +6 -0
  95. package/src/jsi-interfaces.ts +1 -0
@@ -25,4 +25,5 @@ enum class AudioEvent {
25
25
  POSITION_CHANGED,
26
26
  BUFFER_ENDED,
27
27
  RECORDER_ERROR,
28
+ BUFFERING_STATE_CHANGE,
28
29
  }
@@ -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
- auto node = std::static_pointer_cast<AudioFileSourceNode>(node_);
49
- node->assignOnPositionChangedCallbackId(0);
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
- isInitialized_.store(false, std::memory_order_release);
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
- (time >= currentEvent_->getEndTime() && eventQueue_.peekFront().getStartTime() <= time))) {
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: first event with automationTime strictly after cancelTime
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
- float holdValue = currentEvent_->getCalculateValue()(
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 executeSync(jsi::Runtime &rt, const jsi::Value &worklet) const {
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 executeSync(std::function<jsi::Value(jsi::Runtime &)> &&job) const {
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
- executeSync(const std::function<jsi::Value(jsi::Runtime &)> &job) const {
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->executeSync([this, shareableWorklet](jsi::Runtime &rt) -> jsi::Value {
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->executeSync(std::move(job));
65
+ return strongRuntime->runSync(std::move(job));
66
66
  #else
67
67
  return std::nullopt;
68
68
  #endif
@@ -28,5 +28,6 @@ enum class AudioEvent : uint8_t {
28
28
  POSITION_CHANGED,
29
29
  BUFFER_ENDED,
30
30
  RECORDER_ERROR,
31
+ BUFFERING_STATE_CHANGE,
31
32
  };
32
33
  } // namespace audioapi
@@ -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
  };