diff --git a/index.html b/index.html
index 133ac8f..744e040 100644
--- a/index.html
+++ b/index.html
@@ -328,18 +328,33 @@
+
-
+
-
+
+
+
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 {