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

[project-s] 出力する音声が0dBを超えないようにする #1593

Merged
merged 2 commits into from
Sep 30, 2023
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
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);
Copy link
Member

@Hiroshiba Hiroshiba Sep 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

どれくらいこれの影響が出てるか可視化しておくと音声合成の傾向掴めて便利かもと&なんか音量編になったときのデバッグに便利かもとかちょっと思いました。
.reductionで値が取れるらしいので、どこかで監視しておいてなにか変な値になったらwarn出すと参考になるかも?
(かなり思いつきレベルの提案です)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かにリダクションが確認できると後々デバッグなどで役立ちそう&UIでも圧縮中(音が大きすぎる)かどうか確認できると良さそうです。後でこれらも実装したいと思います!

this.correctionGainNode = new GainNode(audioContext);
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
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