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