mirror of
https://codeberg.org/uzu/strudel
synced 2026-08-01 13:29:25 -04:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad7e301394 | |||
| 7acb9a3ac7 | |||
| 9665b233a1 | |||
| 6c67851909 | |||
| 109ea3093b | |||
| 8d991ad8ae | |||
| 0b69ce21c7 | |||
| 4f05674cb3 | |||
| feab3f5c86 | |||
| 90c05ec38a | |||
| 7e0eed87cb | |||
| 1a1197f67a | |||
| 3c5afb32f3 | |||
| 76cbd23859 | |||
| b436ae789c | |||
| 275731afc7 | |||
| 5cb2214d88 |
@@ -11,6 +11,7 @@ import {
|
|||||||
keymap,
|
keymap,
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
|
import {repeatCharKeymap} from './repeatcharacter.mjs';
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
import { logger, registerControl, repl } from '@strudel/core';
|
import { logger, registerControl, repl } from '@strudel/core';
|
||||||
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
|
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
|
||||||
@@ -20,11 +21,16 @@ import { evalBlock } from './block_utilities.mjs';
|
|||||||
import { flash, isFlashEnabled } from './flash.mjs';
|
import { flash, isFlashEnabled } from './flash.mjs';
|
||||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
import { keybindings } from './keybindings.mjs';
|
import { keybindings } from './keybindings.mjs';
|
||||||
import { jumpToCharacter } from './labelJump.mjs';
|
|
||||||
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
|
||||||
|
import {
|
||||||
|
deleteAllInlineBeforeCharacter,
|
||||||
|
InsertCharBeforeChar,
|
||||||
|
jumpToCharacter,
|
||||||
|
jumpToNextCharacter,
|
||||||
|
} from './labelJump.mjs';
|
||||||
|
|
||||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||||
|
|
||||||
@@ -44,9 +50,9 @@ export const extensions = {
|
|||||||
isMultiCursorEnabled: (on) =>
|
isMultiCursorEnabled: (on) =>
|
||||||
on
|
on
|
||||||
? [
|
? [
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
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,
|
decode: JSON.parse,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ANON_LABEL = '$';
|
||||||
|
const SOLO_LABEL = 'S';
|
||||||
|
const MUTE_LABEL = '_';
|
||||||
|
|
||||||
// https://codemirror.net/docs/guide/
|
// https://codemirror.net/docs/guide/
|
||||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
|
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
|
||||||
const settings = codemirrorSettings.get();
|
const settings = codemirrorSettings.get();
|
||||||
@@ -102,6 +112,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
drawSelection({ cursorBlinkRate: 0 }),
|
drawSelection({ cursorBlinkRate: 0 }),
|
||||||
|
repeatCharKeymap,
|
||||||
Prec.highest(
|
Prec.highest(
|
||||||
keymap.of([
|
keymap.of([
|
||||||
{
|
{
|
||||||
@@ -138,12 +149,74 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Alt-w',
|
key: 'Alt-w',
|
||||||
run: (view) => jumpToCharacter(view, '$', 1),
|
run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'Alt-q',
|
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-.',
|
key: 'Ctrl-Shift-.',
|
||||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||||
|
|||||||
@@ -1,18 +1,54 @@
|
|||||||
import { EditorSelection } from '@codemirror/state';
|
import { EditorSelection } from '@codemirror/state';
|
||||||
import { SearchCursor } from '@codemirror/search';
|
import { SearchCursor } from '@codemirror/search';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { syntaxTree } from '@codemirror/language';
|
||||||
|
|
||||||
export function jumpToCharacter(view, character, direction = 1) {
|
/**
|
||||||
const { state, dispatch } = view;
|
* gets all of the positions of a character in a document, excluding commented out lines
|
||||||
const pos = state.selection.main.head;
|
* @param { EditorState} state
|
||||||
|
* @param {String} character
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
function getCharacterPositions(state, character) {
|
||||||
const cursor = new SearchCursor(state.doc, character);
|
const cursor = new SearchCursor(state.doc, character);
|
||||||
|
|
||||||
let characterPositions = [];
|
const characterPositions = [];
|
||||||
let jumpPos;
|
|
||||||
while (!cursor.next().done) {
|
while (!cursor.next().done) {
|
||||||
characterPositions.push(cursor.value.to);
|
|
||||||
|
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;
|
||||||
|
let jumpPos;
|
||||||
|
const characterPositions = getCharacterPositions(state, character);
|
||||||
if (!characterPositions.length) {
|
if (!characterPositions.length) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
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) {
|
if (jumpPos == null) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
const selection = EditorSelection.cursor(jumpPos - 1);
|
||||||
dispatch({
|
dispatch({
|
||||||
selection: EditorSelection.cursor(jumpPos - 1),
|
selection,
|
||||||
scrollIntoView: true,
|
effects: EditorView.scrollIntoView(
|
||||||
|
selection.head,
|
||||||
|
{ y: "start" }
|
||||||
|
)
|
||||||
});
|
});
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
];
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +14,13 @@ import { logger } from './logger.mjs';
|
|||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @typedef {Object} SampleMetaData
|
||||||
|
* @property {string} url
|
||||||
|
* @property {midi} number
|
||||||
|
*/
|
||||||
|
|
||||||
export const getCachedBuffer = (url) => bufferCache[url];
|
export const getCachedBuffer = (url) => bufferCache[url];
|
||||||
|
|
||||||
function humanFileSize(bytes, si) {
|
function humanFileSize(bytes, si) {
|
||||||
@@ -30,10 +37,11 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSampleInfo(hapValue, bank) {
|
function getSampleInfo(hapValue, bank) {
|
||||||
const { speed = 1.0 } = hapValue;
|
const { speed = 1.0 } = hapValue;
|
||||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank);
|
||||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
|
||||||
|
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
return { transpose, url, index, midi, label, playbackRate };
|
return { transpose, url, index, midi, label, playbackRate };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +164,12 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
|
|||||||
if (baseUrl.startsWith('github:')) {
|
if (baseUrl.startsWith('github:')) {
|
||||||
baseUrl = githubPath(baseUrl, '');
|
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)) {
|
if (Array.isArray(value)) {
|
||||||
//return [key, value.map(replaceUrl)];
|
//return [key, value.map(replaceUrl)];
|
||||||
value = value.map(fullUrl);
|
value = value.map(fullUrl);
|
||||||
@@ -367,3 +380,16 @@ export function registerSampleSource(key, bank, params) {
|
|||||||
registerSample(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) => {
|
export const getAccidentalsOffset = (accidentals) => {
|
||||||
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
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) => {
|
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
@@ -29,6 +34,11 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
|||||||
const offset = getAccidentalsOffset(acc);
|
const offset = getAccidentalsOffset(acc);
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} n
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
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;
|
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') {
|
if (typeof value !== 'object') {
|
||||||
throw new Error('valueToMidi: expected object value');
|
throw new Error('valueToMidi: expected object value');
|
||||||
}
|
}
|
||||||
@@ -52,10 +72,15 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
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');
|
throw new Error('valueToMidi: expected freq or note to be set');
|
||||||
}
|
}
|
||||||
return fallbackValue;
|
return parsedValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function nanFallback(value, fallback = 0, silent) {
|
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
|
// deduces relevant info for sample loading from hap.value and sample definition
|
||||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
// 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)
|
// 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) {
|
export function getCommonSampleInfo(hapValue, bank) {
|
||||||
const { s, n = 0 } = hapValue;
|
const { s, n = 0 } = hapValue;
|
||||||
let midi = valueToMidi(hapValue, 36);
|
const maybeMidiNote = __valueToMidi(hapValue);
|
||||||
let transpose = midi - 36; // C3 is middle C;
|
const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
|
||||||
let url;
|
let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
let samplemeta;
|
||||||
if (Array.isArray(bank)) {
|
if (Array.isArray(bank)) {
|
||||||
index = getSoundIndex(n, bank.length);
|
index = getSoundIndex(n, bank.length);
|
||||||
url = bank[index];
|
samplemeta = bank[index];
|
||||||
} else {
|
} else {
|
||||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||||
// object format will expect keys as notes
|
// object format will expect keys as notes
|
||||||
@@ -104,10 +132,14 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||||||
);
|
);
|
||||||
transpose = -midiDiff(closest); // semitones to repitch
|
transpose = -midiDiff(closest); // semitones to repitch
|
||||||
index = getSoundIndex(n, bank[closest].length);
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
url = bank[closest][index];
|
samplemeta = bank[closest][index];
|
||||||
}
|
}
|
||||||
const label = `${s}:${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`
|
/** Selects entries from `source` and renames them via `map`
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import {
|
|||||||
userPattern,
|
userPattern,
|
||||||
} from '../../../user_pattern_utils.mjs';
|
} from '../../../user_pattern_utils.mjs';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { getMetadata } from '../../../metadata_parser.js';
|
|
||||||
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||||
import { parseJSON, isUdels } from '../../util.mjs';
|
|
||||||
import { useSettings } from '../../../settings.mjs';
|
import { useSettings } from '../../../settings.mjs';
|
||||||
import { ActionButton } from '../button/action-button.jsx';
|
import { ActionButton } from '../button/action-button.jsx';
|
||||||
import { Pagination } from '../pagination/Pagination.jsx';
|
import { Pagination } from '../pagination/Pagination.jsx';
|
||||||
@@ -20,8 +18,13 @@ import { useDebounce } from '../usedebounce.jsx';
|
|||||||
import cx from '@src/cx.mjs';
|
import cx from '@src/cx.mjs';
|
||||||
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
|
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'> } */) {
|
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
const meta = pattern.meta
|
||||||
|
|
||||||
let title = meta.title;
|
let title = meta.title;
|
||||||
if (title == null) {
|
if (title == null) {
|
||||||
@@ -58,6 +61,10 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
|||||||
return (
|
return (
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{Object.values(patterns)
|
{Object.values(patterns)
|
||||||
|
.sort((a, b) => {
|
||||||
|
return (b.meta.title ?? "").localeCompare(a.meta.title ?? "")
|
||||||
|
|
||||||
|
})
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((pattern) => {
|
.map((pattern) => {
|
||||||
const id = pattern.id;
|
const id = pattern.id;
|
||||||
@@ -93,7 +100,7 @@ export function PatternsTab({ context }) {
|
|||||||
}
|
}
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(userPatterns).filter(([_key, pattern]) => {
|
Object.entries(userPatterns).filter(([_key, pattern]) => {
|
||||||
const meta = getMetadata(pattern.code);
|
const meta = pattern.meta;
|
||||||
|
|
||||||
// Search for specific meta keys
|
// Search for specific meta keys
|
||||||
const searchLowercaseTrimmed = search.trim().toLowerCase();
|
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 { isAudioFile } from './files.mjs';
|
||||||
import { logger } from '@strudel/core';
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
Promise.all(
|
Promise.all(
|
||||||
[...soundFiles]
|
[...soundFiles]
|
||||||
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
||||||
.map((soundFile, i) => {
|
.map((soundFile) => {
|
||||||
const title = soundFile.title;
|
const title = soundFile.title;
|
||||||
if (!isAudioFile(title)) {
|
if (!isAudioFile(title)) {
|
||||||
return;
|
return;
|
||||||
@@ -58,25 +58,26 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||||
const blob = soundFile.blob;
|
const blob = soundFile.blob;
|
||||||
|
|
||||||
return blobToDataUrl(blob).then((soundPath) => {
|
return blobToDataUrl(blob).then((path) => {
|
||||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
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, sampleInfoMap);
|
||||||
|
|
||||||
sounds.set(parentDirectory, titlePathMap);
|
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
sounds.forEach((titlePathMap, key) => {
|
sounds.forEach((sampleInfoMap, key) => {
|
||||||
const value = Array.from(titlePathMap.keys())
|
const bank = Array.from(sampleInfoMap.keys())
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
})
|
})
|
||||||
.map((title) => titlePathMap.get(title));
|
.map((title) => sampleInfoMap.get(title));
|
||||||
|
registerSampleSource(key, bank, { prebake: false });
|
||||||
registerSampleSource(key, value, { prebake: false });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger('imported sounds registered!', 'success');
|
logger('imported sounds registered!', 'success');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { register } from '@strudel/core';
|
import { register } from '@strudel/core';
|
||||||
import { isUdels } from './repl/util.mjs';
|
import { isUdels } from './repl/util.mjs';
|
||||||
import { computed } from 'nanostores';
|
import { computed } from 'nanostores';
|
||||||
|
import { getMetadata } from './metadata_parser';
|
||||||
|
|
||||||
export const audioEngineTargets = {
|
export const audioEngineTargets = {
|
||||||
webaudio: 'webaudio',
|
webaudio: 'webaudio',
|
||||||
@@ -18,6 +19,11 @@ export const soundFilterType = {
|
|||||||
ALL: 'all',
|
ALL: 'all',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PATTERN_SORT = {
|
||||||
|
"NEWEST": "most recent",
|
||||||
|
"A-Z": "A-Z",
|
||||||
|
}
|
||||||
|
|
||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
activeFooter: 'intro',
|
activeFooter: 'intro',
|
||||||
keybindings: 'codemirror',
|
keybindings: 'codemirror',
|
||||||
@@ -71,6 +77,7 @@ export const $settings = computed(settingsMap, (state) => {
|
|||||||
Object.keys(userPatterns).forEach((key) => {
|
Object.keys(userPatterns).forEach((key) => {
|
||||||
const data = userPatterns[key];
|
const data = userPatterns[key];
|
||||||
data.id = data.id ?? key;
|
data.id = data.id ?? key;
|
||||||
|
data.meta = getMetadata(data.code)
|
||||||
userPatterns[key] = data;
|
userPatterns[key] = data;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user