Skip to content

Commit

Permalink
Merge pull request #439 from xenharmonic-devs/ping-pong-delay
Browse files Browse the repository at this point in the history
Implement ping pong delay for basic reverb
  • Loading branch information
frostburn authored Nov 26, 2023
2 parents a6692fb + d11a850 commit ebf7f64
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 2 deletions.
64 changes: 63 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
mapWhiteQweZxcBlack123Asd,
} from "./keyboard-mapping";
import { VirtualSynth } from "./virtual-synth";
import { Synth } from "@/synth";
import { PingPongDelay, Synth } from "@/synth";
import {
Interval,
parseLine,
Expand Down Expand Up @@ -106,6 +106,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 @@ -235,6 +242,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 @@ -273,6 +284,10 @@ watch(
decayTime,
sustainLevel,
releaseTime,
pingPongDelayTime,
pingPongFeedback,
pingPongSeparation,
pingPongGain,
],
encodeState
);
Expand Down Expand Up @@ -313,6 +328,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 @@ -700,6 +719,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 @@ -879,6 +900,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 @@ -1007,6 +1061,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 @@ -1048,6 +1106,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 @@ -427,6 +427,10 @@ export type DecodedState = {
decayTime: number;
sustainLevel: number;
releaseTime: number;
pingPongDelayTime: number;
pingPongFeedback: number;
pingPongSeparation: number;
pingPongGain: number;
};

export type EncodedState = {
Expand All @@ -446,6 +450,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 @@ -487,6 +495,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 @@ -514,6 +526,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 @@ -565,6 +581,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;
}
10 changes: 10 additions & 0 deletions src/views/NotFoundView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ const props = defineProps<{
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 @@ -64,6 +69,11 @@ function openTheGates(scale: Scale) {
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 ebf7f64

Please sign in to comment.