Compare commits

..

11 Commits

Author SHA1 Message Date
alex 9dc19fb9bf Merge branch 'main' into busparts 2025-10-30 21:37:48 +00:00
alex 4a158db9d2 Revert "state twiddles wip"
This reverts commit 55839e7ee8.
2025-10-30 21:28:58 +00:00
alex 55839e7ee8 state twiddles wip 2025-10-26 21:14:11 +00:00
alex 9aaa9bee6b fixes + tidying 2025-10-24 20:51:12 +01:00
alex 1834d689ef fixes + tidying 2025-10-24 20:48:35 +01:00
Alex McLean 77a7a148f0 wip 2025-10-23 21:26:27 +01:00
Alex McLean 52931e879d nearly there 2025-10-23 16:18:47 +01:00
Alex McLean d5c9be1c30 add _processParts() 2025-10-23 15:14:22 +01:00
Alex McLean 2b30ab3008 Merge branch 'main' of ssh://codeberg.org/uzu/strudel into busparts 2025-10-23 14:59:07 +01:00
Alex McLean 13cbeb3f67 wip 2025-10-23 14:58:57 +01:00
alex 352d8659ec wip 2025-10-23 14:02:41 +01:00
9 changed files with 95 additions and 174 deletions
+9
View File
@@ -72,6 +72,15 @@ export function registerControl(names, ...aliases) {
return bag;
}
// Constructs a superdirt effect bus control
const bus = register('bus', function (busid, control_name, busval, pat) {
return pat[control_name]('c' + busid).withValue((val) => {
const newval = { ...val };
newval['^' + control_name] = [busid, busval];
return newval;
});
});
/**
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
* separated by ':'.
+2 -3
View File
@@ -58,11 +58,10 @@ export class Cyclist {
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
haps.forEach((hap) => {
if (hap.hasOnset()) {
if (hap.hasOnset() || hap.context.processParts) {
const targetTime =
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
(hap.part.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
const duration = hap.duration / this.cps;
// the following line is dumb and only here for backwards compatibility
// see https://codeberg.org/uzu/strudel/pulls/1004
+5 -1
View File
@@ -16,7 +16,7 @@ export class Hap {
then the whole will be returned as None, in which case the given
value will have been sampled from the point halfway between the
start and end of the 'part' timespan.
The context is to store a list of source code locations causing the event.
The context is to store metadata, including a list of source code locations causing the event.
The word 'Event' is more or less a reserved word in javascript, hence this
class is named called 'Hap'.
@@ -161,6 +161,10 @@ export class Hap {
return new Hap(this.whole, this.part, this.value, context);
}
withContext(f) {
return new Hap(this.whole, this.part, this.value, f(this.context));
}
ensureObjectValue() {
/* if (isNote(hap.value)) {
// supports primitive hap values that look like notes
+5
View File
@@ -62,7 +62,12 @@ export class Pattern {
this.__steps = steps === undefined ? undefined : Fraction(steps);
}
_processParts() {
return this.withHap((hap) => hap.withContext((c) => ({ ...c, processParts: true })));
}
setSteps(steps) {
// TODO shouldn't this be pure and return a new pattern?
this._steps = steps;
return this;
}
+1 -7
View File
@@ -14,12 +14,7 @@ npm i @strudel/gamepad --save
import { gamepad } from '@strudel/gamepad';
// Initialize gamepad (optional index parameter, defaults to 0)
const pad = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
//const pad = gamepad(0, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
//const pad = gamepad(0, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
//const pad = gamepad(0, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
const pad = gamepad(0);
// Use gamepad inputs in patterns
const pattern = sequence([
@@ -91,7 +86,6 @@ $: sound("hadoken").gain(pad.checkSequence(HADOKEN))
## Multiple Gamepads
You can connect multiple gamepads by specifying the gamepad index:
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
```javascript
const pad1 = gamepad(0); // First gamepad
+1 -24
View File
@@ -107,32 +107,9 @@ $: s("free_hadouken -").slow(2)
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
`} />
### Button Sequences
### Button Mappings
The gamepad module supports different button mapping configurations to accommodate various gamepad layouts:
- **XBOX** (Default): Standard Xbox-style mapping where A=0, B=1, X=2, Y=3
- **NES**: Nintendo-style mapping where B=0, A=1, Y=2, X=3
- **Custom**: Define your own button mapping by passing an object with button assignments
You can specify the mapping when initializing the gamepad:
<MiniRepl
client:idle
tune={`
const pad1 = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
const pad2 = gamepad(1, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
const pad3 = gamepad(2, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
const pad4 = gamepad(3, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
`}
/>
### Multiple Gamepads
## Multiple Gamepads
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
<MiniRepl
client:idle
+28 -119
View File
@@ -1,62 +1,35 @@
// @strudel/gamepad/index.mjs
import { signal } from '@strudel/core';
import { logger } from '@strudel/core';
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
const buttonMapSettings = {
XBOX: {
// XBOX mapping default
a: 0,
b: 1,
x: 2,
y: 3,
lb: 4,
rb: 5,
lt: 6,
rt: 7,
back: 8,
start: 9,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
},
NES: {
// Nintendo mapping
a: 1,
b: 0,
x: 3,
y: 2,
lb: 4,
rb: 5,
lt: 6,
rt: 7,
back: 8,
start: 9,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
},
export const buttonMap = {
a: 0,
b: 1,
x: 2,
y: 3,
lb: 4,
rb: 5,
lt: 6,
rt: 7,
back: 8,
start: 9,
u: 12,
up: 12,
d: 13,
down: 13,
l: 14,
left: 14,
r: 15,
right: 15,
};
class ButtonSequenceDetector {
constructor(timeWindow = 1000, mapping) {
constructor(timeWindow = 1000) {
this.sequence = [];
this.timeWindow = timeWindow;
this.lastInputTime = 0;
this.buttonStates = Array(16).fill(0); // Track previous state of each button
this.buttonMap = mapping;
// Button mapping for character inputs
}
@@ -71,8 +44,7 @@ class ButtonSequenceDetector {
}
// Store the button name instead of index
const buttonName =
Object.keys(this.buttonMap).find((key) => this.buttonMap[key] === buttonIndex) || buttonIndex.toString();
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
this.sequence.push({
input: buttonName,
@@ -115,9 +87,9 @@ class ButtonSequenceDetector {
// Check if either the input matches directly or they refer to the same button in the map
return (
input === target ||
this.buttonMap[input] === this.buttonMap[target] ||
buttonMap[input] === buttonMap[target] ||
// Also check if the numerical index matches
this.buttonMap[input] === parseInt(target)
buttonMap[input] === parseInt(target)
);
})
? 1
@@ -126,10 +98,9 @@ class ButtonSequenceDetector {
}
class GamepadHandler {
constructor(index = 0, mapping) {
constructor(index = 0) {
// Add index parameter
this._gamepads = {};
this._mapping = mapping;
this._activeGamepad = index; // Use provided index
this._axes = [0, 0, 0, 0];
this._buttons = Array(16).fill(0);
@@ -172,66 +143,12 @@ class GamepadHandler {
}
}
// Add utility function to list all connected gamepads
export const listGamepads = () => {
const gamepads = navigator.getGamepads();
const connectedGamepads = Array.from(gamepads)
.filter((gp) => gp !== null)
.map((gp) => ({
index: gp.index,
id: gp.id,
mapping: gp.mapping,
buttons: gp.buttons.length,
axes: gp.axes.length,
connected: gp.connected,
timestamp: gp.timestamp,
}));
// Format the gamepads info into a readable string
const gamepadsInfo = connectedGamepads.map((gp) => `${gp.index}: ${gp.id}`).join('\n');
logger(`[gamepad] available gamepads:\n${gamepadsInfo}`);
return connectedGamepads;
};
// Module-level state store for toggle states
const gamepadStates = new Map();
export const gamepad = (index = 0, mapping = 'XBOX') => {
// list connected gamepads
const connectedGamepads = listGamepads();
// Check if the requested gamepad index exists
const requestedGamepad = connectedGamepads.find((gp) => gp.index === index);
if (!requestedGamepad) {
throw new Error(
`[gamepad] gamepad at index ${index} not found. available gamepads: ${connectedGamepads.map((gp) => gp.index).join(', ')}`,
);
}
// Handle button mapping
let buttonMap = buttonMapSettings.XBOX;
if (typeof mapping === 'string') {
buttonMap = buttonMapSettings[mapping.toUpperCase()];
} else if (typeof mapping === 'object') {
buttonMap = { ...buttonMapSettings.XBOX, ...mapping };
// Check that all mapping values are valid button indices
const maxButtons = requestedGamepad.buttons; // Standard gamepad has 16 buttons
for (const [key, value] of Object.entries(mapping)) {
if (typeof value !== 'number' || value < 0 || value >= maxButtons) {
throw new Error(
`[gamepad] invalid button mapping for '${key}': ${value}. Must be a number between 0 and ${maxButtons - 1}`,
);
}
}
}
if (!buttonMap) {
throw new Error(`[gamepad] button mapping '${mapping}' not found`);
}
const handler = new GamepadHandler(index, buttonMap);
const sequenceDetector = new ButtonSequenceDetector(2000, buttonMap);
export const gamepad = (index = 0) => {
const handler = new GamepadHandler(index);
const sequenceDetector = new ButtonSequenceDetector(2000);
// Base signal that polls gamepad state and handles sequence detection
const baseSignal = signal((t) => {
@@ -301,14 +218,8 @@ export const gamepad = (index = 0, mapping = 'XBOX') => {
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
};
const checkSequence = btnSequence;
const sequence = btnSequence;
const btnSeq = btnSequence;
const btnseq = btnSequence;
const seq = btnSequence;
logger(
`[gamepad] connected to gamepad ${index} (${requestedGamepad.id}) with ${typeof mapping === 'object' ? 'custom' : mapping} mapping`,
);
const btnseq = btnSeq;
// Return an object with all controls
return {
@@ -323,11 +234,9 @@ export const gamepad = (index = 0, mapping = 'XBOX') => {
]),
),
checkSequence,
sequence,
btnSequence,
btnSeq,
btnseq,
seq,
raw: baseSignal,
};
};
+35 -7
View File
@@ -52,6 +52,7 @@ export function parseControlsFromHap(hap, cps) {
controls.unit === 'c' && controls.speed != null && (controls.speed = controls.speed / cps);
const channels = controls.channels;
channels != undefined && (controls.channels = JSON.stringify(channels));
return controls;
}
@@ -63,16 +64,42 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
const keyvals = Object.entries(controls).flat();
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
if ('oschost' in hap.value) {
msg['host'] = hap.value['oschost'];
}
if ('oscport' in hap.value) {
msg['port'] = hap.value['oscport'];
}
ws.send(JSON.stringify(msg));
}
export async function superdirtTrigger(hap, currentTime, cps = 1, targetTime) {
const ws = await connect();
const controls = parseControlsFromHap(hap, cps);
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
let oschost = '127.0.0.1';
let oscport = 57120;
let bushost = oschost;
let busport = 57110;
for (const [k, v] of Object.entries(controls)) {
if (k.startsWith('^')) {
const bus_idval = v;
const msg = JSON.stringify({
oschost: bushost,
oscport: busport,
address: '/c_set',
args: bus_idval,
timestamp: ts,
});
// console.log('bus message', msg);
ws.send(msg);
// So they aren't sent with trigger messages
delete controls[k];
}
}
if (hap.hasOnset()) {
const keyvals = Object.entries(controls).flat();
ws.send(JSON.stringify({ oschost, oscport, address: '/dirt/play', args: keyvals, timestamp: ts }));
}
}
/**
*
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
@@ -83,3 +110,4 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
* @returns Pattern
*/
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
export const superdirt = register('superdirt', (pat) => pat._processParts().onTrigger(superdirtTrigger));
+9 -13
View File
@@ -12,18 +12,13 @@ import { WebSocketServer } from 'ws';
import osc from 'osc';
const WS_PORT = 8080; // WebSocket server port
const OSC_REMOTE_IP = '127.0.0.1';
const OSC_REMOTE_PORT = 57120;
const udpPort = new osc.UDPPort({
localAddress: '0.0.0.0',
localPort: 0,
remoteAddress: OSC_REMOTE_IP,
remotePort: OSC_REMOTE_PORT,
});
udpPort.open();
console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`);
udpPort.on('error', (e) => {
console.log('Error: ', e);
@@ -36,23 +31,24 @@ wss.on('connection', (ws) => {
console.log('New WebSocket connection');
ws.on('message', (message) => {
let osc_host = '127.0.0.1';
let osc_port = 57120;
let oschost = '127.0.0.1';
let oscport = 57120;
try {
const data = JSON.parse(message);
if ('host' in data) {
osc_host = data['host'];
if ('oschost' in data) {
oschost = data['oschost'];
delete data['oschost'];
}
if ('port' in data) {
osc_port = data['port'];
if ('oscport' in data) {
oscport = data['oscport'];
delete data['oscport'];
}
let msg = { address: data['address'], args: data['args'] };
if ('timestamp' in data) {
msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] };
}
udpPort.send(msg, osc_host, osc_port);
udpPort.send(msg, oschost, oscport);
} catch (err) {
console.error('Error parsing message:', err);
}