UNPKG

5.83 kBPlain TextView Raw
1import { Interval, Seconds, Time } from "../core/type/Units.js";
2import { FeedbackEffect, FeedbackEffectOptions } from "./FeedbackEffect.js";
3import { optionsFromArguments } from "../core/util/Defaults.js";
4import { LFO } from "../source/oscillator/LFO.js";
5import { Delay } from "../core/context/Delay.js";
6import { CrossFade } from "../component/channel/CrossFade.js";
7import { Signal } from "../signal/Signal.js";
8import { readOnly } from "../core/util/Interface.js";
9import { Param } from "../core/context/Param.js";
10import { intervalToFrequencyRatio } from "../core/type/Conversions.js";
11
12export interface PitchShiftOptions extends FeedbackEffectOptions {
13 pitch: Interval;
14 windowSize: Seconds;
15 delayTime: Time;
16}
17
18/**
19 * PitchShift does near-realtime pitch shifting to the incoming signal.
20 * The effect is achieved by speeding up or slowing down the delayTime
21 * of a DelayNode using a sawtooth wave.
22 * Algorithm found in [this pdf](http://dsp-book.narod.ru/soundproc.pdf).
23 * Additional reference by [Miller Pucket](http://msp.ucsd.edu/techniques/v0.11/book-html/node115.html).
24 * @category Effect
25 */
26export class PitchShift extends FeedbackEffect<PitchShiftOptions> {
27 readonly name: string = "PitchShift";
28
29 /**
30 * The pitch signal
31 */
32 private _frequency: Signal<"frequency">;
33
34 /**
35 * Uses two DelayNodes to cover up the jump in the sawtooth wave.
36 */
37 private _delayA: Delay;
38
39 /**
40 * The first LFO.
41 */
42 private _lfoA: LFO;
43
44 /**
45 * The second DelayNode
46 */
47 private _delayB: Delay;
48
49 /**
50 * The second LFO.
51 */
52 private _lfoB: LFO;
53
54 /**
55 * Cross fade quickly between the two delay lines to cover up the jump in the sawtooth wave
56 */
57 private _crossFade: CrossFade;
58
59 /**
60 * LFO which alternates between the two delay lines to cover up the disparity in the
61 * sawtooth wave.
62 */
63 private _crossFadeLFO: LFO;
64
65 /**
66 * The delay node
67 */
68 private _feedbackDelay: Delay;
69
70 /**
71 * The amount of delay on the input signal
72 */
73 readonly delayTime: Param<"time">;
74
75 /**
76 * Hold the current pitch
77 */
78 private _pitch: Interval;
79
80 /**
81 * Hold the current windowSize
82 */
83 private _windowSize;
84
85 /**
86 * @param pitch The interval to transpose the incoming signal by.
87 */
88 constructor(pitch?: Interval);
89 constructor(options?: Partial<PitchShiftOptions>);
90 constructor() {
91 const options = optionsFromArguments(
92 PitchShift.getDefaults(),
93 arguments,
94 ["pitch"]
95 );
96 super(options);
97
98 this._frequency = new Signal({ context: this.context });
99 this._delayA = new Delay({
100 maxDelay: 1,
101 context: this.context,
102 });
103 this._lfoA = new LFO({
104 context: this.context,
105 min: 0,
106 max: 0.1,
107 type: "sawtooth",
108 }).connect(this._delayA.delayTime);
109 this._delayB = new Delay({
110 maxDelay: 1,
111 context: this.context,
112 });
113 this._lfoB = new LFO({
114 context: this.context,
115 min: 0,
116 max: 0.1,
117 type: "sawtooth",
118 phase: 180,
119 }).connect(this._delayB.delayTime);
120 this._crossFade = new CrossFade({ context: this.context });
121 this._crossFadeLFO = new LFO({
122 context: this.context,
123 min: 0,
124 max: 1,
125 type: "triangle",
126 phase: 90,
127 }).connect(this._crossFade.fade);
128 this._feedbackDelay = new Delay({
129 delayTime: options.delayTime,
130 context: this.context,
131 });
132 this.delayTime = this._feedbackDelay.delayTime;
133 readOnly(this, "delayTime");
134 this._pitch = options.pitch;
135
136 this._windowSize = options.windowSize;
137
138 // connect the two delay lines up
139 this._delayA.connect(this._crossFade.a);
140 this._delayB.connect(this._crossFade.b);
141 // connect the frequency
142 this._frequency.fan(
143 this._lfoA.frequency,
144 this._lfoB.frequency,
145 this._crossFadeLFO.frequency
146 );
147 // route the input
148 this.effectSend.fan(this._delayA, this._delayB);
149 this._crossFade.chain(this._feedbackDelay, this.effectReturn);
150 // start the LFOs at the same time
151 const now = this.now();
152 this._lfoA.start(now);
153 this._lfoB.start(now);
154 this._crossFadeLFO.start(now);
155 // set the initial value
156 this.windowSize = this._windowSize;
157 }
158
159 static getDefaults(): PitchShiftOptions {
160 return Object.assign(FeedbackEffect.getDefaults(), {
161 pitch: 0,
162 windowSize: 0.1,
163 delayTime: 0,
164 feedback: 0,
165 });
166 }
167
168 /**
169 * Repitch the incoming signal by some interval (measured in semi-tones).
170 * @example
171 * const pitchShift = new Tone.PitchShift().toDestination();
172 * const osc = new Tone.Oscillator().connect(pitchShift).start().toDestination();
173 * pitchShift.pitch = -12; // down one octave
174 * pitchShift.pitch = 7; // up a fifth
175 */
176 get pitch() {
177 return this._pitch;
178 }
179 set pitch(interval) {
180 this._pitch = interval;
181 let factor = 0;
182 if (interval < 0) {
183 this._lfoA.min = 0;
184 this._lfoA.max = this._windowSize;
185 this._lfoB.min = 0;
186 this._lfoB.max = this._windowSize;
187 factor = intervalToFrequencyRatio(interval - 1) + 1;
188 } else {
189 this._lfoA.min = this._windowSize;
190 this._lfoA.max = 0;
191 this._lfoB.min = this._windowSize;
192 this._lfoB.max = 0;
193 factor = intervalToFrequencyRatio(interval) - 1;
194 }
195 this._frequency.value = factor * (1.2 / this._windowSize);
196 }
197
198 /**
199 * The window size corresponds roughly to the sample length in a looping sampler.
200 * Smaller values are desirable for a less noticeable delay time of the pitch shifted
201 * signal, but larger values will result in smoother pitch shifting for larger intervals.
202 * A nominal range of 0.03 to 0.1 is recommended.
203 */
204 get windowSize(): Seconds {
205 return this._windowSize;
206 }
207 set windowSize(size) {
208 this._windowSize = this.toSeconds(size);
209 this.pitch = this._pitch;
210 }
211
212 dispose(): this {
213 super.dispose();
214 this._frequency.dispose();
215 this._delayA.dispose();
216 this._delayB.dispose();
217 this._lfoA.dispose();
218 this._lfoB.dispose();
219 this._crossFade.dispose();
220 this._crossFadeLFO.dispose();
221 this._feedbackDelay.dispose();
222 return this;
223 }
224}