mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-20 12:37:05 -04:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f046ae883 | |||
| 2b575d50b9 | |||
| 7b92874909 | |||
| 10bfe32a2d | |||
| a41b07c67a | |||
| 20027d7208 | |||
| f38792c569 | |||
| 2c2bfd7070 | |||
| 484c5e6cdd |
@@ -1,8 +1,10 @@
|
||||
import jsdoc from '../../doc.json';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { h } from './html';
|
||||
import { Scale } from '@tonaljs/tonal';
|
||||
import { soundMap } from 'superdough';
|
||||
//TODO: fix tonal scale import
|
||||
// import { Scale } from '@tonaljs/tonal';
|
||||
// import { soundMap } from '@strudel/webaudio';
|
||||
let soundMap = undefined;
|
||||
import { complex } from '@strudel/tonal';
|
||||
|
||||
const escapeHtml = (str) => {
|
||||
@@ -79,7 +81,9 @@ const hasExcludedTags = (doc) =>
|
||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||
|
||||
export function bankCompletions() {
|
||||
const soundDict = soundMap.get();
|
||||
// TODO: FIX IMPORT
|
||||
const soundDict = soundMap?.get() ?? {};
|
||||
|
||||
const banks = new Set();
|
||||
for (const key of Object.keys(soundDict)) {
|
||||
const [bank, suffix] = key.split('_');
|
||||
@@ -90,13 +94,13 @@ export function bankCompletions() {
|
||||
.map((name) => ({ label: name, type: 'bank' }));
|
||||
}
|
||||
|
||||
// Attempt to get all scale names from Tonal
|
||||
// Attempt to get all scale names from Tonal TODO: FIX IMPORT
|
||||
let scaleCompletions = [];
|
||||
try {
|
||||
scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
|
||||
} catch (e) {
|
||||
console.warn('[autocomplete] Could not load scale names from Tonal:', e);
|
||||
}
|
||||
// try {
|
||||
// scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
|
||||
// } catch (e) {
|
||||
// console.warn('[autocomplete] Could not load scale names from Tonal:', e);
|
||||
// }
|
||||
|
||||
// Valid mode values for voicing
|
||||
const modeCompletions = [
|
||||
@@ -268,7 +272,7 @@ function soundHandler(context) {
|
||||
const inside = text.slice(quoteIdx + 1);
|
||||
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
|
||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||
const soundNames = Object.keys(soundMap.get()).sort();
|
||||
const soundNames = Object.keys(soundMap?.get() ?? {}).sort();
|
||||
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
|
||||
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
|
||||
const from = soundContext.to - fragment.length;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -92,11 +92,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
||||
|
||||
async function blobToDataUrl(blob) {
|
||||
return new Promise((resolve) => {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
resolve(URL.createObjectURL(blob));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user