geedback

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

main.ts (9979B)


      1 import "./style.css";
      2 import init, {
      3   GeedbackProcessor,
      4   Waveform,
      5 } from "../geedback-dsp/pkg/geedback_dsp.js";
      6 import { renderAbout } from "./about";
      7 
      8 const app = document.querySelector<HTMLDivElement>("#app")!;
      9 
     10 let processor: GeedbackProcessor | null = null;
     11 let audioCtx: AudioContext | null = null;
     12 let isActive = false;
     13 let wasmInitialized = false;
     14 
     15 // UI Elements (updated in renderHome)
     16 let startBtn: HTMLButtonElement | null = null;
     17 let wasmOutput: HTMLSpanElement | null = null;
     18 let canvas: HTMLCanvasElement | null = null;
     19 let touchXEl: HTMLSpanElement | null = null;
     20 let touchYEl: HTMLSpanElement | null = null;
     21 
     22 const updateRow = (
     23   id: string,
     24   x: number | null,
     25   y?: number | null,
     26   z?: number | null,
     27 ) => {
     28   const row = document.getElementById(id);
     29   if (!row) return;
     30   if (x !== null) {
     31     const xEl = row.querySelector(".x");
     32     if (xEl) xEl.textContent = x.toFixed(2);
     33   }
     34   if (y !== undefined && y !== null) {
     35     const yEl = row.querySelector(".y");
     36     if (yEl) yEl.textContent = y.toFixed(2);
     37   }
     38   if (z !== undefined && z !== null) {
     39     const zEl = row.querySelector(".z");
     40     if (zEl) zEl.textContent = z.toFixed(2);
     41   }
     42 };
     43 
     44 const setupWaveformControls = () => {
     45   [0, 1, 2].forEach((i) => {
     46     const select = document.getElementById(
     47       `osc-${i}-wave`,
     48     ) as HTMLSelectElement;
     49     if (!select) return;
     50     select.addEventListener("change", (e) => {
     51       if (processor) {
     52         const val = (e.target as HTMLSelectElement).value;
     53         processor.set_waveform(i, (Waveform as any)[val]);
     54       }
     55     });
     56     // Restore state if processor already exists
     57     // (Simplified: just default to Sine or use processor state if we had getters)
     58   });
     59 };
     60 
     61 const drawPad = (x: number, y: number) => {
     62   if (!canvas) return;
     63   const ctx = canvas.getContext("2d")!;
     64   ctx.clearRect(0, 0, canvas.width, canvas.height);
     65 
     66   // Grid
     67   ctx.strokeStyle = "#222";
     68   ctx.lineWidth = 1;
     69   for (let i = 1; i < 4; i++) {
     70     ctx.beginPath();
     71     ctx.moveTo((i * canvas.width) / 4, 0);
     72     ctx.lineTo((i * canvas.width) / 4, canvas.height);
     73     ctx.stroke();
     74     ctx.beginPath();
     75     ctx.moveTo(0, (i * canvas.height) / 4);
     76     ctx.lineTo(canvas.width, (i * canvas.height) / 4);
     77     ctx.stroke();
     78   }
     79 
     80   // Touch point - RED
     81   ctx.fillStyle = "#ff4444";
     82   ctx.beginPath();
     83   ctx.arc(x * canvas.width, y * canvas.height, 10, 0, Math.PI * 2);
     84   ctx.fill();
     85 
     86   // Crosshair - RED
     87   ctx.strokeStyle = "#ff4444";
     88   ctx.lineWidth = 2;
     89   ctx.beginPath();
     90   ctx.moveTo(x * canvas.width, 0);
     91   ctx.lineTo(x * canvas.width, canvas.height);
     92   ctx.stroke();
     93   ctx.beginPath();
     94   ctx.moveTo(0, y * canvas.height);
     95   ctx.lineTo(canvas.width, y * canvas.height);
     96   ctx.stroke();
     97 };
     98 
     99 const setupKaossPad = () => {
    100   if (!canvas) return;
    101   let isTouching = false;
    102 
    103   const updateTouch = (e: MouseEvent | TouchEvent) => {
    104     if (!isActive || !canvas) return;
    105     const rect = canvas.getBoundingClientRect();
    106     let clientX, clientY;
    107 
    108     if ("touches" in e) {
    109       clientX = e.touches[0].clientX;
    110       clientY = e.touches[0].clientY;
    111     } else {
    112       clientX = (e as MouseEvent).clientX;
    113       clientY = (e as MouseEvent).clientY;
    114     }
    115 
    116     const x = (clientX - rect.left) / rect.width;
    117     const y = (clientY - rect.top) / rect.height;
    118     const clampedX = Math.max(0, Math.min(1, x));
    119     const clampedY = Math.max(0, Math.min(1, y));
    120 
    121     if (processor) {
    122       processor.set_touch(clampedX, clampedY);
    123     }
    124 
    125     if (touchXEl) touchXEl.textContent = clampedX.toFixed(2);
    126     if (touchYEl) touchYEl.textContent = clampedY.toFixed(2);
    127 
    128     drawPad(clampedX, clampedY);
    129   };
    130 
    131   canvas.width = canvas.offsetWidth;
    132   canvas.height = canvas.offsetHeight;
    133   drawPad(0.5, 0.5);
    134 
    135   canvas.addEventListener("mousedown", (e) => {
    136     isTouching = true;
    137     updateTouch(e);
    138   });
    139   window.addEventListener("mousemove", (e) => {
    140     if (isTouching) updateTouch(e);
    141   });
    142   window.addEventListener("mouseup", () => {
    143     isTouching = false;
    144   });
    145 
    146   canvas.addEventListener(
    147     "touchstart",
    148     (e) => {
    149       if (e.target === canvas) {
    150         isTouching = true;
    151         updateTouch(e);
    152         e.preventDefault();
    153       }
    154     },
    155     { passive: false },
    156   );
    157 
    158   window.addEventListener(
    159     "touchmove",
    160     (e) => {
    161       if (isTouching) {
    162         updateTouch(e);
    163         e.preventDefault();
    164       }
    165     },
    166     { passive: false },
    167   );
    168   window.addEventListener("touchend", () => {
    169     isTouching = false;
    170   });
    171 };
    172 
    173 const startSynth = async () => {
    174   // 1. Request Sensor Permissions (iOS 13+) - MUST be first to keep user gesture context
    175   try {
    176     if (typeof (DeviceMotionEvent as any).requestPermission === "function") {
    177       await (DeviceMotionEvent as any).requestPermission();
    178     }
    179     if (
    180       typeof (DeviceOrientationEvent as any).requestPermission === "function"
    181     ) {
    182       await (DeviceOrientationEvent as any).requestPermission();
    183     }
    184   } catch (e) {
    185     console.error("Sensor Permission Error:", e);
    186   }
    187 
    188   if (!wasmInitialized) {
    189     try {
    190       await init();
    191       wasmInitialized = true;
    192       processor = new GeedbackProcessor();
    193     } catch (e) {
    194       alert("WASM failed");
    195       return;
    196     }
    197   }
    198 
    199   audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
    200   if (processor) processor.set_sample_rate(audioCtx.sampleRate);
    201 
    202   const scriptNode = audioCtx.createScriptProcessor(4096, 0, 1);
    203   scriptNode.onaudioprocess = (e) => {
    204     const out = e.outputBuffer.getChannelData(0);
    205     if (processor && isActive) {
    206       for (let i = 0; i < out.length; i++) out[i] = processor.process();
    207     } else {
    208       for (let i = 0; i < out.length; i++) out[i] = 0;
    209     }
    210   };
    211   scriptNode.connect(audioCtx.destination);
    212   if (audioCtx.state === "suspended") await audioCtx.resume();
    213 
    214   isActive = true;
    215   if (startBtn) {
    216     startBtn.textContent = "Stop Audio & Sensors";
    217     startBtn.classList.add("active");
    218   }
    219 };
    220 
    221 const stopSynth = async () => {
    222   isActive = false;
    223   if (audioCtx) {
    224     await audioCtx.close();
    225     audioCtx = null;
    226   }
    227   if (startBtn) {
    228     startBtn.textContent = "Start Audio & Sensors";
    229     startBtn.classList.remove("active");
    230   }
    231 };
    232 
    233 const renderHome = () => {
    234   app.innerHTML = `
    235     <div>
    236       <nav style="text-align: right;">
    237         <a href="#about">About</a>
    238       </nav>
    239       <h1>Geedback</h1>
    240       <p>Multi-oscillator synthesizer feedback of gravity for Smartphones</p>
    241       <button id="start-btn">${isActive ? "Stop Audio & Sensors" : "Start Audio & Sensors"}</button>
    242 
    243       <canvas id="kaoss-pad"></canvas>
    244 
    245       <div style="margin-top: 20px; padding: 15px; border: 2px solid #ff4444; border-radius: 8px; background: #1a1a1a;">
    246         <div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
    247           <strong>DSP Output:</strong> <span id="wasm-output" style="font-family: monospace;">-</span>
    248         </div>
    249         <div style="font-size: 0.8em; color: #888; margin-bottom: 10px;">
    250           Cutoff (X): <span id="touch-x">-</span> | Resonance (Y): <span id="touch-y">-</span>
    251         </div>
    252 
    253         <div class="osc-controls">
    254           ${[0, 1, 2]
    255             .map(
    256               (i) => `
    257             <div style="margin-bottom: 8px;">
    258               <label>Osc ${i + 1} Wave:</label>
    259               <select id="osc-${i}-wave" class="wave-select">
    260                 <option value="Sine">Sine</option>
    261                 <option value="Triangle">Triangle</option>
    262                 <option value="TriangleSawtooth">Tri-Saw</option>
    263                 <option value="Sawtooth">Sawtooth</option>
    264                 <option value="ReverseSawtooth">Rev-Saw</option>
    265                 <option value="Square">Square</option>
    266                 <option value="WidePulse">Wide Pulse</option>
    267                 <option value="NarrowPulse">Narrow Pulse</option>
    268               </select>
    269             </div>
    270           `,
    271             )
    272             .join("")}
    273         </div>
    274       </div>
    275 
    276       <table border="1" style="margin-top: 20px; width: 100%; border-collapse: collapse; font-size: 0.8em; opacity: 0.7;">
    277         <tbody id="sensor-table">
    278           <tr id="row-accel"><td>Accel</td><td class="x">-</td><td class="y">-</td><td class="z">-</td></tr>
    279           <tr id="row-linear"><td>Linear</td><td class="x">-</td><td class="y">-</td><td class="z">-</td></tr>
    280           <tr id="row-orient"><td>Orient</td><td class="x">-</td><td class="y">-</td><td class="z">-</td></tr>
    281         </tbody>
    282       </table>
    283     </div>
    284   `;
    285 
    286   startBtn = document.querySelector<HTMLButtonElement>("#start-btn")!;
    287   wasmOutput = document.querySelector<HTMLSpanElement>("#wasm-output")!;
    288   canvas = document.querySelector<HTMLCanvasElement>("#kaoss-pad")!;
    289   touchXEl = document.querySelector<HTMLSpanElement>("#touch-x")!;
    290   touchYEl = document.querySelector<HTMLSpanElement>("#touch-y")!;
    291 
    292   if (isActive) startBtn.classList.add("active");
    293 
    294   startBtn.addEventListener("click", () => {
    295     if (!isActive) {
    296       startSynth();
    297     } else {
    298       stopSynth();
    299     }
    300   });
    301 
    302   setupWaveformControls();
    303   setupKaossPad();
    304 };
    305 
    306 const router = () => {
    307   const hash = window.location.hash;
    308   if (hash === "#about") {
    309     renderAbout(app);
    310   } else {
    311     renderHome();
    312   }
    313 };
    314 
    315 window.addEventListener("hashchange", router);
    316 
    317 // Setup global event listeners once
    318 window.addEventListener("devicemotion", (e) => {
    319   if (!isActive || !processor) return;
    320   const la = e.acceleration;
    321   if (la) {
    322     processor.set_linear_accel(la.x || 0, la.y || 0, la.z || 0);
    323     updateRow("row-linear", la.x, la.y, la.z);
    324   }
    325   const a = e.accelerationIncludingGravity;
    326   if (a) updateRow("row-accel", a.x, a.y, a.z);
    327 });
    328 
    329 window.addEventListener("deviceorientation", (e) => {
    330   if (!isActive || !processor) return;
    331   processor.set_orient(e.alpha || 0, e.beta || 0, e.gamma || 0);
    332   updateRow("row-orient", e.alpha, e.beta, e.gamma);
    333 });
    334 
    335 const uiLoop = () => {
    336   if (processor && isActive && wasmOutput)
    337     wasmOutput.textContent = processor.get_latest_output().toFixed(6);
    338   requestAnimationFrame(uiLoop);
    339 };
    340 
    341 // Start
    342 router();
    343 uiLoop();