tone 15.3.12 → 15.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Tone/core/worklet/DelayLine.worklet.ts +25 -2
- package/Tone/core/worklet/ToneAudioWorklet.ts +10 -2
- package/Tone/effect/ReverseDelay.test.ts +52 -0
- package/Tone/effect/ReverseDelay.ts +192 -0
- package/Tone/effect/ReverseDelay.worklet.ts +41 -0
- package/Tone/effect/index.ts +1 -0
- package/Tone/version.ts +1 -1
- package/build/Tone.js +1 -1
- package/build/Tone.js.map +1 -1
- package/build/esm/core/worklet/DelayLine.worklet.js +25 -2
- package/build/esm/core/worklet/DelayLine.worklet.js.map +1 -1
- package/build/esm/core/worklet/ToneAudioWorklet.d.ts +3 -1
- package/build/esm/core/worklet/ToneAudioWorklet.js +4 -0
- package/build/esm/core/worklet/ToneAudioWorklet.js.map +1 -1
- package/build/esm/effect/ReverseDelay.d.ts +48 -0
- package/build/esm/effect/ReverseDelay.js +116 -0
- package/build/esm/effect/ReverseDelay.js.map +1 -0
- package/build/esm/effect/ReverseDelay.worklet.d.ts +4 -0
- package/build/esm/effect/ReverseDelay.worklet.js +38 -0
- package/build/esm/effect/ReverseDelay.worklet.js.map +1 -0
- package/build/esm/effect/index.d.ts +1 -0
- package/build/esm/effect/index.js +1 -0
- package/build/esm/effect/index.js.map +1 -1
- package/build/esm/version.js +1 -1
- package/build/esm/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ const delayLine = /* javascript */ `
|
|
|
5
5
|
* A multichannel buffer for use within an AudioWorkletProcessor as a delay line
|
|
6
6
|
*/
|
|
7
7
|
class DelayLine {
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
constructor(size, channels) {
|
|
10
10
|
this.buffer = [];
|
|
11
11
|
this.writeHead = []
|
|
@@ -25,7 +25,7 @@ const delayLine = /* javascript */ `
|
|
|
25
25
|
*/
|
|
26
26
|
push(channel, value) {
|
|
27
27
|
this.writeHead[channel] += 1;
|
|
28
|
-
if (this.writeHead[channel]
|
|
28
|
+
if (this.writeHead[channel] >= this.size) {
|
|
29
29
|
this.writeHead[channel] = 0;
|
|
30
30
|
}
|
|
31
31
|
this.buffer[channel][this.writeHead[channel]] = value;
|
|
@@ -43,6 +43,29 @@ const delayLine = /* javascript */ `
|
|
|
43
43
|
}
|
|
44
44
|
return this.buffer[channel][readHead];
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the reverse recorded value of the channel given the delay
|
|
49
|
+
* @param channel number
|
|
50
|
+
* @param delay number delay samples
|
|
51
|
+
*/
|
|
52
|
+
getReverse(channel, delay) {
|
|
53
|
+
if (!this.size || !delay) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const readHead = delay * 2 - this.writeHead[channel] - 1;
|
|
58
|
+
|
|
59
|
+
// Gain function to reduce clicking when read is too close to write
|
|
60
|
+
let readWriteDifference = this.writeHead[channel] - readHead;
|
|
61
|
+
if (readWriteDifference < 0) {
|
|
62
|
+
readWriteDifference += this.size
|
|
63
|
+
}
|
|
64
|
+
const delayPct = readWriteDifference / this.size;
|
|
65
|
+
const gainFunction = 4 * delayPct * (1 - delayPct);
|
|
66
|
+
|
|
67
|
+
return this.buffer[channel][readHead] * gainFunction;
|
|
68
|
+
}
|
|
46
69
|
}
|
|
47
70
|
`;
|
|
48
71
|
|
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
import { noOp } from "../util/Interface.js";
|
|
6
6
|
import { getWorkletGlobalScope } from "./WorkletGlobalScope.js";
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
|
|
8
|
+
export interface ToneAudioWorkletOptions extends ToneAudioNodeOptions {
|
|
9
|
+
workletOptions?: Partial<AudioWorkletNodeOptions>
|
|
10
|
+
};
|
|
10
11
|
export abstract class ToneAudioWorklet<
|
|
11
12
|
Options extends ToneAudioWorkletOptions,
|
|
12
13
|
> extends ToneAudioNode<Options> {
|
|
@@ -63,6 +64,13 @@ export abstract class ToneAudioWorklet<
|
|
|
63
64
|
this.context
|
|
64
65
|
);
|
|
65
66
|
|
|
67
|
+
if (options.workletOptions) {
|
|
68
|
+
this.workletOptions = Object.assign({},
|
|
69
|
+
this.workletOptions,
|
|
70
|
+
options.workletOptions
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
if (workletPromise === undefined) {
|
|
67
75
|
workletPromise = this.context.addAudioWorkletModule(blobUrl);
|
|
68
76
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
|
|
3
|
+
import { BasicTests } from "../../test/helper/Basic.js";
|
|
4
|
+
import { CompareToFile } from "../../test/helper/CompareToFile.js";
|
|
5
|
+
import { EffectTests } from "../../test/helper/EffectTests.js";
|
|
6
|
+
import { Oscillator } from "../source/oscillator/Oscillator.js";
|
|
7
|
+
import { ReverseDelay } from "./ReverseDelay.js";
|
|
8
|
+
|
|
9
|
+
describe("ReverseDelay", () => {
|
|
10
|
+
BasicTests(ReverseDelay);
|
|
11
|
+
EffectTests(ReverseDelay, 0.01);
|
|
12
|
+
|
|
13
|
+
context("API", () => {
|
|
14
|
+
it("matches a file", () => {
|
|
15
|
+
return CompareToFile(() => {
|
|
16
|
+
const delay = new ReverseDelay({
|
|
17
|
+
delayTime: 0.2,
|
|
18
|
+
feedback: 0.5,
|
|
19
|
+
wet: 0.5,
|
|
20
|
+
}).toDestination();
|
|
21
|
+
const osc = new Oscillator().connect(delay);
|
|
22
|
+
osc.start(0);
|
|
23
|
+
osc.volume.linearRampToValueAtTime(0, 0.1);
|
|
24
|
+
osc.volume.exponentialRampToValueAtTime(-Infinity, 0.2);
|
|
25
|
+
}, "reverseDelay.wav");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("can pass in options in the constructor", () => {
|
|
29
|
+
const reverse = new ReverseDelay({
|
|
30
|
+
delayTime: 1.25,
|
|
31
|
+
feedback: 0.75,
|
|
32
|
+
});
|
|
33
|
+
expect(reverse.delayTime).to.equal(1.25);
|
|
34
|
+
expect(reverse.feedback).to.equal(0.75);
|
|
35
|
+
reverse.dispose();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("can get/set the options", () => {
|
|
39
|
+
const reverse = new ReverseDelay(1.25, 0.75);
|
|
40
|
+
expect(reverse.delayTime).to.equal(1.25);
|
|
41
|
+
expect(reverse.feedback).to.equal(0.75);
|
|
42
|
+
reverse.set({
|
|
43
|
+
delayTime: "2n",
|
|
44
|
+
feedback: 0.5,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(reverse.get().delayTime).to.be.closeTo(1, 0.01);
|
|
48
|
+
expect(reverse.get().feedback).to.be.closeTo(0.5, 0.01);
|
|
49
|
+
reverse.dispose();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Gain } from "../core/context/Gain.js";
|
|
2
|
+
import { Param } from "../core/context/Param.js";
|
|
3
|
+
import { connectSeries } from "../core/context/ToneAudioNode.js";
|
|
4
|
+
import { NormalRange, Seconds, Time } from "../core/type/Units.js";
|
|
5
|
+
import { optionsFromArguments } from "../core/util/Defaults.js";
|
|
6
|
+
import {
|
|
7
|
+
ToneAudioWorklet,
|
|
8
|
+
ToneAudioWorkletOptions,
|
|
9
|
+
} from "../core/worklet/ToneAudioWorklet.js";
|
|
10
|
+
import { EffectOptions } from "./Effect.js";
|
|
11
|
+
import { Effect } from "./Effect.js";
|
|
12
|
+
import { workletName } from "./ReverseDelay.worklet.js";
|
|
13
|
+
|
|
14
|
+
export interface ReverseDelayOptions extends EffectOptions {
|
|
15
|
+
delayTime: Time;
|
|
16
|
+
feedback: NormalRange;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A feedback delay effect that plays the echos in reverse.
|
|
21
|
+
* Algorithm and gain function found in [this pdf](https://ccrma.stanford.edu/~jingjiez/portfolio/echoing-harmonics/pdfs/A%20Pitch%20Shifting%20Reverse%20Echo%20Audio%20Effect.pdf)
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const reverse = new Tone.ReverseDelay(1.5, 0.75).toDestination();
|
|
25
|
+
* const synth = new Tone.Synth().connect(reverse);
|
|
26
|
+
* synth.triggerAttackRelease("C4", "2n");
|
|
27
|
+
* @category Effect
|
|
28
|
+
*/
|
|
29
|
+
export class ReverseDelay extends Effect<ReverseDelayOptions> {
|
|
30
|
+
readonly name: string = "ReverseDelay";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The node that does the reverse delay effect.
|
|
34
|
+
*/
|
|
35
|
+
private _reverseDelayWorklet: ReverseDelayWorklet;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param delayTime The amount of time the incoming signal will be delayed and reversed.
|
|
39
|
+
* @param feedback The amount of signal which is fed back through the delay.
|
|
40
|
+
*/
|
|
41
|
+
constructor(delayTime?: Time, feedback?: NormalRange);
|
|
42
|
+
constructor(options?: Partial<ReverseDelayOptions>);
|
|
43
|
+
constructor() {
|
|
44
|
+
const options = optionsFromArguments(
|
|
45
|
+
ReverseDelay.getDefaults(),
|
|
46
|
+
arguments,
|
|
47
|
+
["delayTime", "feedback"]
|
|
48
|
+
);
|
|
49
|
+
super(options);
|
|
50
|
+
|
|
51
|
+
this._reverseDelayWorklet = this._connectWorklet(
|
|
52
|
+
options.delayTime,
|
|
53
|
+
options.feedback
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private _connectWorklet(
|
|
58
|
+
delayTime: Time,
|
|
59
|
+
feedback: NormalRange
|
|
60
|
+
): ReverseDelayWorklet {
|
|
61
|
+
const worklet = new ReverseDelayWorklet({
|
|
62
|
+
context: this.context,
|
|
63
|
+
delayTime: this.toSeconds(delayTime),
|
|
64
|
+
feedback,
|
|
65
|
+
});
|
|
66
|
+
this.connectEffect(worklet);
|
|
67
|
+
|
|
68
|
+
return worklet;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The amount of time the incoming signal is delayed and reversed
|
|
73
|
+
*/
|
|
74
|
+
get delayTime(): Time {
|
|
75
|
+
return this._reverseDelayWorklet.delayTime;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
set delayTime(delayTime) {
|
|
79
|
+
const prev = this._reverseDelayWorklet;
|
|
80
|
+
this._reverseDelayWorklet = this._connectWorklet(
|
|
81
|
+
delayTime,
|
|
82
|
+
this.feedback
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Prevent sudden stop when disposing previous worklet
|
|
86
|
+
prev.output.gain.linearRampTo(0, this.toSeconds(this.delayTime));
|
|
87
|
+
this.context.setTimeout(
|
|
88
|
+
() => prev.dispose(),
|
|
89
|
+
this.toSeconds(this.delayTime)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The amount of signal which is fed back through the delay.
|
|
95
|
+
*/
|
|
96
|
+
get feedback(): NormalRange {
|
|
97
|
+
return this._reverseDelayWorklet.feedback.value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
set feedback(feedback) {
|
|
101
|
+
this._reverseDelayWorklet.set({ feedback });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static getDefaults(): ReverseDelayOptions {
|
|
105
|
+
return Object.assign(Effect.getDefaults(), {
|
|
106
|
+
wet: 0.5,
|
|
107
|
+
delayTime: 1,
|
|
108
|
+
feedback: 0.5,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
dispose(): this {
|
|
113
|
+
super.dispose();
|
|
114
|
+
this._reverseDelayWorklet.dispose();
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ReverseDelayWorkletOptions extends ToneAudioWorkletOptions {
|
|
120
|
+
delayTime: Seconds;
|
|
121
|
+
feedback: NormalRange;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Internal class which creates an AudioWorklet to reverse the delay signal
|
|
126
|
+
*/
|
|
127
|
+
class ReverseDelayWorklet extends ToneAudioWorklet<ReverseDelayWorkletOptions> {
|
|
128
|
+
readonly name: string = "ReverseDelayWorklet";
|
|
129
|
+
|
|
130
|
+
readonly input: Gain;
|
|
131
|
+
readonly output: Gain;
|
|
132
|
+
|
|
133
|
+
readonly delayTime: Seconds;
|
|
134
|
+
readonly feedback: Param<"normalRange">;
|
|
135
|
+
|
|
136
|
+
constructor(options?: Partial<ReverseDelayOptions>);
|
|
137
|
+
constructor() {
|
|
138
|
+
const options = optionsFromArguments(
|
|
139
|
+
ReverseDelayWorklet.getDefaults(),
|
|
140
|
+
arguments,
|
|
141
|
+
["delayTime", "feedback"]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
super({
|
|
145
|
+
...options,
|
|
146
|
+
workletOptions: {
|
|
147
|
+
processorOptions: {
|
|
148
|
+
delayTime: options.delayTime,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.input = new Gain({ context: this.context });
|
|
154
|
+
this.output = new Gain({ context: this.context });
|
|
155
|
+
|
|
156
|
+
this.delayTime = options.delayTime;
|
|
157
|
+
this.feedback = new Param<"normalRange">({
|
|
158
|
+
context: this.context,
|
|
159
|
+
value: options.feedback,
|
|
160
|
+
units: "normalRange",
|
|
161
|
+
param: this._dummyParam,
|
|
162
|
+
swappable: true,
|
|
163
|
+
minValue: 0,
|
|
164
|
+
maxValue: 0.9999,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static getDefaults(): ReverseDelayWorkletOptions {
|
|
169
|
+
return Object.assign(ToneAudioWorklet.getDefaults(), {
|
|
170
|
+
delayTime: 1,
|
|
171
|
+
feedback: 0.5,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
protected _audioWorkletName(): string {
|
|
176
|
+
return workletName;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
onReady(node: AudioWorkletNode): void {
|
|
180
|
+
connectSeries(this.input, node, this.output);
|
|
181
|
+
const feedback = node.parameters.get("feedback") as AudioParam;
|
|
182
|
+
this.feedback.setParam(feedback);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
dispose(): this {
|
|
186
|
+
super.dispose();
|
|
187
|
+
this.input.dispose();
|
|
188
|
+
this.output.dispose();
|
|
189
|
+
this.feedback.dispose();
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import "../core/worklet/SingleIOProcessor.worklet.js";
|
|
2
|
+
import "../core/worklet/DelayLine.worklet.js";
|
|
3
|
+
|
|
4
|
+
import { registerProcessor } from "../core/worklet/WorkletGlobalScope.js";
|
|
5
|
+
|
|
6
|
+
export const workletName = "reverse-delay";
|
|
7
|
+
|
|
8
|
+
export const reverseDelayWorklet = /* javascript */ `
|
|
9
|
+
class ReverseDelayWorklet extends SingleIOProcessor {
|
|
10
|
+
|
|
11
|
+
constructor(options) {
|
|
12
|
+
super(options);
|
|
13
|
+
this.delayTime = Math.floor(this.sampleRate * options.processorOptions.delayTime);
|
|
14
|
+
const channels = options.channelCount || 2;
|
|
15
|
+
this.delayLine = new DelayLine(this.delayTime * 2, channels);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static get parameterDescriptors() {
|
|
19
|
+
return [{
|
|
20
|
+
name: "feedback",
|
|
21
|
+
defaultValue: 0.5,
|
|
22
|
+
minValue: 0,
|
|
23
|
+
maxValue: 0.9999,
|
|
24
|
+
automationRate: "k-rate"
|
|
25
|
+
}];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
generate(input, channel, parameters) {
|
|
29
|
+
const reversedSample = this.delayLine.getReverse(channel, this.delayTime);
|
|
30
|
+
const delayedSample = this.delayLine.get(channel, this.delayTime);
|
|
31
|
+
|
|
32
|
+
// Push the forward sample back on the line
|
|
33
|
+
this.delayLine.push(channel, input + delayedSample * parameters.feedback);
|
|
34
|
+
|
|
35
|
+
// Play the reversed sample
|
|
36
|
+
return reversedSample;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
registerProcessor(workletName, reverseDelayWorklet);
|
package/Tone/effect/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from "./Phaser.js";
|
|
|
13
13
|
export * from "./PingPongDelay.js";
|
|
14
14
|
export * from "./PitchShift.js";
|
|
15
15
|
export * from "./Reverb.js";
|
|
16
|
+
export * from "./ReverseDelay.js";
|
|
16
17
|
export * from "./StereoWidener.js";
|
|
17
18
|
export * from "./Tremolo.js";
|
|
18
19
|
export * from "./Vibrato.js";
|
package/Tone/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version: string = "15.
|
|
1
|
+
export const version: string = "15.4.0";
|