mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-19 21:45:10 -04:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2811ba35b | |||
| 61b6e96a4a | |||
| c33c845562 | |||
| 1df7e03668 | |||
| 276979f48a | |||
| 7e719c35f2 | |||
| a2c5af1673 | |||
| 2619144353 | |||
| e8a9974518 | |||
| a458dcf9fd | |||
| 8dbf5ce74c | |||
| 7f02baf93b | |||
| 2cf3ffa507 | |||
| 3583091e43 | |||
| 0ecf60f751 | |||
| 73c57f0f9d | |||
| cc8c9f1c26 | |||
| bcec4695a0 | |||
| e782debc15 | |||
| 459768af2a | |||
| be3422c5e6 | |||
| 382d5df933 | |||
| 13d404613c | |||
| f54a1f07d2 | |||
| 44e371c99d | |||
| c8d0342ecb | |||
| 096e53690f | |||
| e3e3cdfcfc |
@@ -0,0 +1,41 @@
|
||||
# Code of Conduct
|
||||
|
||||
## The Short Version
|
||||
|
||||
We are dedicated to providing a harassment-free experience for everyone, regardless of gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, ethnicity, religion (or lack thereof), technology choices, and so on. This applies both in person or via online media.
|
||||
|
||||
## The Less Short Version
|
||||
|
||||
Harassment includes offensive verbal comments related to gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, ethnicity, religion, technology choices, deliberate intimidation, stalking, following, disruption of performances, inappropriate physical contact, and unwelcome sexual attention.
|
||||
|
||||
If you are being harassed, notice that someone else is being harassed, or have any other concerns, please contact an admin immediately.
|
||||
|
||||
If you report an incident of harassment you can expect to be listened to and taken seriously. The admin will take action they deem appropriate in response depending on the scale and severity of the incident. For example:
|
||||
|
||||
1. Discuss the perpetrator’s behaviour with them and ask them to stop. Explain what they have done and why it is unacceptable. Remind the perpetrator of the code of conduct. Monitor the perpetrator’s behaviour and if it improves then they will continue to be welcome.
|
||||
2. Temporarily exclude the perpetrator from the forum for a cool-off period. Explain what they have done and why it is unacceptable and remind them of the code of conduct. Monitor the person’s behaviour and if it improves then they will continue to be welcome.
|
||||
3. Remove the perpetrator from the forum. Explain what they have done and why it is unacceptable and remind them of the code of conduct.
|
||||
|
||||
# Post-AI humane policy for live coding development
|
||||
|
||||
Please follow the following policy on AI/LLM use.
|
||||
|
||||
## Community
|
||||
|
||||
All community members should be treated with respect and kindness. We welcome contributors to the community who are ready to follow this policy as well as the above code of conduct.
|
||||
|
||||
## Principles
|
||||
|
||||
* We wish to protect the humanity of our communities and projects, to support the motivation and mental health of all of its contributors and users.
|
||||
* To this end, contributors and other users of our projects should not have to engage with LLM-generated code and text as part of their community work.
|
||||
* We recognise that LLM-based scraping and generation of (A)GPL code shared by ourselves and others is of questionable legality. For example under many juristictions, LLM-generated work does not have a copyright holder and can therefore not be licensed under the (A)GPL.
|
||||
* A contributor's personal use of LLMs and related technology is their own business, whether for research, analysis, bug discovery or other assistive technology, so long as the output is not included in any contributions to our projects.
|
||||
|
||||
## In practice
|
||||
|
||||
* Contributions including LLM-generated content will be declined.
|
||||
* LLM-generated text (e.g. commit messages, issue comments, forum posts etc) generated by LLMs will be flagged, and ultimately deleted.
|
||||
* We encourage asking each other questions and working through problems to promote collective knowledge and creative ideas (never telling each other to "just google it").
|
||||
* We strongly discourage others from forking/importing our projects into corporate AI platforms, including Microsoft Github.
|
||||
* All contributions must be submitted by a human who understands the changes, is happy to answer questions about them, and where possible, help maintain the code in the future.
|
||||
* Where stipulated, discussion (including criticism) of LLM technology and use should be kept to designated channels.
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
keymap,
|
||||
lineNumbers,
|
||||
} from '@codemirror/view';
|
||||
import {repeatCharKeymap} from './repeatcharacter.mjs';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { registerControl, repl } from '@strudel/core';
|
||||
import { cleanupDraw, Drawer } from '@strudel/draw';
|
||||
@@ -25,12 +24,7 @@ import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
||||
import {
|
||||
deleteAllInlineBeforeCharacter,
|
||||
InsertCharBeforeChar,
|
||||
jumpToCharacter,
|
||||
jumpToNextCharacter,
|
||||
} from './labelJump.mjs';
|
||||
import { jumpToCharacter } from './labelJump.mjs';
|
||||
|
||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||
|
||||
@@ -50,9 +44,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()]));
|
||||
@@ -80,10 +74,6 @@ 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 }) {
|
||||
const settings = codemirrorSettings.get();
|
||||
@@ -111,7 +101,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
EditorView.updateListener.of((v) => onChange(v)),
|
||||
drawSelection({ cursorBlinkRate: 0 }),
|
||||
repeatCharKeymap,
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{
|
||||
@@ -133,74 +122,12 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
},
|
||||
{
|
||||
key: 'Alt-w',
|
||||
run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
|
||||
run: (view) => jumpToCharacter(view, '$', 1),
|
||||
},
|
||||
{
|
||||
key: 'Alt-q',
|
||||
run: (view) => {
|
||||
return jumpToNextCharacter(view, ANON_LABEL, -1);
|
||||
},
|
||||
run: (view) => jumpToCharacter(view, '$', -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,54 +1,18 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { SearchCursor } from '@codemirror/search';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
export function jumpToCharacter(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;
|
||||
const characterPositions = getCharacterPositions(state, character);
|
||||
while (!cursor.next().done) {
|
||||
characterPositions.push(cursor.value.to);
|
||||
}
|
||||
if (!characterPositions.length) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
if (direction > 0) {
|
||||
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
||||
@@ -57,97 +21,11 @@ export function jumpToNextCharacter(view, character, direction = 1) {
|
||||
}
|
||||
|
||||
if (jumpPos == null) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
const selection = EditorSelection.cursor(jumpPos - 1);
|
||||
dispatch({
|
||||
selection,
|
||||
effects: EditorView.scrollIntoView(
|
||||
selection.head,
|
||||
{ y: "start" }
|
||||
)
|
||||
selection: EditorSelection.cursor(jumpPos - 1),
|
||||
scrollIntoView: 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;
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
])
|
||||
];
|
||||
+88
-23
@@ -28,31 +28,34 @@ export class Cyclist {
|
||||
this.started = false;
|
||||
this.beforeStart = beforeStart;
|
||||
this.cps = 0.5;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
this.checkpoint = {
|
||||
// new checkpoints are created when there's a change in cps
|
||||
cycles: 0, // number of cycles when cps was changed
|
||||
seconds: 0, // clock phase when cps was changed
|
||||
};
|
||||
this.num_ticks_since_checkpoint = 0;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.lastBegin = 0; // query begin of last tick
|
||||
this.lastEnd = 0; // query end of last tick
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.num_cycles_at_cps_change = 0;
|
||||
this.seconds_at_cps_change; // clock phase when cps was changed
|
||||
this.onToggle = onToggle;
|
||||
this.latency = latency; // fixed trigger time offset
|
||||
this.clock = createClock(
|
||||
getTime,
|
||||
// called slightly before each cycle
|
||||
(phase, duration, _, t) => {
|
||||
if (this.num_ticks_since_cps_change === 0) {
|
||||
this.num_cycles_at_cps_change = this.lastEnd;
|
||||
this.seconds_at_cps_change = phase;
|
||||
if (this.num_ticks_since_checkpoint === 0) {
|
||||
this.checkpoint = {
|
||||
cycles: this.lastEnd,
|
||||
seconds: phase,
|
||||
};
|
||||
}
|
||||
this.num_ticks_since_cps_change++;
|
||||
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
|
||||
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
|
||||
this.num_ticks_since_checkpoint++;
|
||||
|
||||
try {
|
||||
const begin = this.lastEnd;
|
||||
var begin = this.lastEnd;
|
||||
this.lastBegin = begin;
|
||||
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
|
||||
var end = this.checkpoint.cycles + this.num_ticks_since_checkpoint * duration * this.cps;
|
||||
this.lastEnd = end;
|
||||
this.lastTick = phase;
|
||||
|
||||
@@ -62,20 +65,79 @@ export class Cyclist {
|
||||
return;
|
||||
}
|
||||
|
||||
// query the pattern for events
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
|
||||
const tickEndTime = phase + duration;
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
const targetTime =
|
||||
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
|
||||
onSchedule?.(hap, targetTime, this.cps);
|
||||
var cpsChanged = false;
|
||||
do {
|
||||
cpsChanged = false;
|
||||
|
||||
// query the pattern for events
|
||||
const haps = this.pattern
|
||||
.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' })
|
||||
.filter((hap) => hap.hasOnset())
|
||||
// Sort the haps based on their onset, so cps changes within the
|
||||
// tick duration happen in the correct order.
|
||||
.sort((a, b) => {
|
||||
const aBegin = a.whole.begin;
|
||||
const bBegin = b.whole.begin;
|
||||
// If haps happen at the same time, trigger the haps that cause
|
||||
// cps changes first, so the durations of the simultaneous haps
|
||||
// are adjusted accordingly.
|
||||
if (aBegin.eq(bBegin)) {
|
||||
if (a.value.cps !== undefined) {
|
||||
return b.value.cps !== undefined ? 0 : -1;
|
||||
}
|
||||
if (b.value.cps !== undefined) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return aBegin.sub(bBegin);
|
||||
});
|
||||
|
||||
haps.forEach((hap) => {
|
||||
const targetTime = (hap.whole.begin - this.checkpoint.cycles) / this.cps + this.checkpoint.seconds;
|
||||
if (tickEndTime < targetTime) {
|
||||
// If the targetTime exceeds the tickEndTime, this means
|
||||
// the cps has slowed down, and the latter haps from the query
|
||||
// no longer fall within the window of the current tick.
|
||||
// They'll be scheduled during the next tick.
|
||||
return;
|
||||
}
|
||||
onSchedule?.(hap, targetTime + this.latency, hap.value.cps ?? this.cps);
|
||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||
cpsChanged = true;
|
||||
this.cps = hap.value.cps;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
this.num_ticks_since_checkpoint = 0;
|
||||
this.checkpoint = {
|
||||
cycles: hap.whole.begin,
|
||||
seconds: targetTime,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// If there are any haps in the query that trigger changes to the
|
||||
// cps that result in the pattern speeding up, the current cycle end
|
||||
// may not capture all the haps that need to be scheduled.
|
||||
// If this is the case, we repeat the query + scheduling with a
|
||||
// newly calculated end time.
|
||||
// This process can repeat multiple times until no additional
|
||||
// changes to the cps are triggered.
|
||||
// In practice, this doesn't happen more than once during a tick.
|
||||
if (cpsChanged) {
|
||||
begin = end;
|
||||
const remainingSecondsSinceCheckpoint = tickEndTime - this.checkpoint.seconds;
|
||||
const remainingCycles = remainingSecondsSinceCheckpoint * this.cps;
|
||||
end = this.checkpoint.cycles + remainingCycles;
|
||||
this.lastEnd = end;
|
||||
if (end < begin) {
|
||||
// If the new end is before the old end this means the track
|
||||
// slowed down, so we don't need to schedule additional haps.
|
||||
// Otherwise, schedule the remaining events.
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
} while (cpsChanged);
|
||||
} catch (e) {
|
||||
errorLogger(e);
|
||||
onError?.(e);
|
||||
@@ -101,8 +163,11 @@ export class Cyclist {
|
||||
}
|
||||
async start() {
|
||||
await this.beforeStart?.();
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
this.num_cycles_at_cps_change = 0;
|
||||
this.checkpoint = {
|
||||
cycles: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
this.num_ticks_since_checkpoint = 0;
|
||||
if (!this.pattern) {
|
||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||
}
|
||||
@@ -132,7 +197,7 @@ export class Cyclist {
|
||||
return;
|
||||
}
|
||||
this.cps = cps;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
this.num_ticks_since_checkpoint = 0;
|
||||
}
|
||||
log(begin, end, haps) {
|
||||
const onsets = haps.filter((h) => h.hasOnset());
|
||||
|
||||
@@ -420,7 +420,7 @@ export const binaryNL = (n, nBits = 16) => {
|
||||
* .partials(randL(8))
|
||||
*/
|
||||
export const randL = (n) => {
|
||||
return signal((t) => (nVal) => getRandsAtTime(t, nVal).map(Math.abs)).appLeft(reify(n));
|
||||
return signal((t, controls) => (nVal) => getRandsAtTime(t, nVal, controls.randSeed).map(Math.abs)).appLeft(reify(n));
|
||||
};
|
||||
|
||||
export const randrun = (n) => {
|
||||
@@ -1143,3 +1143,26 @@ export const perx = new Pattern(function (state) {
|
||||
const n = Fraction(1).div(state.span.duration);
|
||||
return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)];
|
||||
});
|
||||
|
||||
/**
|
||||
* A signal that goes from `0` to `1` over one cycle, and then stays at `1` forever.
|
||||
* Mainly useful in combination with other functions that reset the cycle. See also
|
||||
* `close`.
|
||||
* @tags temporal
|
||||
* @example
|
||||
* sound("bd sd [- bd] sd".fast(2))
|
||||
* .lpf(open.slow(4).range(300,3000)) // close envelope over 4 cycles
|
||||
* .restart("x/4") // restart every 4 cycles
|
||||
*/
|
||||
export const open = signal((t) => t.min(1));
|
||||
|
||||
/**
|
||||
* The same as `open`, but goes from `1` to `0` over one cycle, and then
|
||||
* stays on `0` forever.
|
||||
* @tags temporal
|
||||
* @example
|
||||
* sound("bd sd [- bd] sd".fast(2))
|
||||
* .lpf(close.slow(4).range(300,3000)) // close envelope over 4 cycles
|
||||
* .restart("x/4") // restart every 4 cycles
|
||||
*/
|
||||
export const close = signal((t) => Fraction(1).sub(t).max(0));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
|
||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||
import { registerSound, registerWaveTable, soundMap } from './index.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import {
|
||||
@@ -14,13 +14,6 @@ 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) {
|
||||
@@ -63,9 +56,8 @@ export const getDur = getDuration;
|
||||
|
||||
export function getSampleInfo(hapValue, bank) {
|
||||
const { speed = 1.0 } = hapValue;
|
||||
const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank);
|
||||
|
||||
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||
return { transpose, url, index, midi, label, playbackRate };
|
||||
}
|
||||
|
||||
@@ -188,12 +180,7 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
|
||||
if (baseUrl.startsWith('github:')) {
|
||||
baseUrl = githubPath(baseUrl, '');
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {string} v
|
||||
* @returns {SampleMetaData}
|
||||
*/
|
||||
const fullUrl = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
|
||||
const fullUrl = (v) => baseUrl + v;
|
||||
if (Array.isArray(value)) {
|
||||
//return [key, value.map(replaceUrl)];
|
||||
value = value.map(fullUrl);
|
||||
@@ -404,16 +391,3 @@ 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,12 +19,7 @@ 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) {
|
||||
@@ -34,11 +29,6 @@ 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;
|
||||
};
|
||||
@@ -48,17 +38,7 @@ export const freqToMidi = (freq) => {
|
||||
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} note
|
||||
* @param {number} defaultOctave
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
export const noteToFreq = (note, defaultOctave = 3) => {
|
||||
return midiToFreq(noteToMidi(note, defaultOctave));
|
||||
};
|
||||
function __valueToMidi(value) {
|
||||
export const valueToMidi = (value, fallbackValue) => {
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('valueToMidi: expected object value');
|
||||
}
|
||||
@@ -72,15 +52,10 @@ function __valueToMidi(value) {
|
||||
if (typeof note === 'number') {
|
||||
return note;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export const valueToMidi = (value, fallbackValue) => {
|
||||
const parsedValue = __valueToMidi(value) ?? fallbackValue;
|
||||
if (parsedValue == null) {
|
||||
if (!fallbackValue) {
|
||||
throw new Error('valueToMidi: expected freq or note to be set');
|
||||
}
|
||||
return parsedValue;
|
||||
return fallbackValue;
|
||||
};
|
||||
|
||||
export function nanFallback(value, fallback = 0, silent) {
|
||||
@@ -109,18 +84,15 @@ 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;
|
||||
const maybeMidiNote = __valueToMidi(hapValue);
|
||||
const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
|
||||
let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
let url;
|
||||
let index = 0;
|
||||
let samplemeta;
|
||||
if (Array.isArray(bank)) {
|
||||
index = getSoundIndex(n, bank.length);
|
||||
samplemeta = bank[index];
|
||||
url = bank[index];
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||
// object format will expect keys as notes
|
||||
@@ -132,14 +104,10 @@ export function getCommonSampleInfo(hapValue, bank) {
|
||||
);
|
||||
transpose = -midiDiff(closest); // semitones to repitch
|
||||
index = getSoundIndex(n, bank[closest].length);
|
||||
samplemeta = bank[closest][index];
|
||||
url = bank[closest][index];
|
||||
}
|
||||
const label = `${s}:${index}`;
|
||||
if (maybeMidiNote != null) {
|
||||
transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi);
|
||||
}
|
||||
|
||||
return { transpose, index, midi, label, url: samplemeta.url };
|
||||
return { transpose, url, index, midi, label };
|
||||
}
|
||||
|
||||
/** Selects entries from `source` and renames them via `map`
|
||||
|
||||
@@ -94,9 +94,6 @@ export function getWidgetID(widgetConfig) {
|
||||
|
||||
function widgetWithLocation(node, widgetConfig) {
|
||||
const id = getWidgetID(widgetConfig);
|
||||
// Store the unique ID back into the config so it's available for widget management
|
||||
// This is critical for block-based evaluation to match existing widgets with new ones
|
||||
widgetConfig.id = id;
|
||||
// add loc as identifier to first argument
|
||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||
node.arguments.unshift({
|
||||
|
||||
@@ -2440,6 +2440,43 @@ exports[`runs examples > example "clip" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "close" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd cutoff:3000 ]",
|
||||
"[ 1/8 → 1/4 | s:sd cutoff:2915.625 ]",
|
||||
"[ 5/16 → 3/8 | s:bd cutoff:2789.0625 ]",
|
||||
"[ 3/8 → 1/2 | s:sd cutoff:2746.875 ]",
|
||||
"[ 1/2 → 5/8 | s:bd cutoff:2662.5 ]",
|
||||
"[ 5/8 → 3/4 | s:sd cutoff:2578.125 ]",
|
||||
"[ 13/16 → 7/8 | s:bd cutoff:2451.5625 ]",
|
||||
"[ 7/8 → 1/1 | s:sd cutoff:2409.375 ]",
|
||||
"[ 1/1 → 9/8 | s:bd cutoff:2325 ]",
|
||||
"[ 9/8 → 5/4 | s:sd cutoff:2240.625 ]",
|
||||
"[ 21/16 → 11/8 | s:bd cutoff:2114.0625 ]",
|
||||
"[ 11/8 → 3/2 | s:sd cutoff:2071.875 ]",
|
||||
"[ 3/2 → 13/8 | s:bd cutoff:1987.5 ]",
|
||||
"[ 13/8 → 7/4 | s:sd cutoff:1903.125 ]",
|
||||
"[ 29/16 → 15/8 | s:bd cutoff:1776.5625 ]",
|
||||
"[ 15/8 → 2/1 | s:sd cutoff:1734.375 ]",
|
||||
"[ 2/1 → 17/8 | s:bd cutoff:1650 ]",
|
||||
"[ 17/8 → 9/4 | s:sd cutoff:1565.625 ]",
|
||||
"[ 37/16 → 19/8 | s:bd cutoff:1439.0625 ]",
|
||||
"[ 19/8 → 5/2 | s:sd cutoff:1396.875 ]",
|
||||
"[ 5/2 → 21/8 | s:bd cutoff:1312.5 ]",
|
||||
"[ 21/8 → 11/4 | s:sd cutoff:1228.125 ]",
|
||||
"[ 45/16 → 23/8 | s:bd cutoff:1101.5625 ]",
|
||||
"[ 23/8 → 3/1 | s:sd cutoff:1059.375 ]",
|
||||
"[ 3/1 → 25/8 | s:bd cutoff:975 ]",
|
||||
"[ 25/8 → 13/4 | s:sd cutoff:890.625 ]",
|
||||
"[ 53/16 → 27/8 | s:bd cutoff:764.0625 ]",
|
||||
"[ 27/8 → 7/2 | s:sd cutoff:721.875 ]",
|
||||
"[ 7/2 → 29/8 | s:bd cutoff:637.5 ]",
|
||||
"[ 29/8 → 15/4 | s:sd cutoff:553.125 ]",
|
||||
"[ 61/16 → 31/8 | s:bd cutoff:426.5625 ]",
|
||||
"[ 31/8 → 4/1 | s:sd cutoff:384.375 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "coarse" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:hh coarse:1 ]",
|
||||
@@ -8727,6 +8764,43 @@ exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "open" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:bd cutoff:300 ]",
|
||||
"[ 1/8 → 1/4 | s:sd cutoff:384.375 ]",
|
||||
"[ 5/16 → 3/8 | s:bd cutoff:510.9375 ]",
|
||||
"[ 3/8 → 1/2 | s:sd cutoff:553.125 ]",
|
||||
"[ 1/2 → 5/8 | s:bd cutoff:637.5 ]",
|
||||
"[ 5/8 → 3/4 | s:sd cutoff:721.875 ]",
|
||||
"[ 13/16 → 7/8 | s:bd cutoff:848.4375 ]",
|
||||
"[ 7/8 → 1/1 | s:sd cutoff:890.625 ]",
|
||||
"[ 1/1 → 9/8 | s:bd cutoff:975 ]",
|
||||
"[ 9/8 → 5/4 | s:sd cutoff:1059.375 ]",
|
||||
"[ 21/16 → 11/8 | s:bd cutoff:1185.9375 ]",
|
||||
"[ 11/8 → 3/2 | s:sd cutoff:1228.125 ]",
|
||||
"[ 3/2 → 13/8 | s:bd cutoff:1312.5 ]",
|
||||
"[ 13/8 → 7/4 | s:sd cutoff:1396.875 ]",
|
||||
"[ 29/16 → 15/8 | s:bd cutoff:1523.4375 ]",
|
||||
"[ 15/8 → 2/1 | s:sd cutoff:1565.625 ]",
|
||||
"[ 2/1 → 17/8 | s:bd cutoff:1650 ]",
|
||||
"[ 17/8 → 9/4 | s:sd cutoff:1734.375 ]",
|
||||
"[ 37/16 → 19/8 | s:bd cutoff:1860.9375 ]",
|
||||
"[ 19/8 → 5/2 | s:sd cutoff:1903.125 ]",
|
||||
"[ 5/2 → 21/8 | s:bd cutoff:1987.5 ]",
|
||||
"[ 21/8 → 11/4 | s:sd cutoff:2071.875 ]",
|
||||
"[ 45/16 → 23/8 | s:bd cutoff:2198.4375 ]",
|
||||
"[ 23/8 → 3/1 | s:sd cutoff:2240.625 ]",
|
||||
"[ 3/1 → 25/8 | s:bd cutoff:2325 ]",
|
||||
"[ 25/8 → 13/4 | s:sd cutoff:2409.375 ]",
|
||||
"[ 53/16 → 27/8 | s:bd cutoff:2535.9375 ]",
|
||||
"[ 27/8 → 7/2 | s:sd cutoff:2578.125 ]",
|
||||
"[ 7/2 → 29/8 | s:bd cutoff:2662.5 ]",
|
||||
"[ 29/8 → 15/4 | s:sd cutoff:2746.875 ]",
|
||||
"[ 61/16 → 31/8 | s:bd cutoff:2873.4375 ]",
|
||||
"[ 31/8 → 4/1 | s:sd cutoff:2915.625 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "orbit" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | s:hh delay:0.5 delaytime:0.25 orbit:1 ]",
|
||||
|
||||
@@ -80,6 +80,7 @@ export default defineConfig({
|
||||
/^https:\/\/freesound\.org\/.*/i,
|
||||
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
||||
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
||||
/^https:\/\/felixroos\.github\.io\/webaudiofontdata\/.*/i,
|
||||
].some((regex) => regex.test(url)),
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
|
||||
@@ -9,7 +9,9 @@ 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';
|
||||
@@ -18,13 +20,8 @@ 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 = pattern.meta
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
|
||||
let title = meta.title;
|
||||
if (title == null) {
|
||||
@@ -61,10 +58,6 @@ 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;
|
||||
@@ -100,7 +93,7 @@ export function PatternsTab({ context }) {
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(userPatterns).filter(([_key, pattern]) => {
|
||||
const meta = pattern.meta;
|
||||
const meta = getMetadata(pattern.code);
|
||||
|
||||
// Search for specific meta keys
|
||||
const searchLowercaseTrimmed = search.trim().toLowerCase();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extractMidiNoteFromString, registerSampleSource } from '@strudel/webaudio';
|
||||
import { 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) => {
|
||||
.map((soundFile, i) => {
|
||||
const title = soundFile.title;
|
||||
if (!isAudioFile(title)) {
|
||||
return;
|
||||
@@ -58,26 +58,25 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||
const blob = soundFile.blob;
|
||||
|
||||
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);
|
||||
return blobToDataUrl(blob).then((soundPath) => {
|
||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
||||
|
||||
sounds.set(parentDirectory, sampleInfoMap);
|
||||
titlePathMap.set(title, soundPath);
|
||||
|
||||
sounds.set(parentDirectory, titlePathMap);
|
||||
return;
|
||||
});
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
sounds.forEach((sampleInfoMap, key) => {
|
||||
const bank = Array.from(sampleInfoMap.keys())
|
||||
sounds.forEach((titlePathMap, key) => {
|
||||
const value = Array.from(titlePathMap.keys())
|
||||
.sort((a, b) => {
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map((title) => sampleInfoMap.get(title));
|
||||
registerSampleSource(key, bank, { prebake: false });
|
||||
.map((title) => titlePathMap.get(title));
|
||||
|
||||
registerSampleSource(key, value, { prebake: false });
|
||||
});
|
||||
|
||||
logger('imported sounds registered!', 'success');
|
||||
|
||||
@@ -3,7 +3,6 @@ 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',
|
||||
@@ -19,10 +18,6 @@ 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.
|
||||
@@ -90,7 +85,6 @@ 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