Skip to content

Commit

Permalink
[project-s] 出力する音声が0dBを超えないようにする (#1593)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigprogramming authored Sep 30, 2023
1 parent 17557ce commit 7a94cb4
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 12 deletions.
141 changes: 133 additions & 8 deletions src/infrastructures/AudioRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export class Transport {
}
return this._time;
}

set time(value: number) {
if (this._state === "started") {
this.stop();
Expand Down Expand Up @@ -542,7 +541,7 @@ class AudioPlayerVoice {
}

constructor(audioContext: BaseAudioContext, buffer: AudioBuffer) {
this.audioBufferSourceNode = audioContext.createBufferSource();
this.audioBufferSourceNode = new AudioBufferSourceNode(audioContext);
this.audioBufferSourceNode.buffer = buffer;
this.audioBufferSourceNode.onended = () => {
this._isStopped = true;
Expand Down Expand Up @@ -606,7 +605,7 @@ export class AudioPlayer {
) {
this.audioContext = audioContext;

this.gainNode = this.audioContext.createGain();
this.gainNode = new GainNode(audioContext);
this.gainNode.gain.value = options.volume;
}

Expand Down Expand Up @@ -682,11 +681,11 @@ class SynthVoice {
this.midi = params.midi;
this.envelope = params.envelope;

this.oscillatorNode = audioContext.createOscillator();
this.oscillatorNode = new OscillatorNode(audioContext);
this.oscillatorNode.onended = () => {
this._isStopped = true;
};
this.gainNode = audioContext.createGain();
this.gainNode = new GainNode(audioContext);
this.oscillatorNode.type = params.oscillatorType;
this.oscillatorNode.connect(this.gainNode);
}
Expand Down Expand Up @@ -790,7 +789,7 @@ export class PolySynth implements Instrument {

this.oscillatorType = options.oscillatorType;
this.envelope = options.envelope;
this.gainNode = this.audioContext.createGain();
this.gainNode = new GainNode(this.audioContext);
this.gainNode.gain.value = options.volume;
}

Expand Down Expand Up @@ -869,7 +868,6 @@ export class ChannelStrip {
get volume() {
return this.gainNode.gain.value;
}

set volume(value: number) {
this.gainNode.gain.value = value;
}
Expand All @@ -878,7 +876,134 @@ export class ChannelStrip {
audioContext: BaseAudioContext,
options: ChannelStripOptions = { volume: 0.1 }
) {
this.gainNode = audioContext.createGain();
this.gainNode = new GainNode(audioContext);
this.gainNode.gain.value = options.volume;
}
}

export type LimiterOptions = {
readonly inputGain: number;
readonly outputGain: number;
readonly release: number;
};

/**
* リミッターです。大きい音を抑えます。
*/
export class Limiter {
private readonly inputGainNode: GainNode;
private readonly compNode: DynamicsCompressorNode;
private readonly correctionGainNode: GainNode;
private readonly outputGainNode: GainNode;

get input(): AudioNode {
return this.inputGainNode;
}

get output(): AudioNode {
return this.outputGainNode;
}

/**
* 入力ゲイン(dB)
*/
get inputGain() {
return this.getGainInDecibels(this.inputGainNode);
}
set inputGain(value: number) {
this.setGainInDecibels(value, this.inputGainNode);
}

/**
* 出力ゲイン(dB)
*/
get outputGain() {
return this.getGainInDecibels(this.outputGainNode);
}
set outputGain(value: number) {
this.setGainInDecibels(value, this.outputGainNode);
}

get release() {
return this.compNode.release.value;
}
set release(value: number) {
this.compNode.release.value = value;
}

get reduction() {
return this.compNode.reduction;
}

constructor(
audioContext: BaseAudioContext,
options: LimiterOptions = { inputGain: 0, outputGain: 0, release: 0.25 }
) {
this.inputGainNode = new GainNode(audioContext);
this.compNode = new DynamicsCompressorNode(audioContext);
this.correctionGainNode = new GainNode(audioContext);
this.outputGainNode = new GainNode(audioContext);

// TODO: 伴奏を再生する機能を実装したら、パラメーターを再調整する
this.compNode.threshold.value = -5; // 0dBを超えそうになったら(-5dBを超えたら)圧縮する
this.compNode.ratio.value = 20; // クリッピングが起こらないように、高いレシオ(1/20)で圧縮する
this.compNode.knee.value = 8; // 自然にかかってほしいという気持ちで8に設定(リミッターなので0でも良いかも)
this.compNode.attack.value = 0; // クリッピングが起こらないように、すぐに圧縮を開始する
this.compNode.release.value = options.release; // 歪まないように少し遅めに設定

// メイクアップゲインで上がった分を下げる(圧縮していないときは元の音量で出力)
this.correctionGainNode.gain.value = 0.85;

this.setGainInDecibels(options.inputGain, this.inputGainNode);
this.setGainInDecibels(options.outputGain, this.outputGainNode);

this.inputGainNode.connect(this.compNode);
this.compNode.connect(this.correctionGainNode);
this.correctionGainNode.connect(this.outputGainNode);
}

private linearToDecibel(linearValue: number) {
if (linearValue === 0) {
return -1000;
}
return 20 * Math.log10(linearValue);
}

private decibelToLinear(decibelValue: number) {
if (decibelValue <= -1000) {
return 0;
}
return Math.pow(10, decibelValue / 20);
}

private getGainInDecibels(gainNode: GainNode) {
return this.linearToDecibel(gainNode.gain.value);
}

private setGainInDecibels(value: number, gainNode: GainNode) {
if (!Number.isFinite(value)) {
throw new Error("Not a finite number.");
}
gainNode.gain.value = this.decibelToLinear(value);
}
}

/**
* 音声が0dB(-1~1の範囲)を超えないようにクリップします。
*/
export class Clipper {
private readonly waveShaperNode: WaveShaperNode;

get input(): AudioNode {
return this.waveShaperNode;
}

get output(): AudioNode {
return this.waveShaperNode;
}

constructor(audioContext: BaseAudioContext) {
this.waveShaperNode = new WaveShaperNode(audioContext);
this.waveShaperNode.curve = new Float32Array([-1, 0, 1]);
}
}
28 changes: 24 additions & 4 deletions src/store/singing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ import {
AudioPlayer,
AudioSequence,
ChannelStrip,
Clipper,
Instrument,
Limiter,
NoteEvent,
NoteSequence,
Sequence,
OfflineTransport,
PolySynth,
Sequence,
Transport,
OfflineTransport,
} from "@/infrastructures/AudioRenderer";
import { EngineId, StyleId } from "@/type/preload";
import {
Expand Down Expand Up @@ -245,14 +247,20 @@ const DEFAULT_BEAT_TYPE = 4;
let audioContext: AudioContext | undefined;
let transport: Transport | undefined;
let channelStrip: ChannelStrip | undefined;
let limiter: Limiter | undefined;
let clipper: Clipper | undefined;

// NOTE: テスト時はAudioContextが存在しない
if (window.AudioContext) {
audioContext = new AudioContext();
transport = new Transport(audioContext);
channelStrip = new ChannelStrip(audioContext);
limiter = new Limiter(audioContext);
clipper = new Clipper(audioContext);

channelStrip.output.connect(audioContext.destination);
channelStrip.output.connect(limiter.input);
limiter.output.connect(clipper.input);
clipper.output.connect(audioContext.destination);
}

let playbackPosition = 0;
Expand Down Expand Up @@ -1758,13 +1766,19 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
}

const sampleRate = 48000; // TODO: 設定できるようにする
const withLimiter = false; // TODO: 設定できるようにする

const offlineAudioContext = new OfflineAudioContext(
2,
sampleRate * renderDuration,
sampleRate
);
const offlineTransport = new OfflineTransport();
const channelStrip = new ChannelStrip(offlineAudioContext);
const limiter = withLimiter
? new Limiter(offlineAudioContext)
: undefined;
const clipper = new Clipper(offlineAudioContext);

for (const phrase of allPhrases.values()) {
// TODO: この辺りの処理を共通化する
Expand Down Expand Up @@ -1800,7 +1814,13 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
offlineTransport.addSequence(noteSequence);
}
}
channelStrip.output.connect(offlineAudioContext.destination);
if (limiter) {
channelStrip.output.connect(limiter.input);
limiter.output.connect(clipper.input);
} else {
channelStrip.output.connect(clipper.input);
}
clipper.output.connect(offlineAudioContext.destination);

// スケジューリングを行い、オフラインレンダリングを実行
// TODO: オフラインレンダリング後にメモリーがきちんと開放されるか確認する
Expand Down

0 comments on commit 7a94cb4

Please sign in to comment.