{"id":6186,"date":"2026-04-25T14:04:42","date_gmt":"2026-04-25T14:04:42","guid":{"rendered":"https:\/\/composers-inside-electronics.net\/cie-wp20\/?p=6186"},"modified":"2026-04-27T02:22:58","modified_gmt":"2026-04-27T02:22:58","slug":"player-node-js","status":"publish","type":"post","link":"https:\/\/composers-inside-electronics.net\/cie-wp20\/player-node-js\/","title":{"rendered":"Player Node.js"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">&#8230; a work in progress and prototype <a href=\"http:\/\/cieweb.ddns.net:3001\/player-v1.html\">http:\/\/cieweb.ddns.net:3001\/player-v1.html<\/a><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"937\" src=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-content\/uploads\/2026\/04\/image-26-1024x937.png\" alt=\"\" class=\"wp-image-6187\" srcset=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-content\/uploads\/2026\/04\/image-26-1024x937.png 1024w, https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-content\/uploads\/2026\/04\/image-26-300x275.png 300w, https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-content\/uploads\/2026\/04\/image-26-768x703.png 768w, https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-content\/uploads\/2026\/04\/image-26-1536x1406.png 1536w, https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-content\/uploads\/2026\/04\/image-26.png 1844w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"610\" height=\"716\" src=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-content\/uploads\/2026\/04\/image-27.png\" alt=\"\" class=\"wp-image-6194\" style=\"aspect-ratio:0.8519738995700848;width:181px;height:auto\" srcset=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-content\/uploads\/2026\/04\/image-27.png 610w, https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-content\/uploads\/2026\/04\/image-27-256x300.png 256w\" sizes=\"auto, (max-width: 610px) 100vw, 610px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Here is latest as of April 25 on web audio node.js development. Using Gemini, I was able to extend the previous simple convolution app adding features:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>waveform display<\/li>\n\n\n\n<li>playback progress indicator<\/li>\n\n\n\n<li>loop repeat slider<\/li>\n\n\n\n<li>start and end points<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The base functionality provides a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li> list of sample and impulses<\/li>\n\n\n\n<li>gain control <\/li>\n\n\n\n<li>wet\/dry mix<\/li>\n\n\n\n<li>pitch\/speed<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The current iteration was tested standalone and will be ported under cieweb.ddns.net in the next few days.<\/p>\n\n\n\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_84 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<label for=\"ez-toc-cssicon-toggle-item-6a2896cae0d3a\" class=\"ez-toc-cssicon-toggle-label\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/label><input type=\"checkbox\"  id=\"ez-toc-cssicon-toggle-item-6a2896cae0d3a\"  aria-label=\"Toggle\" \/><nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/player-node-js\/#Intended_Use\" >Intended Use<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/player-node-js\/#Related_links\" >Related links<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/player-node-js\/#To_Do\" >To Do<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/player-node-js\/#Code\" >Code<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/player-node-js\/#Other_Notes\" >Other Notes<\/a><\/li><\/ul><\/nav><\/div>\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Intended_Use\"><\/span>Intended Use<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sketches for new installation, performance and web work for various projects.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Related_links\"><\/span>Related links<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/convolver-node-js-v2-with-osc-post\/\">Convolver node.js V2 with OSC post<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/web-audio-links\/\">Web Audio Links<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/bbc-orchestrator-open-source\/\">BBC Orchestrator Open Source<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/ai-generate-html-to-show-the-relative-distance-to-each-speaker\/\">AI Generate HTML to show the relative distance to each speaker<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/on-node-js-convolution\/\">On node.js convolution<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/ai-generated-node-js-convolution-player\/\">AI generated node.js convolution player<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/composers-inside-electronics.net\/cie-wp20\/ai-on-generate-a-web-page-to-select-an-audio-clip-to-play-through-a-selected-impulse-convolution\/\">AI on \u201cgenerate a web page to select an audio clip to play through a selected impulse convolution\u201d<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"To_Do\"><\/span>To Do<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>fix the link to the project information<\/li>\n\n\n\n<li>test over slow network connection (would a load progress indicator help rather than the simple change in shading as is)<\/li>\n\n\n\n<li>consider reintegrating the OSC messages<\/li>\n<\/ul>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>click for details<\/summary>\n<p class=\"wp-block-paragraph\">Previous work included various OSC messages sent on the backend to be used for interacting  with local sound objects.  Gemini couldn&#8217;t quite easily carry that forward.  <\/p>\n<\/details>\n\n\n\n<ul class=\"wp-block-list\">\n<li>consider separate pitch shifter<\/li>\n<\/ul>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>click for details<\/summary>\n<p class=\"wp-block-paragraph\">Earlier variation had separate pitch and speed controls.  The Gemini prompts need to be very specific . Consider separate frequency shift. Gemini was ok at explaining differences.<\/p>\n<\/details>\n\n\n\n<ul class=\"wp-block-list\">\n<li>smooth looping to suppress click<\/li>\n\n\n\n<li>host under cieweb.ddns.net<\/li>\n\n\n\n<li>use hostname for deriving ip address on backend to improve portability<\/li>\n\n\n\n<li>test front ed across iPhone, iPad, Android, RPI in addition to Desktop<\/li>\n\n\n\n<li>Spectrogram visualization of the impulse and possibly the sample<\/li>\n\n\n\n<li>Github and improve IDE integration (VS)<\/li>\n\n\n\n<li>consider Claude code subscription for accelerated development<\/li>\n\n\n\n<li>link to sweep generator and other projects in progress<\/li>\n\n\n\n<li>a major area to consider is the variation of the variables over time and iterations<\/li>\n\n\n\n<li>consider features separate for performance vs. novice visitor interaction<\/li>\n\n\n\n<li>add IMU gestures (and record playback morphing)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Code\"><\/span>Code<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Click here for HTML: player-v1.html<\/summary>\n<pre class=\"wp-block-code\"><code>&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n    &lt;meta charset=\"UTF-8\"&gt;\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"&gt;\n    &lt;title&gt;Resonance Player V3.6&lt;\/title&gt;\n    &lt;style&gt;\n        :root { --primary: #007bff; --success: #28a745; --danger: #dc3545; --warning: #ffc107; --dark: #212529; }\n        body { font-family: sans-serif; background: #121212; color: #eee; padding: 15px; margin: 0; }\n        .container { max-width: 800px; margin: auto; background: #1e1e1e; padding: 20px; border-radius: 12px; }\n        \n        canvas { width: 100%; height: 120px; background: #000; border-radius: 8px; margin-bottom: 15px; border: 1px solid #444; }\n        \n        .control-group { margin-bottom: 20px; padding: 12px; background: #2a2a2a; border-radius: 8px; }\n        label { display: block; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; color: #aaa; }\n        .val-disp { float: right; color: var(--warning); font-family: monospace; }\n        \n        .playback-monitor { margin-bottom: 20px; }\n        .bar-bg { width: 100%; height: 6px; background: #111; border-radius: 3px; overflow: hidden; position: relative; }\n        #progressBar { height: 100%; width: 0%; background: var(--success); box-shadow: 0 0 10px var(--success); }\n\n        .range-container { position: relative; height: 20px; margin: 10px 0; }\n        .range-slider { position: absolute; width: 100%; pointer-events: none; -webkit-appearance: none; background: none; top: 0; margin: 0; z-index: 2; }\n        .range-slider::-webkit-slider-thumb { pointer-events: auto; -webkit-appearance: none; width: 14px; height: 24px; background: var(--primary); cursor: pointer; border-radius: 3px; border: 1px solid #fff; }\n        \n        .meta-row { display: flex; justify-content: space-between; font-family: monospace; font-size: 0.75rem; color: #888; margin-bottom: 10px; }\n        .meta-row span { color: var(--warning); }\n\n        select, input&#91;type=\"range\"] { width: 100%; margin-bottom: 10px; }\n        .row { display: flex; gap: 10px; flex-wrap: wrap; }\n        .row &gt; div { flex: 1; min-width: 120px; }\n        \n        button { padding: 12px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; text-transform: uppercase; }\n        .btn-play { background: var(--success); color: white; flex: 2; }\n        .btn-stop { background: var(--danger); color: white; flex: 1; }\n        \n        footer { margin-top: 30px; text-align: center; font-size: 0.7rem; opacity: 0.6; }\n        a { color: var(--primary); }\n    &lt;\/style&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n\n&lt;div class=\"container\"&gt;\n    &lt;canvas id=\"viz\"&gt;&lt;\/canvas&gt;\n\n    &lt;div class=\"playback-monitor\"&gt;\n        &lt;label&gt;Playback Position &lt;span class=\"val-disp\" id=\"iterDisp\"&gt;Remaining: 0&lt;\/span&gt;&lt;\/label&gt;\n        &lt;div class=\"bar-bg\"&gt;\n            &lt;div id=\"progressBar\"&gt;&lt;\/div&gt;\n        &lt;\/div&gt;\n    &lt;\/div&gt;\n\n    &lt;div class=\"control-group\"&gt;\n        &lt;label&gt;Loop Region Selection &lt;span class=\"val-disp\" id=\"loopDisp\"&gt;0% - 100%&lt;\/span&gt;&lt;\/label&gt;\n        &lt;div class=\"range-container\"&gt;\n            &lt;input type=\"range\" id=\"loopStart\" class=\"range-slider\" min=\"0\" max=\"100\" value=\"0\"&gt;\n            &lt;input type=\"range\" id=\"loopEnd\" class=\"range-slider\" min=\"0\" max=\"100\" value=\"100\"&gt;\n        &lt;\/div&gt;\n    &lt;\/div&gt;\n\n    &lt;div class=\"meta-row\"&gt;\n        &lt;div&gt;Sample: &lt;span id=\"sampleLen\"&gt;0.00&lt;\/span&gt;s&lt;\/div&gt;\n        &lt;div&gt;Impulse: &lt;span id=\"impulseLen\"&gt;0.00&lt;\/span&gt;s&lt;\/div&gt;\n        &lt;div&gt;Loop: &lt;span id=\"loopLen\"&gt;0.00&lt;\/span&gt;s&lt;\/div&gt;\n    &lt;\/div&gt;\n\n    &lt;div class=\"control-group\"&gt;\n        &lt;label&gt;Iteration Count &lt;span class=\"val-disp\" id=\"countDisp\"&gt;1&lt;\/span&gt;&lt;\/label&gt;\n        &lt;input type=\"range\" id=\"loopIterations\" min=\"1\" max=\"50\" step=\"1\" value=\"1\"&gt;\n    &lt;\/div&gt;\n\n    &lt;div class=\"row\"&gt;\n        &lt;div class=\"control-group\"&gt;\n            &lt;label&gt;Source Sample&lt;\/label&gt;\n            &lt;select id=\"sampleSelect\"&gt;&lt;\/select&gt;\n        &lt;\/div&gt;\n        &lt;div class=\"control-group\"&gt;\n            &lt;label&gt;Convolution Impulse&lt;\/label&gt;\n            &lt;select id=\"impulseSelect\"&gt;&lt;\/select&gt;\n        &lt;\/div&gt;\n    &lt;\/div&gt;\n\n    &lt;div class=\"control-group\"&gt;\n        &lt;label&gt;Main Volume &lt;span class=\"val-disp\" id=\"volDisp\"&gt;0.50&lt;\/span&gt;&lt;\/label&gt;\n        &lt;input type=\"range\" id=\"vol\" min=\"0\" max=\"1\" step=\"0.01\" value=\"0.5\"&gt;\n        \n        &lt;label&gt;Wet\/Dry Mix &lt;span class=\"val-disp\" id=\"mixDisp\"&gt;0.50&lt;\/span&gt;&lt;\/label&gt;\n        &lt;input type=\"range\" id=\"mix\" min=\"0\" max=\"1\" step=\"0.01\" value=\"0.5\"&gt;\n        \n        &lt;label&gt;Pitch\/Speed &lt;span class=\"val-disp\" id=\"pitchDisp\"&gt;1.00x&lt;\/span&gt;&lt;\/label&gt;\n        &lt;input type=\"range\" id=\"pitch\" min=\"0.5\" max=\"5.0\" step=\"0.01\" value=\"1.0\"&gt;\n    &lt;\/div&gt;\n\n    &lt;div class=\"row\"&gt;\n        &lt;button id=\"playBtn\" class=\"btn-play\"&gt;Play Source&lt;\/button&gt;\n        &lt;button id=\"stopBtn\" class=\"btn-stop\"&gt;Stop&lt;\/button&gt;\n    &lt;\/div&gt;\n\n    &lt;footer&gt;\n        CIE-WP20 Project\n    &lt;\/footer&gt;\n&lt;\/div&gt;\n\n&lt;script&gt;\n    window.addEventListener('DOMContentLoaded', () =&gt; {\n        const ctx = new AudioContext();\n        let source, startTime = 0, currentDuration = 0;\n        let currentSampleBuffer = null, currentImpulseBuffer = null;\n        let iterationsRemaining = 0, manualStop = false;\n\n        const mainGain = ctx.createGain();\n        const dryGain = ctx.createGain();\n        const wetGain = ctx.createGain();\n        const convolver = ctx.createConvolver();\n        const analyser = ctx.createAnalyser();\n\n        analyser.fftSize = 1024;\n        const dataArray = new Uint8Array(analyser.frequencyBinCount);\n\n        dryGain.connect(mainGain);\n        wetGain.connect(mainGain);\n        mainGain.connect(ctx.destination);\n        mainGain.connect(analyser);\n\n        const canvas = document.getElementById('viz');\n        const canvasCtx = canvas.getContext('2d');\n        const progressBar = document.getElementById('progressBar');\n\n        function draw() {\n            requestAnimationFrame(draw);\n            analyser.getByteTimeDomainData(dataArray);\n            canvasCtx.fillStyle = '#000';\n            canvasCtx.fillRect(0, 0, canvas.width, canvas.height);\n            canvasCtx.strokeStyle = '#00ff00';\n            canvasCtx.lineWidth = 2;\n            canvasCtx.beginPath();\n            let sliceWidth = canvas.width \/ dataArray.length;\n            let x = 0;\n            for(let i=0; i&lt;dataArray.length; i++) {\n                let v = dataArray&#91;i] \/ 128.0;\n                let y = v * canvas.height \/ 2;\n                if(i===0) canvasCtx.moveTo(x,y); else canvasCtx.lineTo(x,y);\n                x += sliceWidth;\n            }\n            canvasCtx.stroke();\n\n            if (source &amp;&amp; currentDuration &gt; 0) {\n                let elapsed = ctx.currentTime - startTime;\n                let progress = (elapsed \/ currentDuration) * 100;\n                progressBar.style.width = Math.min(progress, 100) + \"%\";\n            }\n        }\n        draw();\n\n        fetch('\/files').then(r =&gt; r.json()).then(data =&gt; {\n            const sSelect = document.getElementById('sampleSelect');\n            const iSelect = document.getElementById('impulseSelect');\n            data.samples.forEach(s =&gt; sSelect.add(new Option(s, s)));\n            data.impulses.forEach(i =&gt; iSelect.add(new Option(i, i)));\n        });\n\n        async function play() {\n            if(ctx.state === 'suspended') await ctx.resume();\n            manualStop = false;\n            \n            const &#91;sRes, iRes] = await Promise.all(&#91;\n                fetch(`\/samples\/${document.getElementById('sampleSelect').value}`).then(r =&gt; r.arrayBuffer()),\n                fetch(`\/impulses\/${document.getElementById('impulseSelect').value}`).then(r =&gt; r.arrayBuffer())\n            ]);\n            \n            currentSampleBuffer = await ctx.decodeAudioData(sRes);\n            currentImpulseBuffer = await ctx.decodeAudioData(iRes);\n\n            document.getElementById('sampleLen').innerText = currentSampleBuffer.duration.toFixed(2);\n            document.getElementById('impulseLen').innerText = currentImpulseBuffer.duration.toFixed(2);\n\n            iterationsRemaining = parseInt(document.getElementById('loopIterations').value);\n            triggerSound();\n        }\n\n        function triggerSound() {\n            if (!currentSampleBuffer || manualStop) return;\n            if (source) { try { source.stop(); } catch(e) {} }\n\n            document.getElementById('iterDisp').innerText = `Remaining: ${iterationsRemaining}`;\n            \n            source = ctx.createBufferSource();\n            source.buffer = currentSampleBuffer;\n            convolver.buffer = currentImpulseBuffer;\n\n            const sPct = parseFloat(document.getElementById('loopStart').value) \/ 100;\n            const ePct = parseFloat(document.getElementById('loopEnd').value) \/ 100;\n            const startPct = Math.min(sPct, ePct);\n            const endPct = Math.max(sPct, ePct);\n            \n            const offset = currentSampleBuffer.duration * startPct;\n            const duration = (currentSampleBuffer.duration * endPct) - offset;\n            const pitch = parseFloat(document.getElementById('pitch').value);\n\n            document.getElementById('loopLen').innerText = duration.toFixed(2);\n\n            source.playbackRate.value = pitch;\n            source.connect(dryGain);\n            source.connect(convolver).connect(wetGain);\n            \n            startTime = ctx.currentTime;\n            currentDuration = (duration &gt; 0 ? duration : 0.01) \/ pitch;\n\n            source.onended = () =&gt; {\n                iterationsRemaining--;\n                if (iterationsRemaining &gt; 0 &amp;&amp; !manualStop) {\n                    triggerSound();\n                } else {\n                    progressBar.style.width = \"0%\";\n                    document.getElementById('iterDisp').innerText = \"Remaining: 0\";\n                }\n            };\n\n            source.start(0, offset, duration &gt; 0 ? duration : 0.01);\n            fetch(`\/play-trigger\/1`);\n        }\n\n        document.getElementById('playBtn').onclick = play;\n        document.getElementById('stopBtn').onclick = () =&gt; { \n            manualStop = true;\n            if(source) source.stop(); \n            currentDuration = 0; \n            progressBar.style.width = \"0%\";\n            document.getElementById('iterDisp').innerText = \"Remaining: 0\";\n            fetch('\/stop-trigger');\n        };\n\n        document.getElementById('vol').oninput = (e) =&gt; {\n            mainGain.gain.setTargetAtTime(e.target.value, ctx.currentTime, 0.02);\n            document.getElementById('volDisp').innerText = parseFloat(e.target.value).toFixed(2);\n            fetch(`\/gain-trigger\/${e.target.value}`);\n        };\n\n        document.getElementById('mix').oninput = (e) =&gt; {\n            const val = parseFloat(e.target.value);\n            dryGain.gain.setTargetAtTime(1-val, ctx.currentTime, 0.02);\n            wetGain.gain.setTargetAtTime(val, ctx.currentTime, 0.02);\n            document.getElementById('mixDisp').innerText = val.toFixed(2);\n        };\n\n        document.getElementById('pitch').oninput = (e) =&gt; {\n            const val = parseFloat(e.target.value);\n            document.getElementById('pitchDisp').innerText = val.toFixed(2) + \"x\";\n            if (source) {\n                source.playbackRate.setTargetAtTime(val, ctx.currentTime, 0.05);\n                const sPct = parseFloat(document.getElementById('loopStart').value) \/ 100;\n                const ePct = parseFloat(document.getElementById('loopEnd').value) \/ 100;\n                currentDuration = (currentSampleBuffer.duration * Math.abs(ePct - sPct)) \/ val;\n            }\n        };\n\n        document.getElementById('loopIterations').oninput = (e) =&gt; {\n            document.getElementById('countDisp').innerText = e.target.value;\n        };\n\n        const handleLoopChange = () =&gt; {\n            document.getElementById('loopDisp').innerText = `${document.getElementById('loopStart').value}% - ${document.getElementById('loopEnd').value}%`;\n        };\n        document.getElementById('loopStart').oninput = document.getElementById('loopEnd').oninput = handleLoopChange;\n        \n        document.getElementById('loopStart').onchange = document.getElementById('loopEnd').onchange = () =&gt; {\n            if (source &amp;&amp; !manualStop) {\n                iterationsRemaining = parseInt(document.getElementById('loopIterations').value);\n                triggerSound();\n            }\n        };\n    });\n&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Click here for JS backend- player-v1.js<\/summary>\n<pre class=\"wp-block-code\"><code>const express = require('express');\nconst { Client } = require('node-osc');\nconst fs = require('fs').promises;\nconst path = require('path');\n\nconst app = express();\n\/\/const HOST = '192.168.254.25'; \/\/ Update to your machine's IP\n\/\/const HOST = '192.168.254.50'; \/\/ Update to your machine's IP\nconst HOST = 'localhost'; \/\/ Update to your machine's IP\nconst PORT = 3000;\nconst oscClient = new Client(HOST, 3333);\n\napp.use(express.static('public'));\napp.use('\/samples', express.static(path.join(__dirname, 'samples')));\napp.use('\/impulses', express.static(path.join(__dirname, 'impulses')));\n\napp.get('\/files', async (req, res) =&gt; {\n    const filter = f =&gt; f.match(\/\\.(mp3|wav|ogg)$\/i);\n    try {\n        const &#91;samples, impulses] = await Promise.all(&#91;\n            fs.readdir('.\/samples'),\n            fs.readdir('.\/impulses')\n        ]);\n        res.json({ samples: samples.filter(filter), impulses: impulses.filter(filter) });\n    } catch (err) { res.status(500).json({ error: \"Disk read error\" }); }\n});\n\n\/\/ Standard OSC Routes\napp.get('\/play-trigger\/:id', (req, res) =&gt; {\n    oscClient.send('\/play', parseInt(req.params.id), () =&gt; res.sendStatus(200));\n});\n\napp.get('\/stop-trigger', (req, res) =&gt; {\n    oscClient.send('\/stop', () =&gt; res.sendStatus(200));\n});\n\napp.get('\/pitch-trigger\/:value', (req, res) =&gt; {\n    oscClient.send('\/pitch', parseFloat(req.params.value), () =&gt; res.sendStatus(200));\n});\n\napp.get('\/gain-trigger\/:value', (req, res) =&gt; {\n    oscClient.send('\/gain', parseFloat(req.params.value), () =&gt; res.sendStatus(200));\n});\n\napp.get('\/file-names', (req, res) =&gt; {\n    oscClient.send('\/names\/sample', req.query.sample || \"none\");\n    oscClient.send('\/names\/impulse', req.query.impulse || \"none\", () =&gt; res.sendStatus(200));\n});\n\n\/\/ Advanced Scanner &amp; Envelope OSC Routes\napp.get('\/scan-toggle\/:state', (req, res) =&gt; {\n    oscClient.send('\/scan\/active', parseInt(req.params.state), () =&gt; res.sendStatus(200));\n});\n\napp.get('\/scan-update', (req, res) =&gt; {\n    const freq = parseFloat(req.query.freq);\n    const amp = parseFloat(req.query.amp);\n    oscClient.send('\/scan\/data', &#91;freq, amp]); \n    res.sendStatus(200);\n});\n\napp.get('\/env-trigger\/:value', (req, res) =&gt; {\n    oscClient.send('\/env', parseFloat(req.params.value));\n    res.sendStatus(200);\n});\n\napp.listen(PORT, HOST, () =&gt; console.log(`Server: http:\/\/${HOST}:${PORT}`));<\/code><\/pre>\n<\/details>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Other_Notes\"><\/span>Other Notes<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>source code on ~\/Dropbox\/JS\/osc-audio-app<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>&#8230; a work in progress and prototype http:\/\/cieweb.ddns.net:3001\/player-v1.html Here is latest as of April 25 on web audio node.js development. Using Gemini, I was able [&#8230;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"h5ap_radio_sources":[],"footnotes":""},"categories":[5],"tags":[410,365,62,420],"class_list":["post-6186","post","type-post","status-publish","format-standard","hentry","category-technical","tag-convolution","tag-js","tag-node-js","tag-prototype"],"post_type":"post","author_name":"Philip Edelstein","_links":{"self":[{"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/posts\/6186","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/comments?post=6186"}],"version-history":[{"count":4,"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/posts\/6186\/revisions"}],"predecessor-version":[{"id":6195,"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/posts\/6186\/revisions\/6195"}],"wp:attachment":[{"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/media?parent=6186"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/categories?post=6186"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/composers-inside-electronics.net\/cie-wp20\/wp-json\/wp\/v2\/tags?post=6186"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}