-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcompressor.js
More file actions
190 lines (186 loc) · 8.34 KB
/
Copy pathcompressor.js
File metadata and controls
190 lines (186 loc) · 8.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
class Compressor extends AudioWorkletProcessor {
NUM_SAMPLES = 128;
SAMPLES_PER_SECOND = 44100;
constructor() {
super();
this.port.onmessage = this.onMessage.bind(this);
}
// for the defaults I just copied audacity's settings
compressorSettings = {
noiseFloorDB: -40,
thresholdDB: -10,
ratioToOne: 2,
inverseRatio: null,
attackTimeSecs: 0.2,
releaseTimeSecs: 1,
knee0to1: 0.0,
compressFromPeak: false,
amplifyToMax: true,
peakDB: -1,
compressorFn: function(sample) {
return sample * this.compressorSettings.inverseRatio;
}
};
//some settings will be preprocessed to avoid doing a lot of mults/divs
settingsInSamples = {
noiseFloor: 0,
noiseFloorInverse: -0,
threshold: 0,
thresholdInverse: -0,
kneeLowerBound: 0,
kneeLowerBoundInverse: -0,
attackTimeSamples: 0,
releaseTimeSamples: 0,
peak: 0,
peakInverse: -0,
maxSamplesToTrack: this.SAMPLES_PER_SECOND * 15
}
//since we are processing the audio online we have to keep track of statistics about the stream
compressorState = {
numSamplesAboveThreshold: 0,
numSamplesBelowNoiseFloor: 0,
isCompressing: false,
//online amplification is going to be difficult because we need to keep a buffer
// of the peak sample, but how do we ensure there is always a peak in the buffer
// while expiring them after a certain time?
peaks: [
]
}
process (inputs, outputs, parameters) {
this.compress(inputs, outputs);
return true;
}
//main loop for compressor
compress(inputs, outputs){
let inputStream = inputs[0];
let outputStream = outputs[0];
let channelIndex = 0;
while(channelIndex < inputStream.length) {
this.handleChannel(inputStream, outputStream, channelIndex);
channelIndex++;
}
}
//handle the process for a single channel
handleChannel(inputStream, outputStream, channelIndex){
let sampleIndex = 0;
let inputChannel = inputStream[channelIndex];
let outputChannel = outputStream[channelIndex];
//loop through the channel's samples and compress if we should do that
while(sampleIndex < inputChannel.length){
let sample = inputChannel[sampleIndex];
this.updateCompressorState(sample);
if(this.compressorState.isCompressing){
sample = this.getCompressedSample(sample);
}
outputChannel[sampleIndex] = sample;
sampleIndex++;
}
//if amplify is true, amp max amplitude to -1dB
//this may not be very useful because of the very short timeframe
if(this.amplifyToMax){
this.amplifyFrame(outputChannel)
}
}
//get the compressed value of the sample
//todo: implement soft knee. Not sure how to do this because I'm very stupid
getCompressedSample(sample, knee) {
//multiplications are faster so use the inverse of the ratio.
return sample * this.compressorSettings.inverseRatio;
}
//do not use yet. I need to keep track of peaks in a buffer that drops expired values
amplifyFrame(outputChannel) {
let maxSample = inputChannel.reduce((max, sample) => sample > max ? sample : max);
let amplifyAmount = this.settingsInSamples.peak - maxSample;
if(maxSample < this.settingsInSamples.peak) {
outputChannel.forEach((sample, index) => {
outputChannel[index] = sample + amplifyAmount;
});
}
}
//checks whether or not the sample should be compressed, as well as updating the statistics for the stream
updateCompressorState(sample) {
let testSample = Math.abs(sample);
//Since this runs online, we need to keep track of the number of samples above the noise floor
// in order to make the attack time work.
// Reset the count when we've passed the release time.
//todo: the compressor doesn't kick in until the attack time completes which means there's a loud part
// if the sound comes very fast. maybe it should compress the whole buffer immediately if the threshold
// is surpassed. it would also really help to have a buffer that's at least the attack time if possible.
let compressionMinimum = this.settingsInSamples.kneeLowerBound > 0 && this.settingsInSamples.kneeLowerBound
|| this.settingsInSamples.threshold
if(testSample >= compressionMinimum) {
this.compressorState.numSamplesAboveThreshold = Math.min(
this.compressorState.numSamplesAboveThreshold + 1,
this.settingsInSamples.maxSamplesToTrack
);
} else {
this.compressorState.numSamplesBelowNoiseFloor = Math.min(
this.compressorState.numSamplesBelowNoiseFloor + 1,
this.settingsInSamples.maxSamplesToTrack
);;
}
let beforeAttack = this.compressorState.numSamplesAboveThreshold < this.settingsInSamples.attackTimeSamples;
let afterRelease = this.compressorState.numSamplesBelowNoiseFloor >= this.settingsInSamples.releaseTimeSamples;
//reset the release count if we're past the attack threshold
if (beforeAttack) {
this.compressorState.numSamplesBelowNoiseFloor = 0;
}
//reset the attack count if we're past the release threshold
if (afterRelease) {
this.compressorState.numSamplesAboveThreshold = 0;
}
//if we're before the attack time or after the release time, don't compress
if(beforeAttack || afterRelease) {
this.compressorState.isCompressing = false;
} else {
this.compressorState.isCompressing = true;
}
}
//preprocess the settings values so we can avoid doing as much math on the stream
getSettingsSampleValues() {
if(this.compressorSettings.knee0to1) {
this.compressorSettings.compressorFn = function(sample) {
const baseChange = 1 / Math.log10(this.compressorSettings.ratioToOne);
return Math.log10(sample) * baseChange;
}
}
this.compressorSettings.inverseRatio = 1 / this.compressorSettings.ratioToOne;
this.settingsInSamples.attackTimeSamples = this.timeToSampleCount(this.compressorSettings.attackTimeSecs);
this.settingsInSamples.releaseTimeSamples = this.timeToSampleCount(this.compressorSettings.releaseTimeSecs);
this.settingsInSamples.noiseFloor = this.dBFSToSample(this.compressorSettings.noiseFloorDB);
this.settingsInSamples.threshold = this.dBFSToSample(this.compressorSettings.thresholdDB);
//this starts the knee at half of the range between the noise floor and threshold.
// no idea if this is valid haha lol
this.settingsInSamples.kneeLowerBound = this.compressorSettings.knee0to1
* ((this.settingsInSamples.threshold - this.settingsInSamples.noiseFloor) / 2);
this.settingsInSamples.peak = this.dBFSToSample(this.compressorSettings.peakDB);
//to avoid counts running out of control we will reset them after hitting a ceiling
this.settingsInSamples.maxSamplesToTrack = Math.max(
this.settingsInSamples.attackTimeSamples,
this.settingsInSamples.releaseTimeSamples
) * 2;
}
//the only message that's supported thus far is a serialized settings object
onMessage(evt) {
let newCompressorSettings = JSON.parse(evt.data.compressorSettings);
Object.assign(this.compressorSettings, newCompressorSettings);
this.getSettingsSampleValues();
}
//convert a -1 to 1 sample value to DBFS
sampleToDBFS(sample) {
return 20 * Math.log10(sample);
}
//convert DBFS to a -1 to 1 sample value
dBFSToSample(dbfs) {
return Math.pow(10, dbfs / 20.0)
}
//convert an amount of time in seconds to a number of samples
timeToSampleCount(timeSeconds){
return timeSeconds * this.SAMPLES_PER_SECOND
}
//convert a number of samples to a time in seconds.
sampleCountToSeconds(numSamples) {
return numSamples / this.SAMPLES_PER_SECOND;
}
}
registerProcessor('compressor', Compressor);