Skip to content

Instantly share code, notes, and snippets.

@tokikokoko
Created March 21, 2023 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tokikokoko/07f63955b1f47ac250c27006d1eda5e8 to your computer and use it in GitHub Desktop.
Save tokikokoko/07f63955b1f47ac250c27006d1eda5e8 to your computer and use it in GitHub Desktop.
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import Slider from "./Slider.svelte";
let enable = false;
let gain = 0.01;
let frequency = 440;
let filterFreq = 10000;
let subGainMultiplier = 0.5;
let detune = 10;
let waveform: OscillatorType = "sawtooth";
let waveforms: OscillatorType[] = ["sawtooth", "sine", "square", "triangle"];
let attackAmount = 0.07;
let releaseAmount = 0.0235;
let envelopeMaxValue = 0.5;
let holdTime = 200;
let currentTimeout: any = null;
let AudioContext;
let audioCtx: AudioContext;
let gainNode: GainNode;
let subGainNode: GainNode;
let ampNode: GainNode;
let osc1: OscillatorNode;
let subOscs: OscillatorNode[] = [];
let subOscsLen = subOscs.length;
let filterNode: BiquadFilterNode;
const onClick = (inputFrequency: number) => {
return (e: MouseEvent) => {
frequency = inputFrequency;
[osc1, subOscs].flat().forEach((osc) => {
osc.frequency.setValueAtTime(frequency, audioCtx.currentTime);
});
onClickBase(e);
}
}
const onClickBase = (e: MouseEvent) => {
if (enable && !!(currentTimeout)) {
clearTimeout(currentTimeout)
}
enable = true;
if (audioCtx.state === "suspended") {
audioCtx.resume();
}
function attack() {
ampNode.gain.value = ampNode.gain.value + attackAmount;
if (ampNode.gain.value < 1) {
currentTimeout = setTimeout(attack, 0.005);
} else {
ampNode.gain.value = 1;
currentTimeout = setTimeout(release, holdTime);
}
};
function release() {
ampNode.gain.value = ampNode.gain.value - releaseAmount;
if (ampNode.gain.value >= 0) {
currentTimeout = setTimeout(release, 0.005);
} else {
ampNode.gain.value = 0;
enable = false
}
};
// play or pause track depending on state
currentTimeout = setTimeout(() => {
ampNode.gain.value = 0;
attack();
}, 0);
};
const onVolChange = (e: any) => {
gainNode.gain.value = e.detail.valueAsNumber;
subGainNode.gain.value = e.detail.valueAsNumber * subGainMultiplier;
gain = e.detail.valueAsNumber;
};
const onFilterFreq = (e: any) => {
filterNode.frequency.setValueAtTime(
e.detail.valueAsNumber,
audioCtx.currentTime
);
filterFreq = e.detail.valueAsNumber;
};
const onChangeWaveform = () => {
[osc1, subOscs].flat().forEach((osc) => {
osc.type = waveform;
});
};
const onAddOsc = () => {
subOscs.push(
(() => {
const len = subOscs.length;
const invert = len % 2 === 0 ? 1 : -1;
const osc = audioCtx.createOscillator();
osc.type = waveform;
osc.frequency.setValueAtTime(frequency, audioCtx.currentTime);
osc.detune.setValueAtTime(detune * len * invert, audioCtx.currentTime);
osc.connect(subGainNode).connect(filterNode);
osc.start();
return osc;
})()
);
subOscsLen = subOscs.length;
};
const onDeleteOsc = () => {
const osc = subOscs.pop();
osc?.disconnect();
subOscsLen = subOscs.length;
};
const onDetue = (e: any) => {
const v = e.detail.valueAsNumber;
detune = v;
subOscs.forEach((osc, index) => {
const id = index + 1;
const invert = id % 2 === 0 ? 1 : -1;
osc.detune.setValueAtTime(v * id * invert, audioCtx.currentTime);
});
};
const onAttackAmount = (e: any) => {
const v = e.detail.valueAsNumber;
attackAmount = v;
};
const onReleaseAmount = (e: any) => {
const v = e.detail.valueAsNumber;
releaseAmount = v;
};
onMount(async () => {
// Initialize audio
AudioContext = window.AudioContext;
audioCtx = new AudioContext();
// Setup osc1
osc1 = audioCtx.createOscillator();
osc1.type = waveform;
osc1.frequency.setValueAtTime(frequency, audioCtx.currentTime);
gainNode = audioCtx.createGain();
gainNode.gain.value = gain;
subGainNode = audioCtx.createGain();
subGainNode.gain.value = gain * subGainMultiplier;
ampNode = audioCtx.createGain();
ampNode.gain.value = 0;
filterNode = audioCtx.createBiquadFilter();
filterNode.Q.value = 10;
filterNode.frequency.setValueAtTime(filterFreq, audioCtx.currentTime);
osc1
.connect(gainNode)
.connect(filterNode)
.connect(ampNode)
.connect(audioCtx.destination);
[osc1].forEach((osc) => {
osc.start();
});
});
onDestroy(() => {
[osc1].forEach((osc) => {
if (osc) {
osc.stop();
}
});
});
</script>
<div class="floating-menu">
{#each [1, 2] as index}
<button class="trigger-button" class:selected={enable} on:click={onClick(440 * index)}>
A{index + 3}
</button>
<button class="trigger-button" class:selected={enable} on:click={onClick(493.88 * index)}>
B{index + 3}
</button>
<button class="trigger-button" class:selected={enable} on:click={onClick(523.251 * index)}>
C{index + 3}
</button>
<button class="trigger-button" class:selected={enable} on:click={onClick(587.330 * index)}>
D{index + 3}
</button>
<button class="trigger-button" class:selected={enable} on:click={onClick(659.255 * index)}>
E{index + 3}
</button>
<button class="trigger-button" class:selected={enable} on:click={onClick(698.456 * index)}>
F{index + 3}
</button>
<button class="trigger-button" class:selected={enable} on:click={onClick(783.991 * index)}>
G{index + 3}
</button>
{/each}
</div>
<h1>Parameters</h1>
<div class="parameter">
<Slider min={0} max={1} value={gain} step={0.001} on:on-change={onVolChange}
><h2>Master volume</h2></Slider
>
</div>
<div class="parameter">
<h2>Oscillators</h2>
<button class="on-button" on:click={onAddOsc}> Add osc </button>
<button class="on-button" on:click={onDeleteOsc}> Delete osc </button>
<h3>count: {subOscsLen}</h3>
</div>
<div class="parameter">
<h2>Waveform</h2>
<select bind:value={waveform} on:change={onChangeWaveform}>
{#each waveforms as waveform}
<option value={waveform}>
{waveform}
</option>
{/each}
</select>
</div>
<div class="parameter">
<Slider min={0} max={30} value={detune} step={0.2} on:on-change={onDetue}
><h2>Detune</h2>
<p>{detune}</p></Slider
>
</div>
<div class="parameter">
<h2>Envelope</h2>
<Slider min={0.0005} max={envelopeMaxValue} value={attackAmount} step={0.0005} on:on-change={onAttackAmount}
><h2>Attack</h2>
<p>{attackAmount}</p></Slider
>
<Slider min={0} max={2000} value={holdTime} step={1} on:on-change={(e) => holdTime = e.detail.valueAsNumber}
><h2>Hold time</h2>
<p>{holdTime}ms</p></Slider
>
<Slider min={0.0005} max={0.1} value={releaseAmount} step={0.0005} on:on-change={onReleaseAmount}
><h2>Release</h2>
<p>{releaseAmount}</p></Slider
>
</div>
<div class="parameter">
<Slider
min={20}
max={10_000}
value={filterFreq}
step={0.01}
on:on-change={onFilterFreq}
><h2>Filter frequency</h2>
<p>{filterFreq} Hz</p></Slider
>
</div>
<style>
.on-button {
padding: 20px;
}
.floating-menu {
padding: 20px;
position: fixed;
float: initial;
bottom: 40px;
display: flex;
flex-wrap: wrap;
width: 90%;
height: 200px;
}
.trigger-button {
height: 100px;
width: 30px;
}
.selected {
background-color: orange;
}
.parameter {
margin: 10px;
padding: 10px;
border: solid;
border-color: gray;
}
h2 {
font-size: 20px;
font-weight: bold;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment