Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/hann window analyser node #280

Merged
merged 3 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/audiodocs/docs/types/channel-interpretation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,3 @@ sidebar_position: 2
| :------------------------: | :------------------------- | :------------ |
| x | y where y > x | Fill each output channel with its counterpart(channel with same number), rest of output channels are silent channels |
| x | y where y < x | Fill each output channel with its counterpart(channel with same number), rest of input channels are skipped |


22 changes: 22 additions & 0 deletions packages/audiodocs/docs/types/window-type.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
sidebar_position: 3
---

# WindowType

`WindowType` type specifies which [window function](https://en.wikipedia.org/wiki/Window_function) is applied when extracting frequency data.

**Acceptable values:**
- `blackman`

Set [Blackman window](https://www.sciencedirect.com/topics/engineering/blackman-window) as window function.

- `hann`

Set [Hanning window](https://www.sciencedirect.com/topics/engineering/hanning-window) as window function.

:::caution

On `Web`, the value of`window` is permanently `'blackman'`, and it cannot be set like in the `Android` or `iOS`.

:::
10 changes: 10 additions & 0 deletions packages/audiodocs/docs/visualization/analyser-node.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ In contrast, a frequency-domain graph reveals how the signal's energy or power i
| `minDecibels` | `number` | Returns float value representing the minimum value for the range of results from [`getByteFrequencyData()`](/visualization/analyser-node#getbytefrequencydata). |
| `maxDecibels` | `number` | Returns float value representing the maximum value for the range of results from [`getByteFrequencyData()`](/visualization/analyser-node#getbytefrequencydata). |
| `smoothingTimeConstant` | `number` | Returns float value representing averaging constant with the last analysis frame. In general the higher value the smoother is the transition between values over time. |
| `window` | [`WindowType`](/types/window-type) | Returns an enumerated value that specifies the type of window function applied when extracting frequency data. |

:::caution

On `Web`, the value of`window` is permanently `'blackman'`, and it cannot be set like in the `Android` or `iOS`.

:::

## Read-only properties

Expand Down Expand Up @@ -101,3 +108,6 @@ Each value in the array is within the range 0 to 255, where value of 127 indicat
- Default value is 0.8.
- From range 0 to 1.
- 0 means no averaging, 1 means "overlap the previous and current buffer quite a lot while computing the value".

#### `window`
- Default value is `'blackman'`
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class AnalyserNodeHostObject : public AudioNodeHostObject {
JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, frequencyBinCount),
JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, minDecibels),
JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, maxDecibels),
JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, smoothingTimeConstant));
JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, smoothingTimeConstant),
JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, window));

addFunctions(
JSI_EXPORT_FUNCTION(
Expand All @@ -36,7 +37,8 @@ class AnalyserNodeHostObject : public AudioNodeHostObject {
JSI_EXPORT_PROPERTY_SETTER(AnalyserNodeHostObject, minDecibels),
JSI_EXPORT_PROPERTY_SETTER(AnalyserNodeHostObject, maxDecibels),
JSI_EXPORT_PROPERTY_SETTER(
AnalyserNodeHostObject, smoothingTimeConstant));
AnalyserNodeHostObject, smoothingTimeConstant),
JSI_EXPORT_PROPERTY_SETTER(AnalyserNodeHostObject, window));
}

