Skip to content

Commit

Permalink
Merge pull request #14 from shiguredo/feature/expose-rnnoise-api
Browse files Browse the repository at this point in the history
`DenoiseState`クラスの追加
  • Loading branch information
sile committed Jan 14, 2022
2 parents d6b94d4 + 767b010 commit 8928394
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 25 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,17 @@ import { Rnnoise } from "@shiguredo/rnnoise-wasm";

// RNNoise の wasm ファイルをロード
Rnnoise.load().then((rnnoise) => {
// ノイズ抑制用インスタンスを生成
const denoiseState = rnnoise.createDenoiseState();

// 音声フレームにノイズ抑制処理を適用する
const frame = new Float32Array(...);
rnnoise.processFrame(frame);

...

// インスタンスを破棄して wasm 用に割り当てたメモリを解放
denoiseState.destroy();
});
```

Expand Down
97 changes: 74 additions & 23 deletions src/rnnoise.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { simd } from "wasm-feature-detect";
import loadRnnoiseModule from "./rnnoise_wasm.js";
import { RnnoiseModule, DenoiseState, F32Ptr } from "./rnnoise_wasm.js";
import * as rnnoise_wasm from "./rnnoise_wasm.js";

/**
* {@link Rnnoise.load} 関数に指定可能なオプション
Expand All @@ -22,35 +22,21 @@ interface RnnoiseOptions {
}

/**
* WebAssembly 用にビルドした [RNNoise](https://github.com/shiguredo/rnnoise) を用いて音声データのノイズ抑制を行うためのクラス
* WebAssembly 用にビルドした [RNNoise](https://github.com/shiguredo/rnnoise) の API を提供するためのクラス
*
* インスタンスを作成するためには {@link Rnnoise.load} 関数を使用してください
*/
class Rnnoise {
private rnnoiseModule: RnnoiseModule;
private denoiseState: DenoiseState;
private pcmInputBuf: F32Ptr;
private pcmOutputBuf: F32Ptr;
private rnnoiseModule: rnnoise_wasm.RnnoiseModule;

/**
* 一度の {@link Rnnoise.processFrame} メソッド呼び出しで処理可能なサンプル数
* 一度の {@link DenoiseState.processFrame} メソッド呼び出しで処理可能なサンプル数
*/
readonly frameSize: number;

private constructor(rnnoiseModule: RnnoiseModule) {
private constructor(rnnoiseModule: rnnoise_wasm.RnnoiseModule) {
this.rnnoiseModule = rnnoiseModule;
this.denoiseState = rnnoiseModule._rnnoise_create();
this.frameSize = rnnoiseModule._rnnoise_get_frame_size();

const pcmInputBuf = rnnoiseModule._malloc(this.frameSize * 4);
const pcmOutputBuf = rnnoiseModule._malloc(this.frameSize * 4);
if (!pcmInputBuf || !pcmOutputBuf) {
// `rnnoiseModule`がGCされればwasm用に割り当てた領域もまとめて解放されるので、
// 個別の領域解放処理は省いている。
throw Error("Failed to allocate PCM buffers.");
}
this.pcmInputBuf = pcmInputBuf;
this.pcmOutputBuf = pcmOutputBuf;
}

/**
Expand Down Expand Up @@ -88,6 +74,53 @@ class Rnnoise {
return Promise.resolve(new Rnnoise(rnnoiseModule));
}

/**
* ノイズ抑制を行うための {@link DenoiseState} インスタンスを生成します
*
* @returns 生成されたインスタンス
*/
createDenoiseState(): DenoiseState {
return new DenoiseState(this.rnnoiseModule);
}
}

const F32_BYTE_SIZE = 4;

/**
* ノイズ抑制に必要な状態を保持するクラス
*
* インスタンスを作成するためには {@link Rnnoise.createDenoiseState} メソッドを使用してください
*
* なお、メモリリークを防ぐために、インスタンスが不要となったら {@link DenoiseState.destroy} メソッドを
* 呼び出す必要があることに注意してください
*/
class DenoiseState {
private rnnoiseModule?: rnnoise_wasm.RnnoiseModule;
private state: rnnoise_wasm.DenoiseState;
private pcmInputBuf: rnnoise_wasm.F32Ptr;
private pcmOutputBuf: rnnoise_wasm.F32Ptr;
private frameSize: number;

/**
* @internal
*/
constructor(rnnoiseModule: rnnoise_wasm.RnnoiseModule) {
this.rnnoiseModule = rnnoiseModule;

this.frameSize = this.rnnoiseModule._rnnoise_get_frame_size();
const state = this.rnnoiseModule._rnnoise_create();
const pcmInputBuf = this.rnnoiseModule._malloc(this.frameSize * F32_BYTE_SIZE);
const pcmOutputBuf = this.rnnoiseModule._malloc(this.frameSize * F32_BYTE_SIZE);
if (!state || !pcmInputBuf || !pcmOutputBuf) {
this.destroy();
throw Error("Failed to allocate DenoiseState or PCM buffers.");
}

this.state = state;
this.pcmInputBuf = pcmInputBuf;
this.pcmOutputBuf = pcmOutputBuf;
}

/**
* 音声フレームにノイズ抑制処理を適用するメソッド
*
Expand All @@ -107,19 +140,37 @@ class Rnnoise {
* 呼び出し側で事前に変換を行っておく必要があります
*/
processFrame(frame: Float32Array): number {
if (this.rnnoiseModule === undefined) {
throw Error("This denoise state has already been destroyed.");
}

if (frame.length != this.frameSize) {
throw Error(`Expected frame size ${this.frameSize}, but got ${frame.length}`);
}

const pcmInputIndex = this.pcmInputBuf / 4;
const pcmOutputIndex = this.pcmOutputBuf / 4;
const pcmInputIndex = this.pcmInputBuf / F32_BYTE_SIZE;
const pcmOutputIndex = this.pcmOutputBuf / F32_BYTE_SIZE;

this.rnnoiseModule.HEAPF32.set(frame, pcmInputIndex);
const vad = this.rnnoiseModule._rnnoise_process_frame(this.denoiseState, this.pcmOutputBuf, this.pcmInputBuf);
const vad = this.rnnoiseModule._rnnoise_process_frame(this.state, this.pcmOutputBuf, this.pcmInputBuf);
frame.set(this.rnnoiseModule.HEAPF32.subarray(pcmOutputIndex, pcmOutputIndex + this.frameSize));

return vad;
}

/**
* インスタンスが割り当てた wasm 内の領域を解放します
*
* 本メソッド呼び出し後に {@link DenoiseState.processFrame} メソッドを呼ぶとエラーとなります
*/
destroy() {
if (this.rnnoiseModule !== undefined) {
this.rnnoiseModule._rnnoise_destroy(this.state);
this.rnnoiseModule._free(this.pcmInputBuf);
this.rnnoiseModule._free(this.pcmOutputBuf);
this.rnnoiseModule = undefined;
}
}
}

export { Rnnoise, RnnoiseOptions };
export { Rnnoise, RnnoiseOptions, DenoiseState };
9 changes: 7 additions & 2 deletions tests/rnnoise.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ import { Rnnoise } from "../dist/rnnoise";

test("Create instance and process a frame (non SIMD)", async () => {
const rnnoise = await Rnnoise.load({ wasmFileName: "rnnoise.wasm" });
const denoiseState = rnnoise.createDenoiseState();
const buffer = new Float32Array(TEST_FRAME);
const vad = rnnoise.processFrame(buffer);
const vad = denoiseState.processFrame(buffer);
denoiseState.destroy();

expect(vad).toBeCloseTo(0.43759429454803467);
buffer.forEach((x, i) => expect(x).toBeCloseTo(TEST_FRAME_PROCESSED[i]));
});

test("Create instance and process a frame (SIMD)", async () => {
const rnnoise = await Rnnoise.load({ wasmFileName: "rnnoise_simd.wasm" });
const denoiseState = rnnoise.createDenoiseState();
const buffer = new Float32Array(TEST_FRAME);
const vad = rnnoise.processFrame(buffer);
const vad = denoiseState.processFrame(buffer);
denoiseState.destroy();

expect(vad).toBeCloseTo(0.43759429454803467);
buffer.forEach((x, i) => expect(x).toBeCloseTo(TEST_FRAME_PROCESSED[i]));
Expand Down

0 comments on commit 8928394

Please sign in to comment.