diff --git a/index.html b/index.html index 133ac8f..744e040 100644 --- a/index.html +++ b/index.html @@ -328,18 +328,33 @@
+
-
+
-
+
+
+
+
Mobile Only
+
+ +
+
+
+ +
+
+
+
+
diff --git a/package-lock.json b/package-lock.json index 977c299..0607aca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3300,8 +3300,8 @@ } }, "node_modules/webchuck": { - "version": "1.2.8", - "resolved": "git+ssh://git@github.com/ccrma/webchuck.git#cbad8acc354106b3b7a53bd260523615364cd66e", + "version": "1.2.9", + "resolved": "git+ssh://git@github.com/ccrma/webchuck.git#9bbca368d066081de1329a83080258b85bdbbbb1", "license": "MIT" }, "node_modules/which": { diff --git a/public/examples/accelDemo.ck b/public/examples/accelDemo.ck new file mode 100644 index 0000000..eb62ef7 --- /dev/null +++ b/public/examples/accelDemo.ck @@ -0,0 +1,64 @@ +//------------------------------------------------------------------------------ +// name: accelDemo.ck +// desc: Accel-erometer WebChucK Demo (mobile only) +// Use mobile accelerometer to control sound synthesis +// NOTE: enable accelerometer in sensor settings +// +// Accel WebChucK Docs: +// https://chuck.stanford.edu/webchuck/docs/classes/Accel.html +// +// author: Mike Mulshine +//------------------------------------------------------------------------------ + +Accel ac; +AccelMsg msg; + +0 => int device; + +// open accel +if( !ac.openAccel( device ) ) me.exit(); +<<< "accel '" + ac.name() + "' ready", "" >>>; + +<<< "only on mobile" >>>; + +SinOsc osc => Envelope gain => dac; +Noise noise => LPF filter => gain; + +100 => float rootOscFreq; +rootOscFreq => osc.freq; + +500 => float rootFilterFreq; +rootFilterFreq => filter.freq; + +0.5 => filter.gain; + +10::ms => gain.duration; +1.0 => gain.target; + +// infinite event loop +while( true ) +{ + // wait on accel event + ac => now; + + // get one or more messages + while( ac.recv( msg ) ) + { + // print accel values + <<< msg.getAccelX() + " " + msg.getAccelY() + " " + msg.getAccelZ() >>>; + // compute average acceleration + (Math.fabs(msg.getAccelX()) + + Math.fabs(msg.getAccelY()) + + Math.fabs(msg.getAccelZ())) * 0.3333 => float avgAccel; + + // control synthesis/filter freq + avgAccel * 10.0 => float dFreq; + rootOscFreq + dFreq => osc.freq; + rootFilterFreq + dFreq * 2.0 => filter.freq; + // control gain + avgAccel / 150.0 => float newGain; + newGain => gain.target; + } + + 10::ms => now; +} diff --git a/public/examples/gyro/gyroDemo.ck b/public/examples/gyro/gyroDemo.ck new file mode 100644 index 0000000..ed5cd70 --- /dev/null +++ b/public/examples/gyro/gyroDemo.ck @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +// name: gyroDemo.ck +// desc: Gyro-scope WebChucK Demo (mobile only) +// Use mobile gyroscope to control audio playback +// NOTE: enable gyroscope in sensor settings +// +// Gyro WebChucK Docs: +// https://chuck.stanford.edu/webchuck/docs/classes/Gyro.html +// +// author: Mike Mulshine +//------------------------------------------------------------------------------ + +Gyro gy; +GyroMsg msg; + +0 => int device; + +// open gyro +if( !gy.openGyro( device ) ) me.exit(); +<<< "gyro '" + gy.name() + "' ready", "" >>>; + +<<< "only on mobile" >>>; + +SndBuf buf => Envelope gain => dac; +buf.read("gyroloop.wav"); +0 => buf.pos; +1 => buf.loop; + +10::ms => gain.duration; +1.0 => gain.target; + +function float clamp(float val, float min, float max) { + if (val < min) return min; + if (val > max) return max; + return val; +} + +// infinite event loop +while( true ) +{ + // wait on gyro event + gy => now; + + // get one or more messages + while( gy.recv( msg ) ) + { + // print gyro values + <<< msg.getGyroX() + " " + msg.getGyroY() + " " + msg.getGyroZ() >>>; + + // normalize Y/Z gyro values to 0.0 to 1.0 + Math.fabs(clamp(msg.getGyroY(), -90, 90)) / 90.0 => float gY; + (clamp(msg.getGyroZ(), -90, 90) + 90.0) / 180.0 => float gZ; + + // control playback rate and envelope + gY => gain.target; + gZ * 2.0 => buf.rate; + } + + 10::ms => now; +} diff --git a/public/examples/gyro/gyroLoop.wav b/public/examples/gyro/gyroLoop.wav new file mode 100644 index 0000000..be2511e Binary files /dev/null and b/public/examples/gyro/gyroLoop.wav differ diff --git a/src/components/examples/examples.ts b/src/components/examples/examples.ts index 562c264..ff21d77 100644 --- a/src/components/examples/examples.ts +++ b/src/components/examples/examples.ts @@ -133,6 +133,28 @@ export default class Examples { () => loadChuckFileFromURL("examples/keyboardHID.ck"), hidNested ); + + // Sensor Nested Examples + const sensorNested = NestedDropdown.createNewNestedDropdown( + this.examplesDropdownContainer, + "sensor", + "Sensor" + ); + Examples.newExample( + "Gyro Demo", + () => { + loadChuckFileFromURL("examples/gyro/gyroDemo.ck"); + loadDataFileFromURL("examples/gyro/gyroLoop.wav"); + }, + sensorNested + ); + Examples.newExample( + "Accel Demo", + () => { + loadChuckFileFromURL("examples/accelDemo.ck"); + }, + sensorNested + ); } /** diff --git a/src/components/fileExplorer/projectSystem.ts b/src/components/fileExplorer/projectSystem.ts index 16873c1..fd6fecc 100644 --- a/src/components/fileExplorer/projectSystem.ts +++ b/src/components/fileExplorer/projectSystem.ts @@ -82,7 +82,11 @@ export default class ProjectSystem { "Enter new file name", "untitled.ck" ); - if (filename === "" || !filename) { + if (filename === null || filename === "") { + return; + } + if (ProjectSystem.projectFiles.has(filename)) { + Console.print(`${filename} already exists`); return; } filename = filename.endsWith(".ck") ? filename : filename + ".ck"; @@ -180,7 +184,7 @@ export default class ProjectSystem { } else { if (wasActive) { ProjectSystem.setActiveFile( - ProjectSystem.projectFiles.values().next().value + ProjectSystem.projectFiles.values().next().value! ); } } diff --git a/src/components/inputPanel/hidPanel.ts b/src/components/inputPanel/hidPanel.ts index 546b7ae..258751f 100644 --- a/src/components/inputPanel/hidPanel.ts +++ b/src/components/inputPanel/hidPanel.ts @@ -14,7 +14,13 @@ const hidLog = document.querySelector("#hidLog")!; export default class HidPanel { public static hidMonitor: InputMonitor; + public static mouseActive: boolean = false; + public static keyboardActive: boolean = false; constructor(hid: HID) { + // Create Hid Log + HidPanel.hidMonitor = new InputMonitor(hidLog, MAX_ELEMENTS, false); + + // Mouse new ButtonToggle( mouseButton, true, @@ -26,6 +32,8 @@ export default class HidPanel { document.addEventListener("mouseup", logMouseClick); document.addEventListener("mousemove", logMouseMoveEvent); document.addEventListener("wheel", logWheelEvent); + HidPanel.mouseActive = true; + HidPanel.setMonitorState(); }, () => { hid.disableMouse(); @@ -33,9 +41,12 @@ export default class HidPanel { document.removeEventListener("mouseup", logMouseClick); document.removeEventListener("mousemove", logMouseMoveEvent); document.removeEventListener("wheel", logWheelEvent); + HidPanel.mouseActive = false; + HidPanel.setMonitorState(); } ); + // Keyboard new ButtonToggle( keyboardButton, true, @@ -45,20 +56,28 @@ export default class HidPanel { hid.enableKeyboard(); document.addEventListener("keydown", logKeyEvent); document.addEventListener("keyup", logKeyEvent); + HidPanel.keyboardActive = true; + HidPanel.setMonitorState(); }, () => { hid.disableKeyboard; document.removeEventListener("keydown", logKeyEvent); document.removeEventListener("keyup", logKeyEvent); + HidPanel.keyboardActive = false; + HidPanel.setMonitorState(); } ); mouseButton.disabled = false; keyboardButton.disabled = false; + HidPanel.mouseActive = true; + HidPanel.keyboardActive = true; + } - // Setup Hid Log - HidPanel.hidMonitor = new InputMonitor(hidLog, MAX_ELEMENTS); - hidLog.style.opacity = "100"; + static setMonitorState() { + HidPanel.hidMonitor.setActive( + HidPanel.mouseActive || HidPanel.keyboardActive + ); } } diff --git a/src/components/inputPanel/inputMonitor.ts b/src/components/inputPanel/inputMonitor.ts index 1e67e80..674cded 100644 --- a/src/components/inputPanel/inputMonitor.ts +++ b/src/components/inputPanel/inputMonitor.ts @@ -9,10 +9,12 @@ export default class InputMonitor { public monitor: HTMLDivElement; public max_elements: number; + public active: boolean; - constructor(div: HTMLDivElement, max_elements: number = 5) { + constructor(div: HTMLDivElement, max_elements: number = 5, active = false) { this.monitor = div; this.max_elements = max_elements; + this.active = active; } /** @@ -39,4 +41,9 @@ export default class InputMonitor { logEntry.classList.add("fade-out"); }, 1500); } + + setActive(active: boolean) { + this.monitor.style.opacity = active ? "1" : "0.5"; + this.active = active; + } } diff --git a/src/components/inputPanel/inputPanelHeader.ts b/src/components/inputPanel/inputPanelHeader.ts index 9d665a1..5176303 100644 --- a/src/components/inputPanel/inputPanelHeader.ts +++ b/src/components/inputPanel/inputPanelHeader.ts @@ -18,6 +18,7 @@ export default class InputPanelHeader { InputPanelHeader.inputContainers.push( document.querySelector("#GUIContainer")! ); + // HID InputPanelHeader.inputButtons.push( document.querySelector("#HIDTab")! @@ -26,6 +27,14 @@ export default class InputPanelHeader { document.querySelector("#HIDContainer")! ); + // HID + InputPanelHeader.inputButtons.push( + document.querySelector("#SensorTab")! + ); + InputPanelHeader.inputContainers.push( + document.querySelector("#SensorContainer")! + ); + // Build toggles with containers for (let i = 0; i < InputPanelHeader.inputButtons.length; i++) { InputPanelHeader.inputToggles.push( diff --git a/src/components/inputPanel/sensorPanel.ts b/src/components/inputPanel/sensorPanel.ts new file mode 100644 index 0000000..ea99296 --- /dev/null +++ b/src/components/inputPanel/sensorPanel.ts @@ -0,0 +1,105 @@ +import { Gyro, Accel } from "webchuck"; +import ButtonToggle from "@/components/toggle/buttonToggle"; +import InputMonitor from "./inputMonitor"; + +// Constants +const MAX_ELEMENTS = 1; + +// Document elements +const gyroButton = document.querySelector("#gyroButton")!; +const gyroLog = document.querySelector("#gyroLog")!; +const accelButton = document.querySelector("#accelButton")!; +const accelLog = document.querySelector("#accelLog")!; + +export default class SensorPanel { + public static gyroMonitor: InputMonitor; + public static accelMonitor: InputMonitor; + public static mouseActive: boolean = false; + public static keyboardActive: boolean = false; + + constructor(gyro: Gyro, accel: Accel) { + SensorPanel.gyroMonitor = new InputMonitor( + gyroLog, + MAX_ELEMENTS, + false + ); + SensorPanel.accelMonitor = new InputMonitor( + accelLog, + MAX_ELEMENTS, + false + ); + + // Gyro + new ButtonToggle( + gyroButton, + false, + "Gyro: On", + "Gyro: Off", + async () => { + if ( + typeof (DeviceOrientationEvent as any).requestPermission === + "function" + ) { + await (DeviceOrientationEvent as any).requestPermission(); + } + gyro.enableGyro(); + window.addEventListener("deviceorientation", logOrientation); + SensorPanel.gyroMonitor.setActive(true); + }, + () => { + gyro.disableGyro(); + window.removeEventListener("deviceorientation", logOrientation); + SensorPanel.gyroMonitor.setActive(false); + } + ); + + // Accel + new ButtonToggle( + accelButton, + false, + "Accel: On", + "Accel: Off", + async () => { + if ( + typeof (DeviceMotionEvent as any).requestPermission === + "function" + ) { + await (DeviceMotionEvent as any).requestPermission(); + } + accel.enableAccel(); + window.addEventListener("devicemotion", logMotion); + SensorPanel.accelMonitor.setActive(true); + }, + () => { + accel.disableAccel(); + window.removeEventListener("devicemotion", logMotion); + SensorPanel.accelMonitor.setActive(false); + } + ); + + gyroButton.disabled = false; + accelButton.disabled = false; + } +} + +//----------------------------------------------------------- +// EVENT HANDLERS FOR SENSOR INPUT +//----------------------------------------------------------- +function logOrientation(event: DeviceOrientationEvent) { + const x = event.alpha ? event.alpha : 0.0; + const y = event.beta ? event.beta : 0.0; + const z = event.gamma ? event.gamma : 0.0; + SensorPanel.gyroMonitor.logEvent( + `x: ${x.toFixed(2)}, y: ${y.toFixed(2)}, z: ${z.toFixed(2)}` + ); +} + +function logMotion(event: DeviceMotionEvent) { + if (event.acceleration === null) return; + const x = event.acceleration.x ? event.acceleration.x : 0.0; + const y = event.acceleration.y ? event.acceleration.y : 0.0; + const z = event.acceleration.z ? event.acceleration.z : 0.0; + SensorPanel.accelMonitor.logEvent( + `x: ${x.toFixed(2)}, y: ${y.toFixed(2)}, z: ${z.toFixed(2)}` + ); +} diff --git a/src/host.ts b/src/host.ts index e8c6734..8f61933 100644 --- a/src/host.ts +++ b/src/host.ts @@ -10,13 +10,14 @@ // date: August 2023 //-------------------------------------------------------- -import { Chuck, HID } from "webchuck"; +import { Chuck, HID, Gyro, Accel } from "webchuck"; import { calculateDisplayDigits } from "@utils/time"; import { ChuckNow } from "@/components/vmMonitor"; import { loadWebChugins } from "@/utils/webChugins"; import Console from "@/components/outputPanel/console"; import Visualizer from "@/components/outputPanel/visualizer"; import HidPanel from "@/components/inputPanel/hidPanel"; +import SensorPanel from "@/components/inputPanel/sensorPanel"; import ChuckBar from "@/components/chuckBar/chuckBar"; import ProjectSystem from "@/components/fileExplorer/projectSystem"; import Recorder from "@/components/chuckBar/recorder"; @@ -42,10 +43,7 @@ let visual: Visualizer; // Recorder let recordGain: GainNode; -// HID -let hid: HID; - -export { theChuck, chuckVersion, audioContext, sampleRate, visual, hid }; +export { theChuck, chuckVersion, audioContext, sampleRate, visual }; // Chuck Time let chuckNowCached: number = 0; @@ -133,9 +131,13 @@ export async function startChuck() { theChuck.connect(recordGain); Recorder.configureRecorder(audioContext, recordGain); - // Start HID, mouse and keyboard on - hid = await HID.init(theChuck); - new HidPanel(hid); + // Enable WebChucK Packages + // HID, mouse and keyboard on + new HidPanel(await HID.init(theChuck)); + new SensorPanel( + await Gyro.init(theChuck, false), + await Accel.init(theChuck, false) + ); // TODO: for debugging, make theChuck global (window as any).theChuck = theChuck; diff --git a/src/styles/index.css b/src/styles/index.css index a5ab7cb..fb0d086 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -291,7 +291,7 @@ td:nth-child(4) { /* INPUT PANEL */ .toggle-button { - @apply m-2 p-1 bg-transparent text-orange enabled:hover:text-white enabled:hover:bg-orange border border-orange rounded transition-all disabled:opacity-50; + @apply mr-2 mb-2 p-1 bg-transparent text-orange enabled:hover:text-white enabled:hover:bg-orange border border-orange rounded transition-all disabled:opacity-50; } .toggle-button.active { @apply bg-orange text-white enabled:hover:bg-orange-400 enabled:dark:hover:bg-orange-400; @@ -304,9 +304,20 @@ td:nth-child(4) { /* HID CONTAINER */ #hidLog { - @apply m-2 p-1 pt-0 w-72 h-32 overflow-hidden border border-dark-d rounded-lg bg-white dark:bg-dark dark:border-dark-4; - width: 280px; - height: 120px; + width: 270px; + height: 128px; +} + +/* SENSOR CONTAINER */ +.sensorLog { + @apply pt-2 !important; + width: 240px; + height: 36px; +} + +/* LOGGER */ +.log-container { + @apply px-2 py-1 mb-1 overflow-hidden border border-dark-d rounded-lg bg-white dark:bg-dark dark:border-dark-4; } .logMsg { margin-bottom: 5px; @@ -422,7 +433,7 @@ dialog::backdrop { } .header { - @apply w-full flex items-center px-1; + @apply w-full flex items-center px-1 border-b border-sky-blue-700 dark:border-dark-5; } .header-item {