Skip to content

Commit

Permalink
Feat/audio param methods (#238)
Browse files Browse the repository at this point in the history
* refactor: refactored types

* feat: added AudioParam missing methods

* docs: update API coverage docs

* refactor: switch priority queue to deque

* feat: added all AudioParam methods to index.ts

---------

Co-authored-by: Maciej Makowski <maciej.makowski@swmansion.com>
maciejmakowski2003 and Maciej Makowski authored Dec 16, 2024
1 parent ac2e8d6 commit 7b88f68
Showing 17 changed files with 452 additions and 159 deletions.
4 changes: 2 additions & 2 deletions apps/fabric-example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1656,7 +1656,7 @@ PODS:
- React-logger (= 0.76.0)
- React-perflogger (= 0.76.0)
- React-utils (= 0.76.0)
- RNAudioAPI (0.3.0-rc2):
- RNAudioAPI (0.3.1):
- DoubleConversion
- glog
- hermes-engine
@@ -2152,7 +2152,7 @@ SPEC CHECKSUMS:
React-utils: d9624101245ebaab39c9f1bd786132da0b4f27ff
ReactCodegen: dbfef1fef26f42c900bb1884fa149d49d501d64d
ReactCommon: 429ca28cd813c31359c73ffac6dc24f93347d522
RNAudioAPI: 837e19b456700c09bfd3d3c0068b82d2a11c68d0
RNAudioAPI: e7c191e81df9b2a725b8409767130b0af869f9bc
RNGestureHandler: 0e5ae8d72ef4afb855e98dcdbe60f27d938abe13
RNReanimated: 006a5d3961bf09c1e96d62ed436e02b2e43b89bb
RNScreens: e389d6a6a66a4f0d3662924ecae803073ccce8ec
34 changes: 6 additions & 28 deletions docs/web-audio-coverage.md
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ Some of the noticeable implementation details that are still in progress or not
- Support of different number of channels (current approach in most of the audio-graph nodes assumes working with two channel audio)
- Multi-input for each node and input mixing (Although specification suggests that most of the nodes can cave only one input or output, common use-cases proves otherwise). Only node that mixes multiple inputs is `DestinationNode`.

## ✅ Completed (**9** out of 33)
## ✅ Completed (**10** out of 32)

<details>
<summary><b>AudioBuffer</b></summary>
@@ -36,8 +36,11 @@ Some of the noticeable implementation details that are still in progress or not
<details>
<summary><b>StereoPannerNode</b></summary>
</details>
<details>
<summary><b>AudioParam</b></summary>
</details>

## 🚧 In Progress (**4** out of 33)
## 🚧 In Progress (**3** out of 32)

<details>
<summary><b>AudioContext</b></summary>
@@ -80,28 +83,6 @@ Some of the noticeable implementation details that are still in progress or not

</details>

<details>
<summary><b>AudioParam</b></summary>

<div style="padding: 16px; padding-left: 42px;">

| Property 🔹/ Method 🔘 | state |
| -------------------------- | ----- |
| 🔹 value ||
| 🔹 defaultValue ||
| 🔹 minValue ||
| 🔹 maxValue ||
| 🔘 setValueAtTime ||
| 🔘 linearRampToValueAtTime ||
| 🔘 setTargetAtTime ||
| 🔘 setValueCurveAtTime ||
| 🔘 cancelScheduledValues ||
| 🔘 cancelAndHoldAtTime ||

</div>

</details>

<details>
<summary><b>BaseAudioContext</b></summary>

@@ -138,7 +119,7 @@ Some of the noticeable implementation details that are still in progress or not

</details>

## ❌ Not yet available (**20** out of 33)
## ❌ Not yet available (**19** out of 32)

<details>
<summary><b>AudioParamMap</b></summary>
@@ -197,6 +178,3 @@ Some of the noticeable implementation details that are still in progress or not
<details>
<summary><b>OfflineAudioContext</b></summary>
</details>
<details>
<summary><b>AudioParamMap</b></summary>
</details>
Original file line number Diff line number Diff line change
@@ -23,8 +23,11 @@ class AudioParamHostObject : public JsiHostObject {
addFunctions(
JSI_EXPORT_FUNCTION(AudioParamHostObject, setValueAtTime),
JSI_EXPORT_FUNCTION(AudioParamHostObject, linearRampToValueAtTime),
JSI_EXPORT_FUNCTION(
AudioParamHostObject, exponentialRampToValueAtTime));
JSI_EXPORT_FUNCTION(AudioParamHostObject, exponentialRampToValueAtTime),
JSI_EXPORT_FUNCTION(AudioParamHostObject, setTargetAtTime),
JSI_EXPORT_FUNCTION(AudioParamHostObject, setValueCurveAtTime),
JSI_EXPORT_FUNCTION(AudioParamHostObject, cancelScheduledValues),
JSI_EXPORT_FUNCTION(AudioParamHostObject, cancelAndHoldAtTime));

addSetters(JSI_EXPORT_PROPERTY_SETTER(AudioParamHostObject, value));
}
@@ -66,6 +69,40 @@ class AudioParamHostObject : public JsiHostObject {
return jsi::Value::undefined();
}

