mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-14 09:46:53 -04:00
Compare commits
25 Commits
osc-timing
..
show12
| Author | SHA1 | Date | |
|---|---|---|---|
| 9258857e36 | |||
| 8f81463b9c | |||
| 8120fc72ab | |||
| d65b24db8f | |||
| f1dd209749 | |||
| d6f0b863f0 | |||
| 5dbbc7b9e1 | |||
| beb16f4040 | |||
| ad7e301394 | |||
| 7acb9a3ac7 | |||
| 9665b233a1 | |||
| 6c67851909 | |||
| 109ea3093b | |||
| 8d991ad8ae | |||
| 0b69ce21c7 | |||
| 4f05674cb3 | |||
| feab3f5c86 | |||
| 90c05ec38a | |||
| 7e0eed87cb | |||
| 1a1197f67a | |||
| 3c5afb32f3 | |||
| 76cbd23859 | |||
| b436ae789c | |||
| 275731afc7 | |||
| 5cb2214d88 |
@@ -11,6 +11,7 @@ import {
|
||||
keymap,
|
||||
lineNumbers,
|
||||
} from '@codemirror/view';
|
||||
import {repeatCharKeymap} from './repeatcharacter.mjs';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { logger, registerControl, repl } from '@strudel/core';
|
||||
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
|
||||
@@ -20,11 +21,16 @@ import { evalBlock } from './block_utilities.mjs';
|
||||
import { flash, isFlashEnabled } from './flash.mjs';
|
||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||
import { keybindings } from './keybindings.mjs';
|
||||
import { jumpToCharacter } from './labelJump.mjs';
|
||||
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
||||
import {
|
||||
deleteAllInlineBeforeCharacter,
|
||||
InsertCharBeforeChar,
|
||||
jumpToCharacter,
|
||||
jumpToNextCharacter,
|
||||
} from './labelJump.mjs';
|
||||
|
||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||
|
||||
@@ -44,9 +50,9 @@ export const extensions = {
|
||||
isMultiCursorEnabled: (on) =>
|
||||
on
|
||||
? [
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
||||
]
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
||||
]
|
||||
: [],
|
||||
};
|
||||
export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||
@@ -75,6 +81,10 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
|
||||
decode: JSON.parse,
|
||||
});
|
||||
|
||||
const ANON_LABEL = '$';
|
||||
const SOLO_LABEL = 'S';
|
||||
const MUTE_LABEL = '_';
|
||||
|
||||
// https://codemirror.net/docs/guide/
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
|
||||
const settings = codemirrorSettings.get();
|
||||
@@ -102,6 +112,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
EditorView.updateListener.of((v) => onChange(v)),
|
||||
drawSelection({ cursorBlinkRate: 0 }),
|
||||
repeatCharKeymap,
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{
|
||||
@@ -138,12 +149,74 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
},
|
||||
{
|
||||
key: 'Alt-w',
|
||||
run: (view) => jumpToCharacter(view, '$', 1),
|
||||
run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
|
||||
},
|
||||
{
|
||||
key: 'Alt-q',
|
||||
run: (view) => jumpToCharacter(view, '$', -1),
|
||||
run: (view) => {
|
||||
return jumpToNextCharacter(view, ANON_LABEL, -1);
|
||||
},
|
||||
},
|
||||
// clear all muted
|
||||
{
|
||||
key: `Alt-Ctrl-0`,
|
||||
run: (view) => {
|
||||
return deleteAllInlineBeforeCharacter(view, MUTE_LABEL + ANON_LABEL);
|
||||
},
|
||||
},
|
||||
// clear all solod
|
||||
{
|
||||
key: `Alt-Shift-0`,
|
||||
run: (view) => {
|
||||
return deleteAllInlineBeforeCharacter(view, SOLO_LABEL + ANON_LABEL);
|
||||
},
|
||||
},
|
||||
// clear all solo and mute
|
||||
{
|
||||
key: `Ctrl-Shift-0`,
|
||||
run: (view) => {
|
||||
return deleteAllInlineBeforeCharacter(view, ANON_LABEL);
|
||||
},
|
||||
},
|
||||
...Array.from({ length: 9 }).map((_, i) => {
|
||||
let num = i + 1;
|
||||
return {
|
||||
key: `Alt-${num}`,
|
||||
run: (view) => {
|
||||
return jumpToCharacter(view, ANON_LABEL, i);
|
||||
},
|
||||
};
|
||||
}),
|
||||
// handle solo toggles 1-9
|
||||
...Array.from({ length: 9 }).map((_, i) => {
|
||||
let num = i + 1;
|
||||
return {
|
||||
key: `Alt-Shift-${num}`,
|
||||
run: (view) => {
|
||||
return InsertCharBeforeChar(view, ANON_LABEL, SOLO_LABEL, i);
|
||||
},
|
||||
};
|
||||
}),
|
||||
// handle mute toggles 1-9
|
||||
...Array.from({ length: 9 }).map((_, i) => {
|
||||
let num = i + 1;
|
||||
return {
|
||||
key: `Alt-Ctrl-${num}`,
|
||||
run: (view) => {
|
||||
return InsertCharBeforeChar(view, ANON_LABEL, MUTE_LABEL, i);
|
||||
},
|
||||
};
|
||||
}),
|
||||
// Handle clearing mutes and solos 1-9
|
||||
...Array.from({ length: 9 }).map((_, i) => {
|
||||
let num = i + 1;
|
||||
return {
|
||||
key: `Ctrl-Shift-${num}`,
|
||||
run: (view) => {
|
||||
return InsertCharBeforeChar(view, ANON_LABEL, '', i);
|
||||
},
|
||||
};
|
||||
}),
|
||||
/* {
|
||||
key: 'Ctrl-Shift-.',
|
||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||
|
||||
@@ -1,18 +1,54 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { SearchCursor } from '@codemirror/search';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
|
||||
export function jumpToCharacter(view, character, direction = 1) {
|
||||
/**
|
||||
* gets all of the positions of a character in a document, excluding commented out lines
|
||||
* @param { EditorState} state
|
||||
* @param {String} character
|
||||
* @returns {number[]}
|
||||
*/
|
||||
function getCharacterPositions(state, character) {
|
||||
const cursor = new SearchCursor(state.doc, character);
|
||||
|
||||
const characterPositions = [];
|
||||
while (!cursor.next().done) {
|
||||
|
||||
const linestartpos = state.doc.lineAt(cursor.value.to).from
|
||||
if (!isLineCommentedOut(state, linestartpos)) {
|
||||
|
||||
characterPositions.push(cursor.value.to);
|
||||
}
|
||||
}
|
||||
return characterPositions;
|
||||
}
|
||||
|
||||
function isLineCommentedOut(state, pos) {
|
||||
|
||||
const line = state.doc.lineAt(pos);
|
||||
// remove white space
|
||||
pos = line.from + line.text.search(/\S/)
|
||||
|
||||
const tree = syntaxTree(state);
|
||||
const node = tree.resolveInner(pos, 1)
|
||||
return node.name.includes("Comment")
|
||||
}
|
||||
|
||||
/**
|
||||
* jump to the next character in a document
|
||||
* @param {EditorView} view
|
||||
* @param {String} character
|
||||
* @param {number} direction 0 or 1
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function jumpToNextCharacter(view, character, direction = 1) {
|
||||
const { state, dispatch } = view;
|
||||
const pos = state.selection.main.head;
|
||||
const cursor = new SearchCursor(state.doc, character);
|
||||
|
||||
let characterPositions = [];
|
||||
let jumpPos;
|
||||
while (!cursor.next().done) {
|
||||
characterPositions.push(cursor.value.to);
|
||||
}
|
||||
const characterPositions = getCharacterPositions(state, character);
|
||||
if (!characterPositions.length) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
if (direction > 0) {
|
||||
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
||||
@@ -21,11 +57,97 @@ export function jumpToCharacter(view, character, direction = 1) {
|
||||
}
|
||||
|
||||
if (jumpPos == null) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
const selection = EditorSelection.cursor(jumpPos - 1);
|
||||
dispatch({
|
||||
selection: EditorSelection.cursor(jumpPos - 1),
|
||||
scrollIntoView: true,
|
||||
selection,
|
||||
effects: EditorView.scrollIntoView(
|
||||
selection.head,
|
||||
{ y: "start" }
|
||||
)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {EditorView} view
|
||||
* @param {String} character
|
||||
* @param {number} index the instance of the character
|
||||
* @returns {true}
|
||||
*/
|
||||
export function jumpToCharacter(view, character, index) {
|
||||
const { state, dispatch } = view;
|
||||
const characterPositions = getCharacterPositions(state, character);
|
||||
const pos = characterPositions.at(index) ?? characterPositions.at(-1);
|
||||
if (pos == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const selection = EditorSelection.cursor(pos - 1);
|
||||
dispatch({
|
||||
selection,
|
||||
effects: EditorView.scrollIntoView(
|
||||
selection.head,
|
||||
{ y: "start" }
|
||||
)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {EditorView} view
|
||||
* @param {String} character
|
||||
* @returns {true}
|
||||
*/
|
||||
export function deleteAllInlineBeforeCharacter(view, character) {
|
||||
const { state, dispatch } = view;
|
||||
const characterPositions = getCharacterPositions(state, character);
|
||||
|
||||
const changes = [];
|
||||
characterPositions.forEach((pos) => {
|
||||
const line = state.doc.lineAt(pos);
|
||||
if (state.doc.sliceString(line.from, line.from + 2) === COMMENT_STRING) {
|
||||
return;
|
||||
}
|
||||
changes.push({
|
||||
from: line.from,
|
||||
to: pos - 1,
|
||||
insert: '',
|
||||
});
|
||||
});
|
||||
dispatch({ changes });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {EditorView} view
|
||||
* @param {String} character
|
||||
* @param {String} character2
|
||||
* @param {number} index
|
||||
* @returns {true}
|
||||
*/
|
||||
export function InsertCharBeforeChar(view, character, character2, index) {
|
||||
const { state, dispatch } = view;
|
||||
|
||||
const changes = [];
|
||||
const characterPositions = getCharacterPositions(state, character);
|
||||
const labelpos = characterPositions.at(index) ?? characterPositions.at(-1);
|
||||
const line = state.doc.lineAt(labelpos);
|
||||
|
||||
//delete preceeding characters
|
||||
changes.push({
|
||||
from: line.from,
|
||||
to: labelpos - 1,
|
||||
insert: '',
|
||||
});
|
||||
|
||||
changes.push({
|
||||
insert: character2,
|
||||
from: line.from,
|
||||
});
|
||||
|
||||
dispatch({ changes });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Compartment } from "@codemirror/state";
|
||||
import { keymap } from "@codemirror/view";
|
||||
|
||||
const repeatMode = new Compartment();
|
||||
|
||||
function repeatPreviousChar(times) {
|
||||
return ({ state, dispatch }) => {
|
||||
const { from, empty } = state.selection.main;
|
||||
if (!empty || from === 0) return false;
|
||||
|
||||
const prevChar = state.doc.sliceString(from - 1, from);
|
||||
const text = Array(times).fill(prevChar).join(" ");
|
||||
|
||||
dispatch(state.update({
|
||||
changes: {
|
||||
from,
|
||||
insert: " " + text
|
||||
}
|
||||
}));
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function exitRepeatMode(view) {
|
||||
view.dispatch({
|
||||
effects: repeatMode.reconfigure([])
|
||||
});
|
||||
}
|
||||
|
||||
const digitBindings = Array.from({ length: 9 }, (_, i) => ({
|
||||
key: String(i + 1),
|
||||
run(view) {
|
||||
repeatPreviousChar(i + 1)(view);
|
||||
exitRepeatMode(view);
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
||||
export const repeatCharKeymap = [
|
||||
|
||||
repeatMode.of([]),
|
||||
|
||||
keymap.of([
|
||||
{
|
||||
key: "Alt-r",
|
||||
run(view) {
|
||||
view.dispatch({
|
||||
effects: repeatMode.reconfigure(keymap.of(digitBindings))
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
])
|
||||
];
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
dough.mjs
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/osc.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { register, noteToMidi } from '@strudel/core';
|
||||
import { Dough, doughsamples } from 'dough-synth';
|
||||
import { getAudioContext, ensureMinimalOutput } from '@strudel/webaudio';
|
||||
import wasm from 'dough-synth/dough.wasm?url';
|
||||
import workletCode from 'dough-synth/dough.js?raw';
|
||||
|
||||
Object.assign(globalThis, { doughsamples });
|
||||
|
||||
let D;
|
||||
/**
|
||||
*
|
||||
* initializes dough ahead of time. you don't need to use this, but if you do, you can await it, so dough is ready before the pattern starts.
|
||||
*
|
||||
* @name initDough
|
||||
* @tags external_io
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* await initDough()
|
||||
* $: chord("<Dm9 Dm11 Dm7>").offset("<-1 0 1 0>").voicing()
|
||||
* .phaser(3).fm(1.4).fmh(1.01)
|
||||
* .dough()
|
||||
*/
|
||||
export function initDough() {
|
||||
if (!D || D.audioContext !== getAudioContext()) {
|
||||
D = new Dough({
|
||||
wasm,
|
||||
workletCode,
|
||||
audioContext: getAudioContext(),
|
||||
});
|
||||
}
|
||||
return D.ready;
|
||||
}
|
||||
|
||||
export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) {
|
||||
const offset = D.context_offset?.[0];
|
||||
if (!offset) {
|
||||
return; // not ready
|
||||
}
|
||||
hap.ensureObjectValue();
|
||||
const event = {
|
||||
dough: 'play',
|
||||
...hap.value,
|
||||
time: targetTime - offset,
|
||||
duration: hap.duration / cps,
|
||||
};
|
||||
if (typeof event.note === 'string') {
|
||||
event.note = noteToMidi(event.note);
|
||||
}
|
||||
D.evaluate(event);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Uses dough as the audio engine. more info at https://dough.strudel.cc
|
||||
*
|
||||
* @name dough
|
||||
* @tags external_io
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* $: chord("<Dm9 Dm11 Dm7>").offset("<-1 0 1 0>").voicing()
|
||||
* .phaser(3).fm(1.4).fmh(1.01)
|
||||
* .dough()
|
||||
*/
|
||||
export const dough = register('dough', (pat) => {
|
||||
initDough();
|
||||
ensureMinimalOutput();
|
||||
return pat.onTrigger(doughTrigger);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@strudel/dough",
|
||||
"version": "1.3.2",
|
||||
"description": "dough synth integration for strudel",
|
||||
"main": "dough.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://codeberg.org/uzu/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://codeberg.org/uzu/strudel/issues"
|
||||
},
|
||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*",
|
||||
"dough-synth": "0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
@@ -67,13 +67,13 @@ if (typeof window !== 'undefined') {
|
||||
});
|
||||
}
|
||||
|
||||
export const dough = async (code) => {
|
||||
export const rawdsp = async (code) => {
|
||||
const ac = getAudioContext();
|
||||
stop();
|
||||
worklet = await dspWorklet(ac, code);
|
||||
worklet.node.connect(ac.destination);
|
||||
};
|
||||
|
||||
export function doughTrigger(hap, currentTime, cps, targetTime) {
|
||||
export function rawdspTrigger(hap, currentTime, cps, targetTime) {
|
||||
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||
import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, registerWaveTable, soundMap } from './index.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import {
|
||||
@@ -14,6 +14,13 @@ import { logger } from './logger.mjs';
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
|
||||
/**
|
||||
*
|
||||
* @typedef {Object} SampleMetaData
|
||||
* @property {string} url
|
||||
* @property {midi} number
|
||||
*/
|
||||
|
||||
export const getCachedBuffer = (url) => bufferCache[url];
|
||||
|
||||
function humanFileSize(bytes, si) {
|
||||
@@ -55,8 +62,9 @@ export const getDur = getDuration;
|
||||
|
||||
export function getSampleInfo(hapValue, bank) {
|
||||
const { speed = 1.0 } = hapValue;
|
||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||
const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank);
|
||||
|
||||
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||
return { transpose, url, index, midi, label, playbackRate };
|
||||
}
|
||||
|
||||
@@ -179,7 +187,12 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
|
||||
if (baseUrl.startsWith('github:')) {
|
||||
baseUrl = githubPath(baseUrl, '');
|
||||
}
|
||||
const fullUrl = (v) => baseUrl + v;
|
||||
/**
|
||||
*
|
||||
* @param {string} v
|
||||
* @returns {SampleMetaData}
|
||||
*/
|
||||
const fullUrl = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
|
||||
if (Array.isArray(value)) {
|
||||
//return [key, value.map(replaceUrl)];
|
||||
value = value.map(fullUrl);
|
||||
@@ -390,3 +403,16 @@ export function registerSampleSource(key, bank, params) {
|
||||
registerSample(key, bank, params);
|
||||
}
|
||||
}
|
||||
|
||||
export function extractMidiNoteFromString(str) {
|
||||
const regex = /_([a-gA-G])([#b])?([1-9])?\b/;
|
||||
const match = str.match(regex);
|
||||
if (match == null) {
|
||||
return BASE_MIDI_NOTE;
|
||||
}
|
||||
const base = match[1].toUpperCase();
|
||||
const accidental = match[2] ?? '';
|
||||
const octave_str = match[3] ?? '';
|
||||
const parsedVal = base + accidental + octave_str;
|
||||
return noteToMidi(parsedVal);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@ const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||
export const getAccidentalsOffset = (accidentals) => {
|
||||
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} note
|
||||
* @param {number} defaultOctave
|
||||
* @returns {number}
|
||||
*/
|
||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
@@ -29,6 +34,11 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
||||
const offset = getAccidentalsOffset(acc);
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @param {number} n
|
||||
* @returns {number}
|
||||
*/
|
||||
export const midiToFreq = (n) => {
|
||||
return Math.pow(2, (n - 69) / 12) * 440;
|
||||
};
|
||||
@@ -38,7 +48,17 @@ export const freqToMidi = (freq) => {
|
||||
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||
};
|
||||
|
||||
export const valueToMidi = (value, fallbackValue) => {
|
||||
/**
|
||||
*
|
||||
* @param {string} note
|
||||
* @param {number} defaultOctave
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
export const noteToFreq = (note, defaultOctave = 3) => {
|
||||
return midiToFreq(noteToMidi(note, defaultOctave));
|
||||
};
|
||||
function __valueToMidi(value) {
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('valueToMidi: expected object value');
|
||||
}
|
||||
@@ -52,10 +72,15 @@ export const valueToMidi = (value, fallbackValue) => {
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
}
|
||||
if (!fallbackValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
export const valueToMidi = (value, fallbackValue) => {
|
||||
const parsedValue = __valueToMidi(value) ?? fallbackValue;
|
||||
if (parsedValue == null) {
|
||||
throw new Error('valueToMidi: expected freq or note to be set');
|
||||
}
|
||||
return fallbackValue;
|
||||
return parsedValue;
|
||||
};
|
||||
|
||||
export function nanFallback(value, fallback = 0, silent) {
|
||||
@@ -84,15 +109,18 @@ export function secondsToCycle(t, cps) {
|
||||
// deduces relevant info for sample loading from hap.value and sample definition
|
||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||
export const BASE_MIDI_NOTE = 36;
|
||||
|
||||
export function getCommonSampleInfo(hapValue, bank) {
|
||||
const { s, n = 0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
let url;
|
||||
const maybeMidiNote = __valueToMidi(hapValue);
|
||||
const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
|
||||
let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
|
||||
let index = 0;
|
||||
let samplemeta;
|
||||
if (Array.isArray(bank)) {
|
||||
index = getSoundIndex(n, bank.length);
|
||||
url = bank[index];
|
||||
samplemeta = bank[index];
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||
// object format will expect keys as notes
|
||||
@@ -104,10 +132,14 @@ export function getCommonSampleInfo(hapValue, bank) {
|
||||
);
|
||||
transpose = -midiDiff(closest); // semitones to repitch
|
||||
index = getSoundIndex(n, bank[closest].length);
|
||||
url = bank[closest][index];
|
||||
samplemeta = bank[closest][index];
|
||||
}
|
||||
const label = `${s}:${index}`;
|
||||
return { transpose, url, index, midi, label };
|
||||
if (maybeMidiNote != null) {
|
||||
transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi);
|
||||
}
|
||||
|
||||
return { transpose, index, midi, label, url: samplemeta.url };
|
||||
}
|
||||
|
||||
/** Selects entries from `source` and renames them via `map`
|
||||
|
||||
@@ -114,11 +114,11 @@ async function fetchSample(url) {
|
||||
return { channels, sampleRate: buffer.sampleRate };
|
||||
}
|
||||
|
||||
export async function doughsamples(sampleMap, baseUrl) {
|
||||
export async function supradoughsamples(sampleMap, baseUrl) {
|
||||
if (typeof sampleMap === 'string') {
|
||||
const [json, base] = await fetchSampleMap(sampleMap);
|
||||
// console.log('json', json, 'base', base);
|
||||
return doughsamples(json, base);
|
||||
return supradoughsamples(json, base);
|
||||
}
|
||||
Object.entries(sampleMap).map(async ([key, urls]) => {
|
||||
if (key !== '_base') {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
superdough,
|
||||
getAudioContext,
|
||||
setLogger,
|
||||
doughTrigger,
|
||||
rawdspTrigger,
|
||||
registerWorklet,
|
||||
setAudioContext,
|
||||
initAudio,
|
||||
@@ -173,8 +173,8 @@ export function webaudioRepl(options = {}) {
|
||||
return repl(options);
|
||||
}
|
||||
|
||||
Pattern.prototype.dough = function () {
|
||||
return this.onTrigger(doughTrigger, 1);
|
||||
Pattern.prototype.rawdsp = function () {
|
||||
return this.onTrigger(rawdspTrigger, 1);
|
||||
};
|
||||
|
||||
function audioBufferToWav(buffer, opt) {
|
||||
|
||||
Generated
+37
-1
@@ -303,6 +303,22 @@ importers:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
|
||||
packages/dough:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@strudel/webaudio':
|
||||
specifier: workspace:*
|
||||
version: link:../webaudio
|
||||
dough-synth:
|
||||
specifier: 0.2.4
|
||||
version: 0.2.4
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/draw:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
@@ -811,6 +827,9 @@ importers:
|
||||
'@strudel/desktopbridge':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/desktopbridge
|
||||
'@strudel/dough':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/dough
|
||||
'@strudel/draw':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/draw
|
||||
@@ -2069,6 +2088,7 @@ packages:
|
||||
'@lerna/create@8.1.9':
|
||||
resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: This package is an implementation detail of Lerna and is no longer published separately.
|
||||
|
||||
'@lezer/common@1.2.3':
|
||||
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
||||
@@ -3068,6 +3088,7 @@ packages:
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
'@vite-pwa/astro@0.5.0':
|
||||
resolution: {integrity: sha512-Yd3Pug/c1EUQJXWvzYh6eTtoqzmSKcdCqWCcNquZeaD13tLWpBb2FIPJ4HMULVY6+GfxMvrT+OBuMrbHQCvftw==}
|
||||
@@ -3719,6 +3740,7 @@ packages:
|
||||
conventional-changelog-core@5.0.1:
|
||||
resolution: {integrity: sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A==}
|
||||
engines: {node: '>=14'}
|
||||
deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead.
|
||||
|
||||
conventional-changelog-preset-loader@3.0.0:
|
||||
resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==}
|
||||
@@ -4050,6 +4072,10 @@ packages:
|
||||
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dough-synth@0.2.4:
|
||||
resolution: {integrity: sha512-qMnMKVj1B2ivUgobNCwqJqNzZHOMN+5339OmCOk07zIDEK7iCYdufa+ZDE2BEgmKrWDyhGS7oTNFjHtTwzEv9w==}
|
||||
hasBin: true
|
||||
|
||||
dset@3.1.4:
|
||||
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4582,6 +4608,7 @@ packages:
|
||||
git-raw-commits@3.0.0:
|
||||
resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==}
|
||||
engines: {node: '>=14'}
|
||||
deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
|
||||
hasBin: true
|
||||
|
||||
git-remote-origin-url@2.0.0:
|
||||
@@ -4591,6 +4618,7 @@ packages:
|
||||
git-semver-tags@5.0.1:
|
||||
resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==}
|
||||
engines: {node: '>=14'}
|
||||
deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
|
||||
hasBin: true
|
||||
|
||||
git-up@7.0.0:
|
||||
@@ -4618,15 +4646,17 @@ packages:
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
glob@9.3.5:
|
||||
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
globalize@0.1.1:
|
||||
resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==}
|
||||
@@ -6409,6 +6439,7 @@ packages:
|
||||
prebuild-install@7.1.1:
|
||||
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
||||
hasBin: true
|
||||
|
||||
precinct@12.1.2:
|
||||
@@ -7233,6 +7264,7 @@ packages:
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
temp-dir@1.0.0:
|
||||
resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==}
|
||||
@@ -7360,6 +7392,7 @@ packages:
|
||||
tsconfck@3.1.4:
|
||||
resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==}
|
||||
engines: {node: ^18 || >=20}
|
||||
deprecated: unmaintained
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0
|
||||
@@ -7639,6 +7672,7 @@ packages:
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
validate-npm-package-license@3.0.4:
|
||||
@@ -11760,6 +11794,8 @@ snapshots:
|
||||
|
||||
dotenv@16.4.7: {}
|
||||
|
||||
dough-synth@0.2.4: {}
|
||||
|
||||
dset@3.1.4: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
|
||||
@@ -3422,6 +3422,30 @@ exports[`runs examples > example "djf" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "dough" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:F3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 0/1 → 1/1 | note:C4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 0/1 → 1/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 0/1 → 1/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 0/1 → 1/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 1/1 → 2/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 1/1 → 2/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 1/1 → 2/1 | note:G4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 1/1 → 2/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:D5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:F5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:C6 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:F4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "drawLine" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "drive" example index 0 1`] = `
|
||||
@@ -5930,6 +5954,30 @@ exports[`runs examples > example "inhabit" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "initDough" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:F3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 0/1 → 1/1 | note:C4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 0/1 → 1/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 0/1 → 1/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 0/1 → 1/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 1/1 → 2/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 1/1 → 2/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 1/1 → 2/1 | note:G4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 1/1 → 2/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:A4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:D5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:F5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 2/1 → 3/1 | note:C6 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:A3 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:D4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:E4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:F4 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
"[ 3/1 → 4/1 | note:C5 phaserrate:3 fmi:1.4 fmh:1.01 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "inside" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:D3 ]",
|
||||
|
||||
@@ -99,6 +99,7 @@ const toneHelpersMocked = {
|
||||
'_spectrum',
|
||||
'markcss',
|
||||
'p',
|
||||
'dough',
|
||||
].forEach((mock) => {
|
||||
strudel.Pattern.prototype[mock] = function () {
|
||||
return this;
|
||||
@@ -174,6 +175,7 @@ evalScope(
|
||||
getDuration,
|
||||
setcps: id,
|
||||
setcpm: id,
|
||||
initDough: id,
|
||||
Clock: {}, // whatever
|
||||
},
|
||||
);
|
||||
|
||||
+2
-2
@@ -597,8 +597,8 @@
|
||||
],
|
||||
"/packages/superdough/dspworklet.mjs": [
|
||||
"dspWorklet",
|
||||
"dough",
|
||||
"doughTrigger"
|
||||
"rawdsp",
|
||||
"rawdspTrigger"
|
||||
],
|
||||
"/packages/superdough/index.mjs": [],
|
||||
"/packages/tonal/tonleiter.mjs": [
|
||||
|
||||
@@ -145,4 +145,13 @@ export default defineConfig({
|
||||
// external: ['fraction.js'], // https://github.com/infusion/Fraction.js/issues/51
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// these are needed for dough, which uses shared memory
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
|
||||
// this is only for the dev server, so the server strudel runs on, also needs those
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'credentialless',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@strudel/motion": "workspace:*",
|
||||
"@strudel/mqtt": "workspace:*",
|
||||
"@strudel/osc": "workspace:*",
|
||||
"@strudel/dough": "workspace:*",
|
||||
"@strudel/serial": "workspace:*",
|
||||
"@strudel/soundfonts": "workspace:*",
|
||||
"@strudel/tidal": "workspace:*",
|
||||
|
||||
@@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
|
||||
<base href={BASE_URL} />
|
||||
|
||||
<!-- Scrollable a11y code helper -->
|
||||
<!-- Scrollable accessibility code helper -->
|
||||
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||
|
||||
<script src="/src/pwa.ts"></script>
|
||||
|
||||
@@ -23,7 +23,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
|
||||
<base href={BASE_URL} />
|
||||
|
||||
<!-- Scrollable a11y code helper -->
|
||||
<!-- Scrollable accessibility code helper -->
|
||||
<script src={`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||
|
||||
<script src="/src/pwa.ts"></script>
|
||||
|
||||
@@ -16,7 +16,7 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
|
||||
<!-- Page Metadata -->
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
<!-- OpenGraph Tags -->
|
||||
<!-- og Tags -->
|
||||
<meta property="og:title" content={formattedContentTitle} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
|
||||
@@ -9,9 +9,7 @@ import {
|
||||
userPattern,
|
||||
} from '../../../user_pattern_utils.mjs';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { getMetadata } from '../../../metadata_parser.js';
|
||||
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||
import { parseJSON, isUdels } from '../../util.mjs';
|
||||
import { useSettings } from '../../../settings.mjs';
|
||||
import { ActionButton } from '../button/action-button.jsx';
|
||||
import { Pagination } from '../pagination/Pagination.jsx';
|
||||
@@ -20,8 +18,13 @@ import { useDebounce } from '../usedebounce.jsx';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
|
||||
|
||||
const PATTERN_SORT = {
|
||||
"NEWEST": "most recent",
|
||||
"A-Z": "A-Z",
|
||||
}
|
||||
|
||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
const meta = pattern.meta
|
||||
|
||||
let title = meta.title;
|
||||
if (title == null) {
|
||||
@@ -58,6 +61,10 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
return (
|
||||
<div className="p-2">
|
||||
{Object.values(patterns)
|
||||
.sort((a, b) => {
|
||||
return (b.meta.title ?? "").localeCompare(a.meta.title ?? "")
|
||||
|
||||
})
|
||||
.reverse()
|
||||
.map((pattern) => {
|
||||
const id = pattern.id;
|
||||
@@ -93,7 +100,7 @@ export function PatternsTab({ context }) {
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(userPatterns).filter(([_key, pattern]) => {
|
||||
const meta = getMetadata(pattern.code);
|
||||
const meta = pattern.meta;
|
||||
|
||||
// Search for specific meta keys
|
||||
const searchLowercaseTrimmed = search.trim().toLowerCase();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { registerSampleSource } from '@strudel/webaudio';
|
||||
import { extractMidiNoteFromString, registerSampleSource } from '@strudel/webaudio';
|
||||
import { isAudioFile } from './files.mjs';
|
||||
import { logger } from '@strudel/core';
|
||||
|
||||
@@ -47,7 +47,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
||||
Promise.all(
|
||||
[...soundFiles]
|
||||
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
||||
.map((soundFile, i) => {
|
||||
.map((soundFile) => {
|
||||
const title = soundFile.title;
|
||||
if (!isAudioFile(title)) {
|
||||
return;
|
||||
@@ -58,25 +58,26 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||
const blob = soundFile.blob;
|
||||
|
||||
return blobToDataUrl(blob).then((soundPath) => {
|
||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
||||
return blobToDataUrl(blob).then((path) => {
|
||||
const sampleInfoMap = sounds.get(parentDirectory) ?? new Map();
|
||||
const midi = extractMidiNoteFromString(title);
|
||||
/** @type {import('@strudel/webaudio').SampleMetaData} */
|
||||
const samplemetadata = { url: path, midi };
|
||||
sampleInfoMap.set(title, samplemetadata);
|
||||
|
||||
titlePathMap.set(title, soundPath);
|
||||
|
||||
sounds.set(parentDirectory, titlePathMap);
|
||||
sounds.set(parentDirectory, sampleInfoMap);
|
||||
return;
|
||||
});
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
sounds.forEach((titlePathMap, key) => {
|
||||
const value = Array.from(titlePathMap.keys())
|
||||
sounds.forEach((sampleInfoMap, key) => {
|
||||
const bank = Array.from(sampleInfoMap.keys())
|
||||
.sort((a, b) => {
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map((title) => titlePathMap.get(title));
|
||||
|
||||
registerSampleSource(key, value, { prebake: false });
|
||||
.map((title) => sampleInfoMap.get(title));
|
||||
registerSampleSource(key, bank, { prebake: false });
|
||||
});
|
||||
|
||||
logger('imported sounds registered!', 'success');
|
||||
|
||||
@@ -85,6 +85,7 @@ export function loadModules() {
|
||||
import('@strudel/motion'),
|
||||
import('@strudel/mqtt'),
|
||||
import('@strudel/mondo'),
|
||||
import('@strudel/dough'),
|
||||
];
|
||||
if (isTauri()) {
|
||||
modules = modules.concat([
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { register } from '@strudel/core';
|
||||
import { isUdels } from './repl/util.mjs';
|
||||
import { computed } from 'nanostores';
|
||||
import { getMetadata } from './metadata_parser';
|
||||
|
||||
export const audioEngineTargets = {
|
||||
webaudio: 'webaudio',
|
||||
@@ -18,6 +19,10 @@ export const soundFilterType = {
|
||||
ALL: 'all',
|
||||
};
|
||||
|
||||
export const PATTERN_SORT = {
|
||||
"NEWEST": "most recent",
|
||||
"A-Z": "A-Z",
|
||||
}
|
||||
const initialPrebakeScript = `// Prebake script
|
||||
//
|
||||
// This is code that is loaded before your pattern is run.
|
||||
@@ -86,6 +91,7 @@ export const $settings = computed(settingsMap, (state) => {
|
||||
Object.keys(userPatterns).forEach((key) => {
|
||||
const data = userPatterns[key];
|
||||
data.id = data.id ?? key;
|
||||
data.meta = getMetadata(data.code)
|
||||
userPatterns[key] = data;
|
||||
});
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user