Skip to content

Commit 95d4432

Browse files
authored
Merge pull request #1 from descriptinc/sr/dynamics-compressor-node
Implement DynamicsCompressorNode
2 parents a7d09ff + 2733261 commit 95d4432

13 files changed

+44948
-34
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ context.encodeAudioData(audioData, { type: "mp3" }).then((arrayBuffer) => {
214214
- `ChannelMergerNode`
215215
- `ChannelSplitterNode`
216216
- `DelayNode` (noisy..)
217+
- `DynamicsCompressorNode`
217218
- `GainNode`
218219
- `IIRFIlterNode`
219220
- `OscillatorNode` (use wave-table synthesis, not use periodic wave)

src/impl/DynamicsCompressorNode.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use strict";
22

33
const AudioNode = require("./AudioNode");
4-
const DynamicsCompressorNodeDSP = require("./dsp/DynamicsCompressorNode");
4+
const { DynamicsCompressor, CompressorParameters } = require("./dsp/DynamicsCompressor");
55
const { defaults } = require("../utils");
66
const { EXPLICIT } = require("../constants/ChannelCountMode");
77
const { CONTROL_RATE } = require("../constants/AudioParamRate");
@@ -41,24 +41,26 @@ class DynamicsCompressorNode extends AudioNode {
4141
this._ratio = this.addParam(CONTROL_RATE, ratio);
4242
this._attack = this.addParam(CONTROL_RATE, attack);
4343
this._release = this.addParam(CONTROL_RATE, release);
44+
45+
this.compressor = new DynamicsCompressor(this.sampleRate, this.outputs[0].getNumberOfChannels());
4446
}
4547

4648
/**
47-
* @param {AudioParam}
49+
* @return {AudioParam}
4850
*/
4951
getThreshold() {
5052
return this._threshold;
5153
}
5254

5355
/**
54-
* @param {AudioParam}
56+
* @return {AudioParam}
5557
*/
5658
getKnee() {
5759
return this._knee;
5860
}
5961

6062
/**
61-
* @param {AudioParam}
63+
* @return {AudioParam}
6264
*/
6365
getRatio() {
6466
return this._ratio;
@@ -67,26 +69,39 @@ class DynamicsCompressorNode extends AudioNode {
6769
/**
6870
* @return {number}
6971
*/
70-
/* istanbul ignore next */
7172
getReduction() {
72-
throw new TypeError("NOT YET IMPLEMENTED");
73+
return this.compressor.parameterValue(CompressorParameters.REDUCTION);
7374
}
7475

7576
/**
76-
* @param {AudioParam}
77+
* @return {AudioParam}
7778
*/
7879
getAttack() {
7980
return this._attack;
8081
}
8182

8283
/**
83-
* @param {AudioParam}
84+
* @return {AudioParam}
8485
*/
8586
getRelease() {
8687
return this._release;
8788
}
88-
}
8989

90-
Object.assign(DynamicsCompressorNode.prototype, DynamicsCompressorNodeDSP);
90+
dspInit() {
91+
super.dspInit();
92+
}
93+
94+
dspProcess() {
95+
super.dspProcess();
96+
97+
this.compressor.setParameterValue(CompressorParameters.THRESHOLD, this._threshold.getValue());
98+
this.compressor.setParameterValue(CompressorParameters.KNEE, this._knee.getValue());
99+
this.compressor.setParameterValue(CompressorParameters.RATIO, this._ratio.getValue());
100+
this.compressor.setParameterValue(CompressorParameters.ATTACK, this._attack.getValue());
101+
this.compressor.setParameterValue(CompressorParameters.RELEASE, this._release.getValue());
102+
103+
this.compressor.dspProcess(this.inputs, this.outputs, this.blockSize);
104+
}
105+
}
91106

92107
module.exports = DynamicsCompressorNode;

src/impl/dsp/BiquadFilterKernel.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use strict";
22

3+
const { flushDenormalFloatToZero } = require('../../utils');
4+
35
class BiquadFilterKernel {
46
constructor() {
57
this.coefficients = [ 0, 0, 0, 0, 0 ];
@@ -87,8 +89,4 @@ class BiquadFilterKernel {
8789
}
8890
}
8991

90-
function flushDenormalFloatToZero(f) {
91-
return (Math.abs(f) < 1.175494e-38) ? 0.0 : f;
92-
}
93-
9492
module.exports = BiquadFilterKernel;

src/impl/dsp/DynamicsCompressor.js

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"use strict";
2+
3+
// Port from Chromium
4+
// https://chromium.googlesource.com/chromium/blink/+/master/Source/platform/audio/DynamicsCompressor.cpp
5+
6+
const assert = require("assert");
7+
const nmap = require("nmap");
8+
const DynamicsCompressorKernel = require("./DynamicsCompressorKernel");
9+
10+
const THRESHOLD = 0;
11+
const KNEE = 1;
12+
const RATIO = 2;
13+
const ATTACK = 3;
14+
const RELEASE = 4;
15+
const PRE_DELAY = 5;
16+
const RELEASE_ZONE_1 = 6;
17+
const RELEASE_ZONE_2 = 7;
18+
const RELEASE_ZONE_3 = 8;
19+
const RELEASE_ZONE_4 = 9;
20+
const POST_GAIN = 10;
21+
const FILTER_STAGE_GAIN = 11;
22+
const FILTER_STAGE_RATIO = 12;
23+
const FILTER_ANCHOR = 13;
24+
const EFFECT_BLEND = 14;
25+
const REDUCTION = 15;
26+
const PARAM_LAST = 16;
27+
28+
const CompressorParameters = {
29+
THRESHOLD,
30+
KNEE,
31+
RATIO,
32+
ATTACK,
33+
RELEASE,
34+
REDUCTION,
35+
};
36+
37+
class DynamicsCompressor {
38+
constructor(sampleRate, numberOfChannels) {
39+
this.numberOfChannels = numberOfChannels;
40+
this.sampleRate = sampleRate;
41+
this.nyquist = sampleRate / 2;
42+
this.compressor = new DynamicsCompressorKernel(sampleRate, numberOfChannels);
43+
44+
this.lastFilterStageRatio = -1;
45+
this.lastAnchor = -1;
46+
this.lastFilterStageGain = -1;
47+
48+
this.parameters = new Array(PARAM_LAST);
49+
50+
this.setNumberOfChannels(numberOfChannels);
51+
this.initializeParameters();
52+
}
53+
54+
setParameterValue(parameterId, value) {
55+
if (parameterId < PARAM_LAST) {
56+
this.parameters[parameterId] = value;
57+
}
58+
}
59+
60+
initializeParameters() {
61+
// Initializes compressor to default values.
62+
63+
this.parameters[THRESHOLD] = -24; // dB
64+
this.parameters[KNEE] = 30; // dB
65+
this.parameters[RATIO] = 12; // unit-less
66+
this.parameters[ATTACK] = 0.003; // seconds
67+
this.parameters[RELEASE] = 0.250; // seconds
68+
this.parameters[PRE_DELAY] = 0.006; // seconds
69+
70+
// Release zone values 0 -> 1.
71+
this.parameters[RELEASE_ZONE_1] = 0.09;
72+
this.parameters[RELEASE_ZONE_2] = 0.16;
73+
this.parameters[RELEASE_ZONE_3] = 0.42;
74+
this.parameters[RELEASE_ZONE_4] = 0.98;
75+
this.parameters[FILTER_STAGE_GAIN] = 4.4; // dB
76+
this.parameters[FILTER_STAGE_RATIO] = 2;
77+
this.parameters[FILTER_ANCHOR] = 15000 / this.nyquist;
78+
this.parameters[POST_GAIN] = 0; // dB
79+
this.parameters[REDUCTION] = 0; // dB
80+
81+
// Linear crossfade (0 -> 1).
82+
this.parameters[EFFECT_BLEND] = 1;
83+
}
84+
85+
parameterValue(parameterId) {
86+
return this.parameters[parameterId];
87+
}
88+
89+
process(sourceBus, destinatonBus, framesToProcess) {
90+
// Though numberOfChannels is retrived from destinationBus, we still name it numberOfChannels instead of numberOfDestinationChannels.
91+
// It's because we internally match sourceChannels's size to destinationBus by channel up/down mix. Thus we need numberOfChannels
92+
// to do the loop work for both m_sourceChannels and m_destinationChannels.
93+
94+
const numberOfChannels = destinatonBus.getNumberOfChannels();
95+
const numberOfSourceChannels = sourceBus.getNumberOfChannels();
96+
97+
assert(numberOfChannels === this.numberOfChannels && numberOfChannels > 0);
98+
99+
if (numberOfChannels !== this.numberOfChannels || numberOfSourceChannels === 0) {
100+
destinatonBus.zeros();
101+
return;
102+
}
103+
104+
switch (numberOfChannels) {
105+
case 2: { // stereo
106+
this.sourceChannels[0] = sourceBus.getChannelData()[0];
107+
108+
if (numberOfSourceChannels > 1) {
109+
this.sourceChannels[1] = sourceBus.getChannelData()[1];
110+
} else {
111+
// Simply duplicate mono channel input data to right channel for stereo processing.
112+
this.sourceChannels[1] = sourceBus.getChannelData()[0];
113+
}
114+
break;
115+
}
116+
default: {
117+
// FIXME : support other number of channels.
118+
assert(false);
119+
destinatonBus.zeros();
120+
return;
121+
}
122+
}
123+
124+
for (let i = 0; i < numberOfChannels; i++) {
125+
this.destinationChannels[i] = destinatonBus.getMutableData()[i];
126+
}
127+
128+
const filterStageGain = this.parameterValue(FILTER_STAGE_GAIN);
129+
const filterStageRatio = this.parameterValue(FILTER_STAGE_RATIO);
130+
const anchor = this.parameterValue(FILTER_ANCHOR);
131+
132+
if (
133+
filterStageGain !== this.lastFilterStageGain ||
134+
filterStageRatio !== this.lastFilterStageRatio ||
135+
anchor !== this.lastAnchor
136+
) {
137+
this.lastFilterStageGain = filterStageGain;
138+
this.lastFilterStageRatio = filterStageRatio;
139+
this.lastAnchor = anchor;
140+
}
141+
142+
const dbThreshold = this.parameterValue(THRESHOLD);
143+
const dbKnee = this.parameterValue(KNEE);
144+
const ratio = this.parameterValue(RATIO);
145+
const attackTime = this.parameterValue(ATTACK);
146+
const releaseTime = this.parameterValue(RELEASE);
147+
const preDelayTime = this.parameterValue(PRE_DELAY);
148+
149+
// This is effectively a master volume on the compressed signal (pre-blending).
150+
const dbPostGain = this.parameterValue(POST_GAIN);
151+
152+
// Linear blending value from dry to completely processed (0 -> 1)
153+
// 0 means the signal is completely unprocessed.
154+
// 1 mixes in only the compressed signal.
155+
const effectBlend = this.parameterValue(EFFECT_BLEND);
156+
157+
const releaseZone1 = this.parameterValue(RELEASE_ZONE_1);
158+
const releaseZone2 = this.parameterValue(RELEASE_ZONE_2);
159+
const releaseZone3 = this.parameterValue(RELEASE_ZONE_3);
160+
const releaseZone4 = this.parameterValue(RELEASE_ZONE_4);
161+
162+
this.compressor.process(
163+
this.sourceChannels,
164+
this.destinationChannels,
165+
numberOfChannels,
166+
framesToProcess,
167+
168+
dbThreshold,
169+
dbKnee,
170+
ratio,
171+
attackTime,
172+
releaseTime,
173+
preDelayTime,
174+
dbPostGain,
175+
effectBlend,
176+
177+
releaseZone1,
178+
releaseZone2,
179+
releaseZone3,
180+
releaseZone4
181+
);
182+
183+
// Update the compression amount.
184+
this.setParameterValue(REDUCTION, this.compressor.meteringGain);
185+
}
186+
187+
reset() {
188+
this.lastFilterStageRatio = -1; // for recalc
189+
this.lastAnchor = -1;
190+
this.lastFilterStageGain = -1;
191+
192+
this.compressor.reset();
193+
}
194+
195+
setNumberOfChannels(numberOfChannels) {
196+
this.sourceChannels = nmap(numberOfChannels, () => new Float32Array());
197+
this.destinationChannels = nmap(numberOfChannels, () => new Float32Array());
198+
199+
this.compressor.setNumberOfChannels(numberOfChannels);
200+
this.numberOfChannels = numberOfChannels;
201+
}
202+
203+
dspProcess(inputs, outputs, blockSize) {
204+
this.process(inputs[0].bus, outputs[0].bus, blockSize);
205+
}
206+
}
207+
208+
module.exports = {
209+
DynamicsCompressor,
210+
CompressorParameters,
211+
};

0 commit comments

Comments
 (0)