From 9e5307180b64c89e6cf1fa72ccf4135f957f0bc8 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Mon, 17 Jun 2024 11:40:32 +0300 Subject: [PATCH] Replace ad-hoc metallophone waveforms Use metallophone literature as a starting point and tune missing details by ear. ref #757 --- src/stores/audio.ts | 13 +++-- src/synth.ts | 112 +++++++++++++++++++++++++------------------- 2 files changed, 73 insertions(+), 52 deletions(-) diff --git a/src/stores/audio.ts b/src/stores/audio.ts index 4503d180..8319e38e 100644 --- a/src/stores/audio.ts +++ b/src/stores/audio.ts @@ -52,6 +52,9 @@ type AudioStore = { mainHighpass: Ref } +const DEFAULT_WAVEFORM = 'semisine' +const DEFAULT_APERIODIC = 'jegogan' + export const useAudioStore = defineStore<'audio', AudioStore>('audio', () => { const context = ref(null) // Chromium has some issues with audio nodes as props @@ -71,14 +74,14 @@ export const useAudioStore = defineStore<'audio', AudioStore>('audio', () => { let aperiodicSynth: AperiodicSynth // Synth params - const waveform = ref('semisine') + const waveform = ref(DEFAULT_WAVEFORM) const attackTime = ref(0.01) const decayTime = ref(0.3) const sustainLevel = ref(0.8) const releaseTime = ref(0.01) const stackSize = ref(3) const spread = ref(2.5) - const aperiodicWaveform = ref('steel') + const aperiodicWaveform = ref(DEFAULT_APERIODIC) // Fix Firefox issues with audioContext.currentTime being in the past using a delay. // This is a locally stored user preference, but shown on the Synth tab. const audioDelay = ref(0.001) @@ -198,10 +201,10 @@ export const useAudioStore = defineStore<'audio', AudioStore>('audio', () => { initializeCustomWaves(context.value) oscillatorVoiceParams.type = 'custom' - oscillatorVoiceParams.periodicWave = PERIODIC_WAVES['semisine'].value + oscillatorVoiceParams.periodicWave = PERIODIC_WAVES[DEFAULT_WAVEFORM].value unisonVoiceParams.type = 'custom' - unisonVoiceParams.periodicWave = PERIODIC_WAVES['semisine'].value - aperiodicVoiceParams.aperiodicWave = APERIODIC_WAVES['steel'].value + unisonVoiceParams.periodicWave = PERIODIC_WAVES[DEFAULT_WAVEFORM].value + aperiodicVoiceParams.aperiodicWave = APERIODIC_WAVES[DEFAULT_APERIODIC].value // These all should start with polyphony 0 to save resources oscillatorSynth = new Synth(context.value, audioDestination) diff --git a/src/synth.ts b/src/synth.ts index 43873bed..d3d4b296 100644 --- a/src/synth.ts +++ b/src/synth.ts @@ -33,13 +33,15 @@ export const WAVEFORMS = BASIC_WAVEFORMS.concat(CUSTOM_WAVEFORMS) export const PERIODIC_WAVES: Record> = {} export const APERIODIC_WAVEFORMS = [ + 'jegogan', + 'jublag', + 'ugal', + 'gender', 'tin', 'bronze', 'steel', 'silver', 'platinum', - 'pelog', - 'slendro', '12-TET' ] export const APERIODIC_WAVES: Record> = {} @@ -295,54 +297,70 @@ function initializeAperiodic(audioContext: BaseAudioContext) { ) ) - APERIODIC_WAVES['pelog'] = computed(() => { - const pelogSpectrum = [ - 0, 670, 1215, 1885, 2430, 2700, 3380, 4315, 4430, 4980, 5645, 5810, 6075, 6195, 6345, 6615, - 6745, 6860, 7025, 7290, 7410, 7560, 7830, 7960, 8075, 8240 - ].map((c) => centsToValue(0.999 * c)) - const pelogAmplitudes = pelogSpectrum.map((_, i) => (i + 1) ** -1.2) - pelogAmplitudes[1] = 0.1 - pelogAmplitudes[4] = 0.3 - pelogAmplitudes[6] = 0.2 - pelogAmplitudes[10] = 0.15 - pelogSpectrum.push(9) - pelogAmplitudes.push(0.15) - pelogSpectrum.push(16) - pelogAmplitudes.push(0.09) - pelogSpectrum.push(64) - pelogAmplitudes.push(0.06) - return new AperiodicWave( - audioContext, - pelogSpectrum, - pelogAmplitudes.map((a) => 0.33 * a), - maxNumberOfVoices, - tolerance - ) - }) + APERIODIC_WAVES['gender'] = computed(() => { + const spectrum_ = [1, 2.26, 3.358, 3.973, 7.365, 13, 29, 31, 37] + const amplitudes_ = [1, 0.6, 0.3, 0.4, 0.2, 0.05, 0.04, 0.01, 0.006].map((a) => 0.4 * a) - APERIODIC_WAVES['slendro'] = computed(() => { - const slendroSpectrum = [ - 0, 717, 1208, 1682, 1925, 2416, 2647, 3371, 3624, 4579, 4832, 5063, 5549, 5787, 6040, 6271, - 6514, 6757, 6995, 7248, 7479, 7722, 7965, 8203 - ].map(centsToValue) - const slendroAmplitudes = slendroSpectrum.map((_, i) => (i + 1) ** -1.5) - slendroAmplitudes[1] = 0.01 - for (let h = 2; h < 128; ++h) { - for (let i = 0; i < slendroSpectrum.length; ++i) { - if (Math.abs(Math.log(slendroSpectrum[i] / h)) < 0.01) { - slendroSpectrum[i] = Math.cbrt(h * slendroSpectrum[i] ** 2) - slendroAmplitudes[i] = 1.5 * (i + 1) ** -0.9 - break - } - } + // Add shimmer + const spectrum: number[] = [] + const amplitudes: number[] = [] + for (let i = 0; i < spectrum_.length; ++i) { + spectrum.push(spectrum_[i] * 1.004) + spectrum.push(spectrum_[i] / 1.004) + amplitudes.push(amplitudes_[i]) + amplitudes.push(0.6 * amplitudes_[i]) } - return new AperiodicWave( - audioContext, - slendroSpectrum, - slendroAmplitudes.map((a) => 0.3 * a), - maxNumberOfVoices, - tolerance + + return new AperiodicWave(audioContext, spectrum, amplitudes, maxNumberOfVoices, tolerance) + }) + + // https://pubs.aip.org/asa/jasa/article/127/5/EL197/783208/Vibrational-characteristics-of-Balinese-gamelan + APERIODIC_WAVES['jublag'] = computed(() => { + // Spectrum is from literature + const spectrum = [1, 2.77, 5.18, 5.33] + // Made up stuff to round it off + spectrum.push(9.1, 18.9, 23) + // Add shimmer + spectrum[0] = 1.01 + spectrum.unshift(1 / spectrum[0]) + spectrum.push(2.76) + + // Amplitudes are made up + const amplitudes = [1, 0.5, 0.5, 0.3, 0.2, 0.15, 0.1, 0.09, 0.2].map((a) => 0.45 * a) + + return new AperiodicWave(audioContext, spectrum, amplitudes, maxNumberOfVoices, tolerance) + }) + + // https://pubs.aip.org/asa/jasa/article/127/5/EL197/783208/Vibrational-characteristics-of-Balinese-gamelan + APERIODIC_WAVES['ugal'] = computed(() => { + // Spectrum is from literature + const spectrum = [1, 2.61, 4.8, 4.94, 6.32] + // Made up stuff to round it off + spectrum.push(9.9, 17, 24.1) + // Add shimmer + spectrum[0] = 1.008 + spectrum.unshift(1 / spectrum[0]) + spectrum.push(2.605) + spectrum.push(4.81) + + // Amplitudes are made up + const amplitudes = [0.6, 1, 0.45, 0.3, 0.15, 0.2, 0.07, 0.08, 0.05, 0.1, 0.1].map( + (a) => 0.45 * a ) + + return new AperiodicWave(audioContext, spectrum, amplitudes, maxNumberOfVoices, tolerance) + }) + + // https://pubs.aip.org/asa/jasa/article/127/5/EL197/783208/Vibrational-characteristics-of-Balinese-gamelan + APERIODIC_WAVES['jegogan'] = computed(() => { + // Spectrum is from literature + const spectrum = [ + 1, 2.8, 5.5, 9, 16.7, 17.8, 20.5, 22.9, 24.9, 27, 28.1, 29.2, 29.5, 30, 31.8, 33.3, 36, 36.9, + 40.6, 41.4 + ] + // Amplitudes are made up + const amplitudes = spectrum.map((i) => (0.7 * (Math.cos(0.3 * i * i) + 1.6)) / (i ** 1.4 + 1.6)) + return new AperiodicWave(audioContext, spectrum, amplitudes, maxNumberOfVoices, tolerance) }) APERIODIC_WAVES['12-TET'] = computed(() => {