JSI_HOST_FUNCTION(setTargetAtTime) {
auto target = static_cast<float>(args[0].getNumber());
double startTime = args[1].getNumber();
double timeConstant = args[2].getNumber();
param_->setTargetAtTime(target, startTime, timeConstant);
return jsi::Value::undefined();
}

JSI_HOST_FUNCTION(setValueCurveAtTime) {
auto values = args[0].getObject(runtime).asArray(runtime);
auto length = static_cast<int>(values.length(runtime));
auto valuesData = new float[length];
for (size_t i = 0; i < values.length(runtime); i++) {
valuesData[i] =
static_cast<float>(values.getValueAtIndex(runtime, i).getNumber());
}
double startTime = args[1].getNumber();
double duration = args[2].getNumber();
param_->setValueCurveAtTime(valuesData, length, startTime, duration);
return jsi::Value::undefined();
}

JSI_HOST_FUNCTION(cancelScheduledValues) {
double cancelTime = args[0].getNumber();
param_->cancelScheduledValues(cancelTime);
return jsi::Value::undefined();
}

JSI_HOST_FUNCTION(cancelAndHoldAtTime) {
double cancelTime = args[0].getNumber();
param_->cancelAndHoldAtTime(cancelTime);
return jsi::Value::undefined();
}

JSI_PROPERTY_SETTER(value) {
param_->setValue(static_cast<float>(value.getNumber()));
}
269 changes: 217 additions & 52 deletions packages/react-native-audio-api/common/cpp/core/AudioParam.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include "AudioParam.h"

#include "AudioUtils.h"
#include "BaseAudioContext.h"

