Skip to content

Commit

Permalink
Implement ping pong delay for basic reverb
Browse files Browse the repository at this point in the history
ref #356
  • Loading branch information
frostburn committed May 19, 2023
1 parent c75afb2 commit a57414f
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 9 deletions.
64 changes: 63 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
mapWhiteQweZxcBlack123Asd,
} from "./keyboard-mapping";
import { VirtualSynth } from "./virtual-synth";
import { Synth } from "@/synth";
import { PingPongDelay, Synth } from "@/synth";
import { Interval, parseLine, Scale } from "scale-workshop-core";
// === Root props and audio ===
Expand Down Expand Up @@ -104,6 +104,13 @@ const maxPolyphony = ref(6);
const midiWhiteMode = ref<"off" | "simple" | "blackAverage" | "keyColors">(
"off"
);
// Stereo ping pong delay and associated params
const pingPongDelay = new PingPongDelay(rootProps.audioContext);
const pingPongGainNode = rootProps.audioContext.createGain();
const pingPongDelayTime = ref(0.3);
const pingPongFeedback = ref(0.8);
const pingPongSeparation = ref(1);
const pingPongGain = ref(0);
// These are user preferences and are fetched from local storage.
const newline = ref(UNIX_NEWLINE);
const colorScheme = ref<"light" | "dark">("light");
Expand Down Expand Up @@ -228,6 +235,10 @@ const encodeState = debounce(() => {
decayTime: decayTime.value,
sustainLevel: sustainLevel.value,
releaseTime: releaseTime.value,
pingPongDelayTime: pingPongDelayTime.value,
pingPongFeedback: pingPongFeedback.value,
pingPongSeparation: pingPongSeparation.value,
pingPongGain: pingPongGain.value,
};
const query = encodeQuery(state) as LocationQuery;
Expand Down Expand Up @@ -258,6 +269,10 @@ watch(
decayTime,
sustainLevel,
releaseTime,
pingPongDelayTime,
pingPongFeedback,
pingPongSeparation,
pingPongGain,
],
encodeState
);
Expand Down Expand Up @@ -298,6 +313,10 @@ router.afterEach((to, from) => {
decayTime.value = state.decayTime;
sustainLevel.value = state.sustainLevel;
releaseTime.value = state.releaseTime;
pingPongDelayTime.value = state.pingPongDelayTime;
pingPongFeedback.value = state.pingPongFeedback;
pingPongSeparation.value = state.pingPongSeparation;
pingPongGain.value = state.pingPongGain;
} catch (error) {
console.error(`Error parsing version ${query.get("version")} URL`, error);
}
Expand Down Expand Up @@ -692,6 +711,8 @@ onMounted(() => {
const gain = ctx.createGain();
gain.gain.setValueAtTime(mainVolume.value, ctx.currentTime);
gain.connect(ctx.destination);
gain.connect(pingPongDelay.destination);
pingPongDelay.connect(pingPongGainNode).connect(ctx.destination);
mainGain.value = gain;
const lowpass = ctx.createBiquadFilter();
Expand Down Expand Up @@ -871,6 +892,39 @@ watch(maxPolyphony, (newValue) => {
}
});
// Ping pong delay parameter watchers
watch(
pingPongDelayTime,
(newValue) => {
pingPongDelay.delayTime = newValue;
},
{ immediate: true }
);
watch(
pingPongFeedback,
(newValue) => {
pingPongDelay.feedback = newValue;
},
{ immediate: true }
);
watch(
pingPongSeparation,
(newValue) => {
pingPongDelay.separation = newValue;
},
{ immediate: true }
);
watch(
pingPongGain,
(newValue) => {
pingPongGainNode.gain.setValueAtTime(
newValue,
rootProps.audioContext.currentTime
);
},
{ immediate: true }
);
function setMaxPolyphony(newValue: number) {
if (newValue < 1) {
newValue = 1;
Expand Down Expand Up @@ -995,6 +1049,10 @@ watch(degreeDownCode, (newValue) =>
:sustainLevel="sustainLevel"
:releaseTime="releaseTime"
:maxPolyphony="maxPolyphony"
:pingPongDelayTime="pingPongDelayTime"
:pingPongFeedback="pingPongFeedback"
:pingPongSeparation="pingPongSeparation"
:pingPongGain="pingPongGain"
:typingKeyboard="typingKeyboard"
:keyboardMapping="keyboardMapping"
:showVirtualQwerty="showVirtualQwerty"
Expand Down Expand Up @@ -1034,6 +1092,10 @@ watch(degreeDownCode, (newValue) =>
@update:sustainLevel="sustainLevel = $event"
@update:releaseTime="releaseTime = $event"
@update:maxPolyphony="setMaxPolyphony"
@update:pingPongDelayTime="pingPongDelayTime = $event"
@update:pingPongFeedback="pingPongFeedback = $event"
@update:pingPongSeparation="pingPongSeparation = $event"
@update:pingPongGain="pingPongGain = $event"
@panic="panic"
/>
</template>
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/url-encode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ describe("URL encoder", () => {
decayTime: 0.3,
sustainLevel: 0.8,
releaseTime: 0.01,
pingPongDelayTime: 0.3,
pingPongFeedback: 0.8,
pingPongSeparation: 1.0,
pingPongGain: 0.0,
};
const encoded = encodeQuery(state);
expect(encoded).toMatchObject({
Expand Down
65 changes: 65 additions & 0 deletions src/synth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,68 @@ export class Synth {
}
}
}

// Simple feedback loop bouncing sound between left and right channels.
export class PingPongDelay {
audioContext: AudioContext;
delayL: DelayNode;
delayR: DelayNode;
gainL: GainNode;
gainR: GainNode;
panL: StereoPannerNode;
panR: StereoPannerNode;
destination: AudioNode;

constructor(audioContext: AudioContext, maxDelayTime = 5) {
this.audioContext = audioContext;
this.delayL = audioContext.createDelay(maxDelayTime);
this.delayR = audioContext.createDelay(maxDelayTime);
this.gainL = audioContext.createGain();
this.gainR = audioContext.createGain();
this.panL = audioContext.createStereoPanner();
this.panR = audioContext.createStereoPanner();

// Create a feedback loop with a gain stage.
this.delayL
.connect(this.gainL)
.connect(this.delayR)
.connect(this.gainR)
.connect(this.delayL);
// Tap outputs.
this.gainL.connect(this.panL);
this.gainR.connect(this.panR);

// Tag input.
this.destination = this.delayL;
}

set delayTime(value: number) {
const now = this.audioContext.currentTime;
this.delayL.delayTime.setValueAtTime(value, now);
this.delayR.delayTime.setValueAtTime(value, now);
}

set feedback(value: number) {
const now = this.audioContext.currentTime;
this.gainL.gain.setValueAtTime(value, now);
this.gainR.gain.setValueAtTime(value, now);
}

set separation(value: number) {
const now = this.audioContext.currentTime;
this.panL.pan.setValueAtTime(-value, now);
this.panR.pan.setValueAtTime(value, now);
}

connect(destination: AudioNode) {
this.panL.connect(destination);
this.panR.connect(destination);
return destination;
}

disconnect(destination: AudioNode) {
this.panL.disconnect(destination);
this.panR.disconnect(destination);
return destination;
}
}
28 changes: 28 additions & 0 deletions src/url-encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,10 @@ export type DecodedState = {
decayTime: number;
sustainLevel: number;
releaseTime: number;
pingPongDelayTime: number;
pingPongFeedback: number;
pingPongSeparation: number;
pingPongGain: number;
};

export type EncodedState = {
Expand All @@ -414,6 +418,10 @@ export type EncodedState = {
y?: string; // decaY time
s?: string; // Sustain level
r?: string; // Release time
t?: string; // Ping pong delay time
b?: string; // Ping pong feedback
i?: string; // Ping pong stereo separation
g?: string; // Ping pong gain
};

export function decodeQuery(
Expand Down Expand Up @@ -455,6 +463,10 @@ export function decodeQuery(
decayTime: unmillify(get("y", "8c")),
sustainLevel: unmillify(get("s", "m8")),
releaseTime: unmillify(get("r", "a")),
pingPongDelayTime: unmillify(get("t", "8c")),
pingPongFeedback: unmillify(get("b", "m8")),
pingPongSeparation: unmillify(get("i", "rs")),
pingPongGain: unmillify(get("g", "0")),
};
}

Expand Down Expand Up @@ -482,6 +494,10 @@ export function encodeQuery(state: DecodedState): EncodedState {
y: millify(state.decayTime),
s: millify(state.sustainLevel),
r: millify(state.releaseTime),
t: millify(state.pingPongDelayTime),
b: millify(state.pingPongFeedback),
i: millify(state.pingPongSeparation),
g: millify(state.pingPongGain),
};

// The app includes version information so we can safely strip defaults
Expand Down Expand Up @@ -533,6 +549,18 @@ export function encodeQuery(state: DecodedState): EncodedState {
if (result.r === "a") {
delete result.r;
}
if (result.t === "8c") {
delete result.t;
}
if (result.b === "m8") {
delete result.b;
}
if (result.i === "rs") {
delete result.i;
}
if (result.g === "0") {
delete result.g;
}

return result;
}
27 changes: 20 additions & 7 deletions src/views/NotFoundView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/
import OctaplexPortal from "@/components/modals/generation/SummonOctaplex.vue";
import type { Synth } from "@/synth";
import { encodeQuery } from "@/url-encode";
import type { Scale } from "scale-workshop-core";
import { nextTick, ref } from "vue";
Expand All @@ -28,7 +27,16 @@ const props = defineProps<{
equaveShift: number;
degreeShift: number;
synth: Synth;
waveform: string;
attackTime: number;
decayTime: number;
sustainLevel: number;
releaseTime: number;
pingPongDelayTime: number;
pingPongFeedback: number;
pingPongSeparation: number;
pingPongGain: number;
}>();
const emit = defineEmits(["update:scale", "update:scaleName"]);
Expand Down Expand Up @@ -56,11 +64,16 @@ function openTheGates(scale: Scale) {
equaveShift: props.equaveShift,
degreeShift: props.degreeShift,
waveform: props.synth.waveform,
attackTime: props.synth.attackTime,
decayTime: props.synth.decayTime,
sustainLevel: props.synth.sustainLevel,
releaseTime: props.synth.releaseTime,
waveform: props.waveform,
attackTime: props.attackTime,
decayTime: props.decayTime,
sustainLevel: props.sustainLevel,
releaseTime: props.releaseTime,
pingPongDelayTime: props.pingPongDelayTime,
pingPongFeedback: props.pingPongFeedback,
pingPongSeparation: props.pingPongSeparation,
pingPongGain: props.pingPongGain,
};
const query = encodeQuery(state) as LocationQuery;
Expand Down
Loading

0 comments on commit a57414f

Please sign in to comment.