1import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel/core'; 2import { 3 getAudioContext, 4 registerSound, 5 getParamADSR, 6 getADSRValues, 7 getPitchEnvelope, 8 getVibratoOscillator, 9 onceEnded, 10 releaseAudioNode, 11} from '@strudel/webaudio'; 12import gm from './gm.mjs'; 13 14let defaultSoundfontUrl = 'https://felixroos.github.io/webaudiofontdata/sound'; 15let soundfontUrl = defaultSoundfontUrl; 16 17export function setSoundfontUrl(value) { 18 soundfontUrl = value; 19} 20 21let loadCache = {}; 22async function loadFont(name) { 23 if (loadCache[name]) { 24 return loadCache[name]; 25 } 26 const load = async () => { 27 // TODO: make soundfont source configurable 28 const url = `${soundfontUrl}/${name}.js`; 29 const preset = await fetch(url).then((res) => res.text()); 30 let [_, data] = preset.split('={'); 31 return eval('{' + data); 32 }; 33 loadCache[name] = load(); 34 return loadCache[name]; 35} 36 37export async function getFontBufferSource(name, value, ac) { 38 let { note = 'c3', freq } = value; 39 let midi; 40 if (freq) { 41 midi = freqToMidi(freq); 42 } else if (typeof note === 'string') { 43 midi = noteToMidi(note); 44 } else if (typeof note === 'number') { 45 midi = note; 46 } else { 47 throw new Error(`unexpected "note" type "${typeof note}"`); 48 } 49 50 const { buffer, zone } = await getFontPitch(name, midi, ac); 51 const src = ac.createBufferSource(); 52 src.buffer = buffer; 53 const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune; 54 const playbackRate = 1.0 * Math.pow(2, (100.0 * midi - baseDetune) / 1200.0); 55 // src detune? 56 src.playbackRate.value = playbackRate; 57 const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd; 58 if (!loop) { 59 /* const waveDuration = duration + this.afterTime; 60 if (waveDuration > zone.buffer.duration / playbackRate) { 61 waveDuration = zone.buffer.duration / playbackRate; 62 // TODO: do sth with waveduration 63 } */ 64 } else { 65 src.loop = true; 66 src.loopStart = zone.loopStart / zone.sampleRate; 67 src.loopEnd = zone.loopEnd / zone.sampleRate; 68 //+ (zone.delay ? zone.delay : 0); 69 } 70 return src; 71} 72 73let bufferCache = {}; 74export async function getFontPitch(name, pitch, ac) { 75 const key = `${name}:::${pitch}`; 76 if (bufferCache[key]) { 77 return bufferCache[key]; 78 } 79 // console.log('load buffer', key); 80 const load = async () => { 81 const preset = await loadFont(name); 82 if (!preset) { 83 throw new Error(`Could not load soundfont ${name}`); 84 } 85 const zone = findZone(preset, pitch); 86 if (!zone) { 87 throw new Error('no soundfont zone found for preset ', name, 'pitch', pitch); 88 } 89 const buffer = await getBuffer(zone, ac); 90 if (!buffer) { 91 throw new Error(`no soundfont buffer found for preset ${name}, pitch: ${pitch}`); 92 } 93 return { buffer, zone }; 94 }; 95 bufferCache[key] = load(); // dont await here to cache promise immediately! 96 return bufferCache[key]; 97} 98 99function findZone(preset, pitch) { 100 return preset.find((zone) => { 101 return zone.keyRangeLow <= pitch && zone.keyRangeHigh + 1 >= pitch; 102 }); 103}
promisified version of https://github.com/felixroos/webaudiofont/blob/c6f97249b60dcfafc20fca5bb381294a6b2f8f51/npm/dist/WebAudioFontPlayer.js#L740
106async function getBuffer(zone, audioContext) { 107 if (zone.sample) { 108 console.warn('zone.sample untested!'); 109 const decoded = atob(zone.sample); 110 zone.buffer = audioContext.createBuffer(1, decoded.length / 2, zone.sampleRate); 111 const float32Array = zone.buffer.getChannelData(0); 112 let b1, b2, n; 113 for (var i = 0; i < decoded.length / 2; i++) { 114 b1 = decoded.charCodeAt(i * 2); 115 b2 = decoded.charCodeAt(i * 2 + 1); 116 if (b1 < 0) { 117 b1 = 256 + b1; 118 } 119 if (b2 < 0) { 120 b2 = 256 + b2; 121 } 122 n = b2 * 256 + b1; 123 if (n >= 65536 / 2) { 124 n = n - 65536; 125 } 126 float32Array[i] = n / 65536.0; 127 } 128 } else { 129 if (zone.file) { 130 const datalen = zone.file.length; 131 const arraybuffer = new ArrayBuffer(datalen); 132 const view = new Uint8Array(arraybuffer); 133 const decoded = atob(zone.file); 134 let b; 135 for (let i = 0; i < decoded.length; i++) { 136 b = decoded.charCodeAt(i); 137 view[i] = b; 138 } 139 return new Promise((resolve) => audioContext.decodeAudioData(arraybuffer, resolve)); 140 } 141 } 142}
144export function registerSoundfonts() { 145 Object.entries(gm).forEach(([name, fonts]) => { 146 registerSound( 147 name, 148 async (time, value, onended) => { 149 const [attack, decay, sustain, release] = getADSRValues([ 150 value.attack, 151 value.decay, 152 value.sustain, 153 value.release, 154 ]); 155 156 const { duration } = value; 157 const n = getSoundIndex(value.n, fonts.length); 158 const font = fonts[n]; 159 const ctx = getAudioContext(); 160 const bufferSource = await getFontBufferSource(font, value, ctx); 161 bufferSource.start(time); 162 const envGain = ctx.createGain(); 163 const node = bufferSource.connect(envGain); 164 const holdEnd = time + duration; 165 getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, time, holdEnd, 'linear'); 166 let envEnd = holdEnd + release + 0.01;
vibrato
173 bufferSource.stop(envEnd); 174 const stop = (releaseTime) => {}; 175 onceEnded(bufferSource, () => { 176 releaseAudioNode(bufferSource); 177 vibratoHandle?.stop(); 178 onended(); 179 }); 180 return { node, stop, nodes: { source: [bufferSource], ...vibratoHandle?.nodes } }; 181 }, 182 { type: 'soundfont', prebake: true, fonts }, 183 ); 184 }); 185}