Created
March 21, 2023 09:26
-
-
Save tokikokoko/07f63955b1f47ac250c27006d1eda5e8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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