JSI_PROPERTY_GETTER(fftSize) {
Expand Down Expand Up @@ -64,6 +66,12 @@ class AnalyserNodeHostObject : public AudioNodeHostObject {
return {analyserNode->getSmoothingTimeConstant()};
}

JSI_PROPERTY_GETTER(window) {
auto analyserNode = std::static_pointer_cast<AnalyserNode>(node_);
auto windowType = analyserNode->getWindowType();
return jsi::String::createFromUtf8(runtime, windowType);
}

JSI_HOST_FUNCTION(getFloatFrequencyData) {
auto destination = args[0].getObject(runtime).asArray(runtime);
auto length = static_cast<int>(destination.getProperty(runtime, "length").asNumber());
Expand Down Expand Up @@ -147,5 +155,10 @@ class AnalyserNodeHostObject : public AudioNodeHostObject {
auto smoothingTimeConstant = static_cast<float>(value.getNumber());
analyserNode->setSmoothingTimeConstant(smoothingTimeConstant);
}

JSI_PROPERTY_SETTER(window) {
auto analyserNode = std::static_pointer_cast<AnalyserNode>(node_);
analyserNode->setWindowType(value.getString(runtime).utf8(runtime));
}
};
} // namespace audioapi
37 changes: 30 additions & 7 deletions packages/react-native-audio-api/common/cpp/core/AnalyserNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ AnalyserNode::AnalyserNode(audioapi::BaseAudioContext *context)
minDecibels_(DEFAULT_MIN_DECIBELS),
maxDecibels_(DEFAULT_MAX_DECIBELS),
smoothingTimeConstant_(DEFAULT_SMOOTHING_TIME_CONSTANT),
windowType_(WindowType::BLACKMAN),
vWriteIndex_(0) {
inputBuffer_ = std::make_unique<AudioArray>(MAX_FFT_SIZE * 2);
magnitudeBuffer_ = std::make_unique<AudioArray>(fftSize_ / 2);
Expand Down Expand Up @@ -47,6 +48,10 @@ float AnalyserNode::getSmoothingTimeConstant() const {
return smoothingTimeConstant_;
}

std::string AnalyserNode::getWindowType() const {
return AnalyserNode::toString(windowType_);
}

void AnalyserNode::setFftSize(int fftSize) {
if (fftSize_ == fftSize) {
return;
Expand All @@ -69,6 +74,10 @@ void AnalyserNode::setSmoothingTimeConstant(float smoothingTimeConstant) {
smoothingTimeConstant_ = smoothingTimeConstant;
}

void AnalyserNode::setWindowType(const std::string &type) {
windowType_ = AnalyserNode::fromString(type);
}

void AnalyserNode::getFloatFrequencyData(float *data, int length) {
doFFTAnalysis();

Expand Down Expand Up @@ -197,7 +206,14 @@ void AnalyserNode::doFFTAnalysis() {
tempBuffer.copy(inputBuffer_.get(), vWriteIndex_ - fftSize_, 0, fftSize_);
}

AnalyserNode::applyWindow(tempBuffer.getData(), fftSize_);
switch (windowType_) {
case WindowType::BLACKMAN:
AnalyserNode::applyBlackManWindow(tempBuffer.getData(), fftSize_);
break;
case WindowType::HANN:
AnalyserNode::applyHannWindow(tempBuffer.getData(), fftSize_);
break;
}

// do fft analysis - get frequency domain data
fftFrame_->doFFT(tempBuffer.getData());
Expand All @@ -220,16 +236,23 @@ void AnalyserNode::doFFTAnalysis() {
}
}

void AnalyserNode::applyWindow(float *data, int length) {
void AnalyserNode::applyBlackManWindow(float *data, int length) {
// https://www.sciencedirect.com/topics/engineering/blackman-window
auto alpha = 0.16f;
auto a0 = 0.5f * (1 - alpha);
auto a1 = 0.5f;
auto a2 = 0.5f * alpha;
// https://docs.scipy.org/doc//scipy-1.2.3/reference/generated/scipy.signal.windows.blackman.html#scipy.signal.windows.blackman

for (int i = 0; i < length; ++i) {
auto x = static_cast<float>(i) / static_cast<float>(length);
auto window = a0 - a1 * cos(2 * PI * x) + a2 * cos(4 * PI * x);
auto window = 0.42f - 0.5f * cos(2 * PI * x) + 0.08f * cos(4 * PI * x);
data[i] *= window;
}
}

void AnalyserNode::applyHannWindow(float *data, int length) {
// https://www.sciencedirect.com/topics/engineering/hanning-window
// https://docs.scipy.org/doc//scipy-1.2.3/reference/generated/scipy.signal.windows.hann.html#scipy.signal.windows.hann
for (int i = 0; i < length; ++i) {
auto x = static_cast<float>(i) / static_cast<float>(length - 1);
auto window = 0.5f - 0.5f * cos(2 * PI * x);
data[i] *= window;
}
}
Expand Down
35 changes: 33 additions & 2 deletions packages/react-native-audio-api/common/cpp/core/AnalyserNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <memory>
#include <cstddef>
#include <string>

#include "AudioNode.h"

Expand All @@ -13,18 +14,21 @@ class FFTFrame;

class AnalyserNode : public AudioNode {
public:
enum class WindowType { BLACKMAN, HANN };
explicit AnalyserNode(BaseAudioContext *context);

int getFftSize() const;
int getFrequencyBinCount() const;
float getMinDecibels() const;
float getMaxDecibels() const;

float getSmoothingTimeConstant() const;
std::string getWindowType() const;

void setFftSize(int fftSize);
void setMinDecibels(float minDecibels);
void setMaxDecibels(float maxDecibels);
void setSmoothingTimeConstant(float smoothingTimeConstant);
void setWindowType(const std::string &type);

void getFloatFrequencyData(float *data, int length);
void getByteFrequencyData(uint8_t *data, int length);
Expand All @@ -39,6 +43,7 @@ class AnalyserNode : public AudioNode {
float minDecibels_;
float maxDecibels_;
float smoothingTimeConstant_;
WindowType windowType_;

std::unique_ptr<AudioArray> inputBuffer_;
std::unique_ptr<AudioBus> downMixBus_;
Expand All @@ -48,8 +53,34 @@ class AnalyserNode : public AudioNode {
std::unique_ptr<AudioArray> magnitudeBuffer_;
bool shouldDoFFTAnalysis_ { true };

static WindowType fromString(const std::string &type) {
std::string lowerType = type;
std::transform(
lowerType.begin(), lowerType.end(), lowerType.begin(), ::tolower);
if (lowerType == "blackman") {
return WindowType::BLACKMAN;
}
if (lowerType == "hann") {
return WindowType::HANN;
}

throw std::invalid_argument("Unknown window type");
}

static std::string toString(WindowType type) {
switch (type) {
case WindowType::BLACKMAN:
return "blackman";
case WindowType::HANN:
return "hann";
default:
throw std::invalid_argument("Unknown window type");
}
}

void doFFTAnalysis();
static void applyWindow(float *data, int length);
static void applyBlackManWindow(float *data, int length);
static void applyHannWindow(float *data, int length);
};

} // namespace audioapi
9 changes: 9 additions & 0 deletions packages/react-native-audio-api/src/core/AnalyserNode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IndexSizeError } from '../errors';
import { IAnalyserNode } from '../interfaces';
import { WindowType } from './types';
import AudioNode from './AudioNode';

export default class AnalyserNode extends AudioNode {
Expand Down Expand Up @@ -63,6 +64,14 @@ export default class AnalyserNode extends AudioNode {
(this.node as IAnalyserNode).smoothingTimeConstant = value;
}

public get window(): WindowType {
return (this.node as IAnalyserNode).window;
}

public set window(value: WindowType) {
(this.node as IAnalyserNode).window = value;
}

public get frequencyBinCount(): number {
return (this.node as IAnalyserNode).frequencyBinCount;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-audio-api/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ export type OscillatorType =
export interface PeriodicWaveConstraints {
disableNormalization: boolean;
}

export type WindowType = 'blackman' | 'hann';
1 change: 1 addition & 0 deletions packages/react-native-audio-api/src/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export {
ChannelCountMode,
ChannelInterpretation,
ContextState,
WindowType,
} from './core/types';
16 changes: 15 additions & 1 deletion packages/react-native-audio-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ContextState, PeriodicWaveConstraints } from './core/types';
import {
ContextState,
PeriodicWaveConstraints,
WindowType,
} from './core/types';

export class AudioBuffer {
readonly length: number;
Expand Down Expand Up @@ -132,6 +136,16 @@ export class AnalyserNode extends AudioNode {
this.smoothingTimeConstant = node.smoothingTimeConstant;
}

public get window(): WindowType {
return 'blackman';
}

public set window(value: WindowType) {
console.log(
'React Native Audio API: setting window is not supported on web'
);
}

public getByteFrequencyData(array: number[]): void {
(this.node as globalThis.AnalyserNode).getByteFrequencyData(
new Uint8Array(array)
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native-audio-api/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
OscillatorType,
ChannelCountMode,
ChannelInterpretation,
WindowType,
} from './core/types';

export interface IBaseAudioContext {
Expand Down Expand Up @@ -144,6 +145,7 @@ export interface IAnalyserNode extends IAudioNode {
minDecibels: number;
maxDecibels: number;
smoothingTimeConstant: number;
window: WindowType;

getFloatFrequencyData: (array: number[]) => void;
getByteFrequencyData: (array: number[]) => void;
Expand Down
Loading