namespace audioapi {
@@ -12,12 +14,11 @@ AudioParam::AudioParam(
defaultValue_(defaultValue),
minValue_(minValue),
maxValue_(maxValue),
context_(context),
changesQueue_() {
context_(context) {
startTime_ = 0;
endTime_ = 0;
startValue_ = 0;
endValue_ = 0;
startValue_ = value_;
endValue_ = value_;
calculateValue_ = [this](double, double, float, float, double) {
return value_;
};
@@ -40,94 +41,258 @@ float AudioParam::getMaxValue() const {
}

void AudioParam::setValue(float value) {
value_ = checkValue(value);
value_ = std::clamp(value, minValue_, maxValue_);
}

float AudioParam::getValueAtTime(double time) {
if (!changesQueue_.empty()) {
if (endTime_ < time) {
auto change = *changesQueue_.begin();
startTime_ = change.getStartTime();
endTime_ = change.getEndTime();
startValue_ = change.getStartValue();
endValue_ = change.getEndValue();
calculateValue_ = change.getCalculateValue();
changesQueue_.erase(changesQueue_.begin());
}
if (endTime_ < time && !eventsQueue_.empty()) {
auto event = eventsQueue_.front();
startTime_ = event.getStartTime();
endTime_ = event.getEndTime();
startValue_ = event.getStartValue();
endValue_ = event.getEndValue();
calculateValue_ = event.getCalculateValue();
eventsQueue_.pop_front();
}

if (startTime_ <= time) {
value_ =
calculateValue_(startTime_, endTime_, startValue_, endValue_, time);
}
setValue(calculateValue_(startTime_, endTime_, startValue_, endValue_, time));

return value_;
}

void AudioParam::setValueAtTime(float value, double time) {
value = checkValue(value);
auto calculateValue = [](double, double, float, float endValue, double) {
void AudioParam::setValueAtTime(float value, double startTime) {
if (startTime <= getQueueEndTime()) {
return;
}

auto calculateValue = [](double startTime,
double,
float startValue,
float endValue,
double time) {
if (time < startTime) {
return startValue;
}

return endValue;
};

auto paramChange = ParamChange(time, time, value, value, calculateValue);
changesQueue_.insert(paramChange);
auto event = ParamChangeEvent(
startTime,
startTime,
getQueueEndValue(),
value,
calculateValue,
ParamChangeEventType::SET_VALUE);
updateQueue(event);
}

void AudioParam::linearRampToValueAtTime(float value, double time) {
value = checkValue(value);
void AudioParam::linearRampToValueAtTime(float value, double endTime) {
if (endTime <= getQueueEndTime()) {
return;
}

auto calculateValue = [](double startTime,
double endTime,
float startValue,
float endValue,
double time) {
return time >= endTime ? endValue
: startValue +
(endValue - startValue) * (time - startTime) /
(endTime - startTime);
if (time < startTime) {
return startValue;
}

if (time < endTime) {
return static_cast<float>(
startValue +
(endValue - startValue) * (time - startTime) / (endTime - startTime));
}

return endValue;
};

auto paramChange =
ParamChange(getStartTime(), time, getStartValue(), value, calculateValue);
changesQueue_.emplace(paramChange);
auto event = ParamChangeEvent(
getQueueEndTime(),
endTime,
getQueueEndValue(),
value,
calculateValue,
ParamChangeEventType::LINEAR_RAMP);
updateQueue(event);
}

void AudioParam::exponentialRampToValueAtTime(float value, double time) {
value = checkValue(value);
void AudioParam::exponentialRampToValueAtTime(float value, double endTime) {
if (endTime <= getQueueEndTime()) {
return;
}

auto calculateValue = [](double startTime,
double endTime,
float startValue,
float endValue,
double time) {
return time >= endTime ? endValue
: startValue *
pow(endValue / startValue,
(time - startTime) / (endTime - startTime));
if (time < startTime) {
return startValue;
}

if (time < endTime) {
return static_cast<float>(
startValue *
pow(endValue / startValue,
(time - startTime) / (endTime - startTime)));
}

return endValue;
};

auto paramChange =
ParamChange(getStartTime(), time, getStartValue(), value, calculateValue);
changesQueue_.emplace(paramChange);
auto event = ParamChangeEvent(
getQueueEndTime(),
endTime,
getQueueEndValue(),
value,
calculateValue,
ParamChangeEventType::EXPONENTIAL_RAMP);
updateQueue(event);
}

float AudioParam::checkValue(float value) const {
return std::clamp(value, minValue_, maxValue_);
void AudioParam::setTargetAtTime(
float target,
double startTime,
double timeConstant) {
if (startTime <= getQueueEndTime()) {
return;
}

auto calculateValue =
[timeConstant, target](
double startTime, double, float startValue, float, double time) {
if (time < startTime) {
return startValue;
}

return static_cast<float>(
target +
(startValue - target) * exp(-(time - startTime) / timeConstant));
};

auto event = ParamChangeEvent(
startTime,
startTime,
getQueueEndValue(),
getQueueEndValue(),
calculateValue,
ParamChangeEventType::SET_TARGET);
updateQueue(event);
}

double AudioParam::getStartTime() {
if (changesQueue_.empty()) {
return context_->getCurrentTime();
void AudioParam::setValueCurveAtTime(
const float *values,
int length,
double startTime,
double duration) {
if (startTime <= getQueueEndTime()) {
return;
}

return changesQueue_.rbegin()->getEndTime();
auto calculateValue = [&values, length](
double startTime,
double endTime,
float startValue,
float endValue,
double time) {
if (time < startTime) {
return startValue;
}

if (time < endTime) {
auto k = static_cast<int>(std::floor(
(length - 1) / (endTime - startTime) * (time - startTime)));
auto factor = static_cast<float>(
k - (time - startTime) * (length - 1) / (endTime - startTime));

return AudioUtils::linearInterpolate(values, k, k + 1, factor);
}

return endValue;
};

auto event = ParamChangeEvent(
startTime,
startTime + duration,
getQueueEndValue(),
values[length - 1],
calculateValue,
ParamChangeEventType::SET_VALUE_CURVE);
updateQueue(event);
}

void AudioParam::cancelScheduledValues(double cancelTime) {
auto it = eventsQueue_.rbegin();
while (it->getEndTime() >= cancelTime) {
if (it->getStartTime() >= cancelTime ||
it->getType() == ParamChangeEventType::SET_VALUE_CURVE) {
eventsQueue_.pop_back();
}

it++;
}
}

float AudioParam::getStartValue() {
if (changesQueue_.empty()) {
return this->value_;
void AudioParam::cancelAndHoldAtTime(double cancelTime) {
auto it = eventsQueue_.rbegin();
while (it->getEndTime() >= cancelTime) {
if (it->getStartTime() >= cancelTime) {
eventsQueue_.pop_back();
}

it++;
}

if (eventsQueue_.empty()) {
endTime_ = cancelTime;
}

if (!eventsQueue_.empty()) {
auto lastEvent = eventsQueue_.rbegin();
if (lastEvent->getEndTime() > cancelTime) {
lastEvent->setEndTime(cancelTime);
}
}
}

double AudioParam::getQueueEndTime() {
if (eventsQueue_.empty()) {
return endTime_;
}

return eventsQueue_.back().getEndTime();
}

float AudioParam::getQueueEndValue() {
if (eventsQueue_.empty()) {
return this->endValue_;
}

return eventsQueue_.back().getEndValue();
}

void AudioParam::updateQueue(ParamChangeEvent &event) {
if (!eventsQueue_.empty()) {
auto prev = eventsQueue_.back();

if (prev.getType() == ParamChangeEventType::SET_TARGET) {
prev.setEndTime(event.getStartTime());
prev.setEndValue(prev.getCalculateValue()(
prev.getStartTime(),
prev.getEndTime(),
prev.getStartValue(),
prev.getEndValue(),
event.getStartTime()));
}

event.setStartValue(prev.getEndValue());
}

return changesQueue_.rbegin()->getEndValue();
eventsQueue_.push_back(event);
}

} // namespace audioapi
25 changes: 18 additions & 7 deletions packages/react-native-audio-api/common/cpp/core/AudioParam.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#pragma once

#include <deque>
#include <memory>
#include <set>
#include <vector>

#include "ParamChange.h"
#include "ParamChangeEvent.h"
#include "ParamChangeEventType.h"

namespace audioapi {

@@ -20,31 +21,41 @@ class AudioParam {

[[nodiscard]] float getValue() const;
float getValueAtTime(double time);
void setValue(float value);
[[nodiscard]] float getDefaultValue() const;
[[nodiscard]] float getMinValue() const;
[[nodiscard]] float getMaxValue() const;

void setValue(float value);

void setValueAtTime(float value, double startTime);
void linearRampToValueAtTime(float value, double endTime);
void exponentialRampToValueAtTime(float value, double endTime);
void setTargetAtTime(float target, double startTime, double timeConstant);
void setValueCurveAtTime(
const float *values,
int length,
double startTime,
double duration);
void cancelScheduledValues(double cancelTime);
void cancelAndHoldAtTime(double cancelTime);

private:
float value_;
float defaultValue_;
float minValue_;
float maxValue_;
BaseAudioContext *context_;
std::set<ParamChange> changesQueue_;
std::deque<ParamChangeEvent> eventsQueue_;

double startTime_;
double endTime_;
float startValue_;
float endValue_;
std::function<float(double, double, float, float, double)> calculateValue_;

float checkValue(float value) const;
double getStartTime();
float getStartValue();
double getQueueEndTime();
float getQueueEndValue();
void updateQueue(ParamChangeEvent &event);
};

} // namespace audioapi
46 changes: 0 additions & 46 deletions packages/react-native-audio-api/common/cpp/core/ParamChange.cpp

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include "ParamChangeEvent.h"

#include <utility>

namespace audioapi {

ParamChangeEvent::ParamChangeEvent(
double startTime,
double endTime,
float startValue,
float endValue,
std::function<float(double, double, float, float, double)> calculateValue,
ParamChangeEventType type)
: startTime_(startTime),
endTime_(endTime),
startValue_(startValue),
endValue_(endValue),
calculateValue_(std::move(calculateValue)),
type_(type) {}

double ParamChangeEvent::getEndTime() const {
return endTime_;
}

double ParamChangeEvent::getStartTime() const {
return startTime_;
}

float ParamChangeEvent::getEndValue() const {
return endValue_;
}

float ParamChangeEvent::getStartValue() const {
return startValue_;
}

std::function<float(double, double, float, float, double)>
ParamChangeEvent::getCalculateValue() const {
return calculateValue_;
}

ParamChangeEventType ParamChangeEvent::getType() const {
return type_;
}

void ParamChangeEvent::setEndTime(double endTime) {
endTime_ = endTime;
}

void ParamChangeEvent::setStartValue(float startValue) {
startValue_ = startValue;
}

void ParamChangeEvent::setEndValue(float endValue) {
endValue_ = endValue;
}

} // namespace audioapi
Original file line number Diff line number Diff line change
@@ -2,33 +2,39 @@

#include <functional>
#include <memory>
#include "ParamChangeEventType.h"

namespace audioapi {

class ParamChange {
class ParamChangeEvent {
public:
explicit ParamChange(
explicit ParamChangeEvent(
double startTime,
double endTime,
float startValue,
float endValue,
std::function<float(double, double, float, float, double)>
calculateValue);
std::function<float(double, double, float, float, double)> calculateValue,
ParamChangeEventType type);

[[nodiscard]] double getEndTime() const;
[[nodiscard]] double getStartTime() const;
[[nodiscard]] float getEndValue() const;
[[nodiscard]] float getStartValue() const;
[[nodiscard]] std::function<float(double, double, float, float, double)>
getCalculateValue() const;
bool operator<(const ParamChange &other) const;
[[nodiscard]] ParamChangeEventType getType() const;

void setEndTime(double endTime);
void setStartValue(float startValue);
void setEndValue(float endValue);

private:
double startTime_;
double endTime_;
float startValue_;
float endValue_;
std::function<float(double, double, float, float, double)> calculateValue_;
ParamChangeEventType type_;
};

} // namespace audioapi
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
#pragma once

#include <algorithm>
#include <stdexcept>
#include <string>

namespace audioapi {

enum class BiquadFilterType {
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
#pragma once

#include <stdexcept>
#include <string>

namespace audioapi {

enum class ChannelCountMode { MAX, CLAMPED_MAX, EXPLICIT };
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
#pragma once

#include <stdexcept>
#include <string>

namespace audioapi {

enum class ChannelInterpretation { SPEAKERS, DISCRETE };
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
#pragma once

#include <stdexcept>
#include <string>

namespace audioapi {

enum class ContextState { SUSPENDED, RUNNING, CLOSED };
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
#pragma once

#include <algorithm>
#include <stdexcept>
#include <string>

namespace audioapi {

enum class OscillatorType { SINE, SQUARE, SAWTOOTH, TRIANGLE, CUSTOM };
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#pragma once

namespace audioapi {

enum class ParamChangeEventType {
LINEAR_RAMP,
EXPONENTIAL_RAMP,
SET_VALUE,
SET_TARGET,
SET_VALUE_CURVE,
};

} // namespace audioapi
48 changes: 48 additions & 0 deletions packages/react-native-audio-api/src/core/AudioParam.ts
Original file line number Diff line number Diff line change
@@ -52,4 +52,52 @@ export default class AudioParam {

this.audioParam.exponentialRampToValueAtTime(value, endTime);
}

public setTargetAtTime(
target: number,
startTime: number,
timeConstant: number
): void {
if (startTime < 0) {
throw new RangeError(
`Time must be a finite non-negative number: ${startTime}`
);
}

this.audioParam.setTargetAtTime(target, startTime, timeConstant);
}

public setValueCurveAtTime(
values: number[],
startTime: number,
duration: number
): void {
if (startTime < 0) {
throw new RangeError(
`Time must be a finite non-negative number: ${startTime}`
);
}

this.audioParam.setValueCurveAtTime(values, startTime, duration);
}

public cancelScheduledValues(cancelTime: number): void {
if (cancelTime < 0) {
throw new RangeError(
`Time must be a finite non-negative number: ${cancelTime}`
);
}

this.audioParam.cancelScheduledValues(cancelTime);
}

public cancelAndHoldAtTime(cancelTime: number): void {
if (cancelTime < 0) {
throw new RangeError(
`Time must be a finite non-negative number: ${cancelTime}`
);
}

this.audioParam.cancelAndHoldAtTime(cancelTime);
}
}
28 changes: 28 additions & 0 deletions packages/react-native-audio-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -244,6 +244,34 @@ export class AudioParam {
public exponentialRampToValueAtTime(value: number, endTime: number): void {
this.param.exponentialRampToValueAtTime(value, endTime);
}

public setTargetAtTime(
target: number,
startTime: number,
timeConstant: number
): void {
this.param.setTargetAtTime(target, startTime, timeConstant);
}

public setValueCurveAtTime(
values: number[],
startTime: number,
duration: number
): void {
this.param.setValueCurveAtTime(
new Float32Array(values),
startTime,
duration
);
}

public cancelScheduledValues(startTime: number): void {
this.param.cancelScheduledValues(startTime);
}

public cancelAndHoldAtTime(cancelTime: number): void {
this.param.cancelAndHoldAtTime(cancelTime);
}
}

export class BiquadFilterNode extends AudioNode {
12 changes: 12 additions & 0 deletions packages/react-native-audio-api/src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -121,6 +121,18 @@ export interface IAudioParam {
setValueAtTime: (value: number, startTime: number) => void;
linearRampToValueAtTime: (value: number, endTime: number) => void;
exponentialRampToValueAtTime: (value: number, endTime: number) => void;
setTargetAtTime: (
target: number,
startTime: number,
timeConstant: number
) => void;
setValueCurveAtTime: (
values: number[],
startTime: number,
duration: number
) => void;
cancelScheduledValues: (cancelTime: number) => void;
cancelAndHoldAtTime: (cancelTime: number) => void;
}

export interface IPeriodicWave {}

0 comments on commit 7b88f68

Please sign in to comment.