1 /* 2 soundboard.js 3 @author Michael Murtaugh, XPUB 4 @year 2024 5 @license CC0: Public Domain 6 */ 7 8 var audioCtx = new AudioContext(); 9 var tuna = new Tuna(audioCtx); 10 let verbose = true; 11 12 // enable a button that activates the audioCtx 13 document.querySelector("button#start-audio-context")?.addEventListener( 14 "click", () => { 15 // check if context is in suspended state (autoplay policy) 16 if (audioCtx.state === "suspended") { audioCtx.resume(); } 17 }, false ); 18 19 // store nodes by id in nodes_by_id 20 const nodes_by_id = {}; 21 22 /** audio.sb-source, video.sb-source => createMediaElementSource */ 23 document.querySelectorAll("audio.sb-source,video.sb-source")?.forEach( (elt) => { 24 if (verbose) console.log(`sb-source: ${elt}`); 25 if (!elt.id) { console.log(`sb-source: ${elt} has no id`); } 26 nodes_by_id[elt.id] = {elt: elt, node: audioCtx.createMediaElementSource(elt)}; 27 }) 28 29 //input[type=range].sb-gain => createGain 30 document.querySelectorAll("input[type=range].sb-gain")?.forEach((elt)=>{ 31 if (!elt.id) { console.log(`sb-source: ${elt} has no id`); } 32 const node = audioCtx.createGain(elt); 33 node.gain.value = elt.value; 34 nodes_by_id[elt.id] = {elt: elt, node: node}; 35 elt.addEventListener("input", () => { node.gain.value = elt.value; }, false); 36 }) 37 38 // input[type=range].sb-panner => createPanner 39 document.querySelectorAll("input[type=range].sb-panner")?.forEach((elt)=>{ 40 if (!elt.id) { console.log(`sb-source: ${elt} has no id`); } 41 const node = audioCtx.createStereoPanner(elt); 42 node.pan.value = elt.value; 43 nodes_by_id[elt.id] = {elt: elt, node: node}; 44 elt.addEventListener("input", () => { node.pan.value = elt.value; }, false); 45 }) 46 47 // input[type=checkbox].sb-tuna => tuna[tunaClassname] 48 document.querySelectorAll("input[type=checkbox].sb-tuna")?.forEach((elt)=>{ 49 if (!elt.id) { console.log(`sb-tuna: ${elt} has no id`); } 50 const klassName = elt.dataset.tunaClassname; 51 const node = new tuna[klassName]({ 52 bypass: !elt.checked 53 }); 54 nodes_by_id[elt.id] = {elt: elt, node: node}; 55 elt.addEventListener("input", () => { 56 node.bypass = !elt.checked; 57 }, false); 58 }) 59 60 /** 61 * @function sb-tuna-param 62 * @description matches: input[type=range].sb-tuna-param 63 * @param {string} for - the id of main tuna filter element (the checkbox) 64 * @param {string} param - the name of tuna parameter to set 65 */ 66 document.querySelectorAll("input[type=range].sb-tuna-param")?.forEach((elt)=>{ 67 const node = nodes_by_id[elt.dataset.for]; 68 if (!node) { console.log(`couldn't find id ${elt.dataset.for} in sb-tuna-param ${elt.name}`)} 69 node.node[elt.dataset.param] = elt.value; 70 elt.addEventListener("input", () => { 71 // if (verbose) { console.log(`setting ${elt.dataset.param} to ${elt.value}`)} 72 node.node[elt.dataset.param] = elt.value; 73 }, false); 74 }) 75 76 // connect the audio graph using data-connect params 77 // uses data attributes: 78 // data-connect: id of element to connect to (or destination for audioCtx.destination) 79 for (const id of Object.keys(nodes_by_id)) { 80 const node = nodes_by_id[id]; 81 if (node.elt.dataset.connect) { 82 const to_node = node.elt.dataset.connect == "destination" ? audioCtx.destination : nodes_by_id[node.elt.dataset.connect]?.node; 83 if (!to_node) { 84 console.log(`soundboard: in {id}: can't find to "{node.elt.dataset.connect}"`); 85 continue; 86 } 87 // make the connection 88 if (verbose) { console.log(`soundboard: ${node.elt.id} --> ${node.elt.dataset.connect}`)}; 89 node.node.connect(to_node); 90 } 91 } 92