mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 06:19:33 -04:00
Merge branch 'main' into glossing/perf-lfos
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# changelog
|
||||
|
||||
NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted...
|
||||
|
||||
## november 2025
|
||||
|
||||
- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783)
|
||||
- 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754)
|
||||
- 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784)
|
||||
- 2025-11-27T20:36:14+01:00 Use errorLogger for query and tonal errors by glossing in: [#1782](https://codeberg.org/uzu/strudel/pulls/1782)
|
||||
- 2025-11-27T20:27:10+01:00 [perf] level 5 `connect-leak` on `vowel` by jeromew in: [#1779](https://codeberg.org/uzu/strudel/pulls/1779)
|
||||
- 2025-11-26T08:23:38+01:00 feat: add prebake script import button for loading .strudel files by daslyfe in: [#1774](https://codeberg.org/uzu/strudel/pulls/1774)
|
||||
- 2025-11-26T07:39:28+01:00 Fix Sampler port trampling by Dayglo in: [#1717](https://codeberg.org/uzu/strudel/pulls/1717)
|
||||
- 2025-11-25T20:59:09+01:00 Feat: Hook up octave and fix example by glossing in: [#1773](https://codeberg.org/uzu/strudel/pulls/1773)
|
||||
- 2025-11-25T20:48:09+01:00 [perf] fix `connect-leak` in `tremolo` param by jeromew in: [#1780](https://codeberg.org/uzu/strudel/pulls/1780)
|
||||
- 2025-11-24T17:51:20+01:00 filter modulation improvements! by daslyfe in: [#1769](https://codeberg.org/uzu/strudel/pulls/1769)
|
||||
- 2025-11-24T01:20:53+01:00 Add o as a synonym for orbit by daslyfe in: [#1766](https://codeberg.org/uzu/strudel/pulls/1766)
|
||||
- 2025-11-23T22:23:26+01:00 [perf] fix `connect-leak` in `fm` modulation by jeromew in: [#1758](https://codeberg.org/uzu/strudel/pulls/1758)
|
||||
- 2025-11-23T09:47:24+01:00 Fix interoperability issue between `all` and `await initHydra()` by jeromew in: [#1663](https://codeberg.org/uzu/strudel/pulls/1663)
|
||||
- 2025-11-23T04:23:06+01:00 Bug fix: Swap l/r gains with temp variable by glossing in: [#1768](https://codeberg.org/uzu/strudel/pulls/1768)
|
||||
- 2025-11-23T00:52:28+01:00 FIX: prevent LFO filter modulation from popping from negative values by daslyfe in: [#1767](https://codeberg.org/uzu/strudel/pulls/1767)
|
||||
- 2025-11-21T01:56:11+01:00 Bug Fix: Use frac due to negative frequencies from FM by glossing in: [#1759](https://codeberg.org/uzu/strudel/pulls/1759)
|
||||
- 2025-11-20T19:48:16+01:00 [perf] fix `connect-leak` added by #1742 when noise() is not used by jeromew in: [#1757](https://codeberg.org/uzu/strudel/pulls/1757)
|
||||
- 2025-11-20T14:51:00+01:00 [perf] fix `connect-leak` in `delay` effect by jeromew in: [#1755](https://codeberg.org/uzu/strudel/pulls/1755)
|
||||
- 2025-11-20T14:49:41+01:00 [perf] fix `connect leak` when .noise() is in the mix by jeromew in: [#1742](https://codeberg.org/uzu/strudel/pulls/1742)
|
||||
- 2025-11-18T23:52:45+01:00 wchooseCycles has now notes in an example by scrappy_fiddler in: [#1748](https://codeberg.org/uzu/strudel/pulls/1748)
|
||||
- 2025-11-17T05:31:54+01:00 Feature: Partials by glossing in: [#1659](https://codeberg.org/uzu/strudel/pulls/1659)
|
||||
- 2025-11-16T22:06:14+01:00 README: update superdough documentation by TristanMlct in: [#1741](https://codeberg.org/uzu/strudel/pulls/1741)
|
||||
- 2025-11-16T21:08:54+01:00 Worklet optimizations by glossing in: [#1730](https://codeberg.org/uzu/strudel/pulls/1730)
|
||||
- 2025-11-15T14:59:30+01:00 [perf] disconnect Offline AudioNode connections in generateReverb by jeromew in: [#1740](https://codeberg.org/uzu/strudel/pulls/1740)
|
||||
- 2025-11-15T14:57:47+01:00 [perf] disconnect `send` effect AudioNode when `room` is used by jeromew in: [#1736](https://codeberg.org/uzu/strudel/pulls/1736)
|
||||
- 2025-11-12T21:22:34+01:00 [supradough] fix: unstable filter by froos in: [#1593](https://codeberg.org/uzu/strudel/pulls/1593)
|
||||
- 2025-11-12T21:12:07+01:00 Fix link syntax in `project-start` by Kissaki in: [#1724](https://codeberg.org/uzu/strudel/pulls/1724)
|
||||
- 2025-11-12T21:06:13+01:00 Fix web README sample code by Kissaki in: [#1725](https://codeberg.org/uzu/strudel/pulls/1725)
|
||||
- 2025-11-12T20:59:39+01:00 Fix typo: 'studel' -> 'strudel' by drhayes in: [#1726](https://codeberg.org/uzu/strudel/pulls/1726)
|
||||
- 2025-11-12T15:23:41+01:00 fix for node 24 support in tests - #1718 by ausav in: [#1719](https://codeberg.org/uzu/strudel/pulls/1719)
|
||||
- 2025-11-11T09:00:49+01:00 soundAlias example fix by PepsiiMan in: [#1720](https://codeberg.org/uzu/strudel/pulls/1720)
|
||||
- 2025-11-10T08:18:36+01:00 fix: Make Supradough package build by munshkr in: [#1711](https://codeberg.org/uzu/strudel/pulls/1711)
|
||||
- 2025-11-10T08:18:00+01:00 added docs for spaces in scale names by ondras in: [#1715](https://codeberg.org/uzu/strudel/pulls/1715)
|
||||
- 2025-11-10T02:44:01+01:00 Feature: LFOs for Filters by glossing in: [#1636](https://codeberg.org/uzu/strudel/pulls/1636)
|
||||
- 2025-11-05T17:07:00+01:00 improvement: sync midi messages to audio context clock by daslyfe in: [#1708](https://codeberg.org/uzu/strudel/pulls/1708)
|
||||
- 2025-11-04T19:36:14+01:00 Add stick button mappings to gamepad implementation and improve docs by kaosuryoko in: [#1696](https://codeberg.org/uzu/strudel/pulls/1696)
|
||||
- 2025-11-04T19:29:35+01:00 Refactor sound stopping and triggering logic in SoundsTab component by IJOL in: [#1688](https://codeberg.org/uzu/strudel/pulls/1688)
|
||||
- 2025-11-04T19:13:21+01:00 Add setting to toggle pattern auto-start on pattern change by moumar in: [#1690](https://codeberg.org/uzu/strudel/pulls/1690)
|
||||
- 2025-11-04T18:58:50+01:00 Voicings JSDoc by sharkeys_lunchbox in: [#1686](https://codeberg.org/uzu/strudel/pulls/1686)
|
||||
- 2025-11-01T07:50:40+01:00 FIX: Loading local samples uses too much memory by daslyfe in: [#1706](https://codeberg.org/uzu/strudel/pulls/1706)
|
||||
|
||||
|
||||
## october 2025
|
||||
|
||||
- 2025-10-28T22:58:24+01:00 Bug Fix: Handle scale-for-notes when n also supplied by glossing in: [#1625](https://codeberg.org/uzu/strudel/pulls/1625)
|
||||
- 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624)
|
||||
- 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630)
|
||||
- 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701)
|
||||
- 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697)
|
||||
- 2025-10-26T17:09:22+01:00 Fix ZZFX example by moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685)
|
||||
- 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682)
|
||||
- 2025-10-23T03:47:31+02:00 Adds back shape to superdough by glossing in: [#1672](https://codeberg.org/uzu/strudel/pulls/1672)
|
||||
- 2025-10-22T22:15:19+02:00 Fix sampler.mjs githubPath by jeromew in: [#1684](https://codeberg.org/uzu/strudel/pulls/1684)
|
||||
- 2025-10-22T16:20:50+02:00 Fix onPaint for widgets by yaxu in: [#1658](https://codeberg.org/uzu/strudel/pulls/1658)
|
||||
- 2025-10-22T15:19:12+02:00 Fix a bug introduced by #4e17cfbdd6 by milliganf in: [#1679](https://codeberg.org/uzu/strudel/pulls/1679)
|
||||
- 2025-10-18T04:29:20+02:00 Bug Fix: Wavetable: phase wrapping at 1 and detune by glossing in: [#1620](https://codeberg.org/uzu/strudel/pulls/1620)
|
||||
- 2025-10-16T20:26:28+02:00 github samples: default to "samples" if repository is not specified by prezmop in: [#1644](https://codeberg.org/uzu/strudel/pulls/1644)
|
||||
- 2025-10-16T17:33:39+02:00 Docs: add example of custom chained function by dariusk in: [#1642](https://codeberg.org/uzu/strudel/pulls/1642)
|
||||
- 2025-10-14T12:39:48+02:00 textbox by yaxu in: [#1650](https://codeberg.org/uzu/strudel/pulls/1650)
|
||||
- 2025-10-13T07:05:57+02:00 Feature: Wavetable FM by glossing in: [#1623](https://codeberg.org/uzu/strudel/pulls/1623)
|
||||
- 2025-10-12T20:43:42+02:00 fix: repair REPL sample sources and URL concat (#1640) by erikfox in: [#1646](https://codeberg.org/uzu/strudel/pulls/1646)
|
||||
- 2025-10-10T11:25:02+02:00 Add control-metadata by yaxu in: [#1634](https://codeberg.org/uzu/strudel/pulls/1634)
|
||||
- 2025-10-07T22:59:39+02:00 Fix MQTT: change the trigger handler to match new hap by jurrchen in: [#1629](https://codeberg.org/uzu/strudel/pulls/1629)
|
||||
- 2025-10-05T21:42:19+02:00 Fix case sensitivity for synonym search by vvolhejn in: [#1618](https://codeberg.org/uzu/strudel/pulls/1618)
|
||||
- 2025-10-01T08:56:35+02:00 Optimize wavetable synth by glossing in: [#1613](https://codeberg.org/uzu/strudel/pulls/1613)
|
||||
- 2025-10-01T02:02:33+02:00 fix: pattern switching by daslyfe in: [#1616](https://codeberg.org/uzu/strudel/pulls/1616)
|
||||
|
||||
## September 2025
|
||||
|
||||
- 2025-09-29T02:10:54+02:00 fix wavetable JSON by daslyfe in: [#1611](https://codeberg.org/uzu/strudel/pulls/1611)
|
||||
- 2025-09-28T21:28:16+02:00 rename wavetable controls to match existing naming conventions by daslyfe in: [#1610](https://codeberg.org/uzu/strudel/pulls/1610)
|
||||
- 2025-09-27T23:28:13+02:00 Feature: Wavetable synth improvements by glossing in: [#1607](https://codeberg.org/uzu/strudel/pulls/1607)
|
||||
- 2025-09-27T21:43:10+02:00 wavetable improvements by daslyfe in: [#1608](https://codeberg.org/uzu/strudel/pulls/1608)
|
||||
- 2025-09-26T08:48:46+02:00 create orbit based DJ filter by daslyfe in: [#1603](https://codeberg.org/uzu/strudel/pulls/1603)
|
||||
- 2025-09-23T13:28:20+02:00 fix: time signal by daslyfe in: [#1583](https://codeberg.org/uzu/strudel/pulls/1583)
|
||||
- 2025-09-21T16:16:06+02:00 mondo: fix all + setcpm + setcps by froos in: [#1600](https://codeberg.org/uzu/strudel/pulls/1600)
|
||||
- 2025-09-19T21:59:43+02:00 fix: osc error message by froos in: [#1597](https://codeberg.org/uzu/strudel/pulls/1597)
|
||||
- 2025-09-18T21:09:56+02:00 osc: --debug flag to see incoming messages by froos in: [#1595](https://codeberg.org/uzu/strudel/pulls/1595)
|
||||
- 2025-09-17T14:12:10+02:00 simplify osc usage by froos in: [#1588](https://codeberg.org/uzu/strudel/pulls/1588)
|
||||
- 2025-09-16T16:59:58+02:00 dev: logger errors to console in dev mode by daslyfe in: [#1585](https://codeberg.org/uzu/strudel/pulls/1585)
|
||||
- 2025-09-15T22:52:14+02:00 add tic80 font by froos in: [#1579](https://codeberg.org/uzu/strudel/pulls/1579)
|
||||
- 2025-09-15T00:00:19+02:00 supradough: fix delay + add some jsdoc types by froos in: [#1578](https://codeberg.org/uzu/strudel/pulls/1578)
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
stringifyValues,
|
||||
} from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
|
||||
let stringParser;
|
||||
|
||||
@@ -414,7 +414,7 @@ export class Pattern {
|
||||
try {
|
||||
return this.query(new State(new TimeSpan(begin, end), controls));
|
||||
} catch (err) {
|
||||
logger(`[query]: ${err.message}`, 'error');
|
||||
errorLogger(err, 'query');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,8 +244,7 @@ export function repl({
|
||||
}
|
||||
|
||||
if (!isPattern(pattern)) {
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
pattern = silence;
|
||||
}
|
||||
logger(`[eval] code updated`);
|
||||
pattern = await setPattern(pattern, autostart);
|
||||
|
||||
@@ -35,6 +35,7 @@ export async function initHydra(options = {}) {
|
||||
hydra.synth.s0.init({ src: canvas });
|
||||
}
|
||||
}
|
||||
return hydra;
|
||||
}
|
||||
|
||||
export function clearHydra() {
|
||||
|
||||
@@ -9,6 +9,7 @@ import readline from 'readline';
|
||||
import os from 'os';
|
||||
|
||||
const LOG = !!process.env.LOG || false;
|
||||
const PORT = process.env.PORT || 5432;
|
||||
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
|
||||
|
||||
const isAudioFile = (f) => {
|
||||
@@ -54,7 +55,6 @@ async function getBanks(directory, flat = false) {
|
||||
banks[bank].push(subDir);
|
||||
return subDir;
|
||||
});
|
||||
banks._base = `http://localhost:5432`;
|
||||
return { banks, files };
|
||||
}
|
||||
|
||||
@@ -134,8 +134,6 @@ const server = http.createServer(async (req, res) => {
|
||||
readStream.pipe(res);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
const PORT = process.env.PORT || 5432;
|
||||
const IP_ADDRESS = '0.0.0.0';
|
||||
let IP;
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getCommonSampleInfo } from './util.mjs';
|
||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||
import { registerSound, registerWaveTable } from './index.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
@@ -211,7 +211,7 @@ export async function fetchSampleMap(url) {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = url.split('/').slice(0, -1).join('/');
|
||||
const base = getBaseURL(url);
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
|
||||
@@ -263,8 +263,8 @@ let audioReady;
|
||||
export async function initAudioOnFirstClick(options) {
|
||||
if (!audioReady) {
|
||||
audioReady = new Promise((resolve) => {
|
||||
document.addEventListener('click', async function listener() {
|
||||
document.removeEventListener('click', listener);
|
||||
document.addEventListener('mousedown', async function listener() {
|
||||
document.removeEventListener('mousedown', listener);
|
||||
await initAudio(options);
|
||||
resolve();
|
||||
});
|
||||
|
||||
@@ -493,7 +493,7 @@ export function getOscillator(s, t, value, onended) {
|
||||
}
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
noiseMix || o.disconnect();
|
||||
noiseMix?.node.disconnect();
|
||||
onended();
|
||||
};
|
||||
|
||||
@@ -114,3 +114,13 @@ export function getCommonSampleInfo(hapValue, bank) {
|
||||
export const pickAndRename = (source, map) => {
|
||||
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
||||
};
|
||||
|
||||
export const getBaseURL = (url) => {
|
||||
try {
|
||||
// For real URLs
|
||||
return new URL('.', new URL(url)).href.replace(/\/$/, ''); // removes trailing slash
|
||||
} catch {
|
||||
// For pseudo URLS
|
||||
return url.split('/').slice(0, -1).join('/');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,7 +45,8 @@ if (typeof GainNode !== 'undefined') {
|
||||
throw new Error('vowel: unknown vowel ' + letter);
|
||||
}
|
||||
const { gains, qs, freqs } = vowelFormant[letter];
|
||||
const makeupGain = ac.createGain();
|
||||
this.makeupGain = ac.createGain();
|
||||
this.audioNodes = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const gain = ac.createGain();
|
||||
gain.gain.value = gains[i];
|
||||
@@ -53,14 +54,25 @@ if (typeof GainNode !== 'undefined') {
|
||||
filter.type = 'bandpass';
|
||||
filter.Q.value = qs[i];
|
||||
filter.frequency.value = freqs[i];
|
||||
this.connect(filter);
|
||||
super.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(makeupGain);
|
||||
this.audioNodes.push(filter);
|
||||
gain.connect(this.makeupGain);
|
||||
this.audioNodes.push(gain);
|
||||
}
|
||||
makeupGain.gain.value = 8; // how much makeup gain to add?
|
||||
this.connect = (target) => makeupGain.connect(target);
|
||||
this.makeupGain.gain.value = 8; // how much makeup gain to add?
|
||||
return this;
|
||||
}
|
||||
connect(target) {
|
||||
this.makeupGain.connect(target);
|
||||
}
|
||||
disconnect() {
|
||||
this.makeupGain.disconnect();
|
||||
this.audioNodes.forEach((n) => n.disconnect());
|
||||
super.disconnect();
|
||||
this.makeupGain = null;
|
||||
this.audioNodes = null;
|
||||
}
|
||||
}
|
||||
|
||||
AudioContext.prototype.createVowelFilter = function (letter) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getCommonSampleInfo } from './util.mjs';
|
||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
applyParameterModulators,
|
||||
@@ -190,6 +190,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
||||
if (url.startsWith('local:')) {
|
||||
url = `http://localhost:5432`;
|
||||
}
|
||||
const base = getBaseURL(url);
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
@@ -200,7 +201,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
||||
}
|
||||
return fetch(url)
|
||||
.then((res) => res.json())
|
||||
.then((json) => _processTables(json, url, frameLen, options))
|
||||
.then((json) => _processTables(json, base, frameLen, options))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${url}"`);
|
||||
|
||||
@@ -5,7 +5,16 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
||||
import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core';
|
||||
import {
|
||||
_mod,
|
||||
errorLogger,
|
||||
getAccidentalsOffset,
|
||||
isNote,
|
||||
logger,
|
||||
noteToMidi,
|
||||
register,
|
||||
removeUndefineds,
|
||||
} from '@strudel/core';
|
||||
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
||||
|
||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
||||
@@ -294,7 +303,7 @@ export const scale = register(
|
||||
}
|
||||
if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset));
|
||||
} catch (err) {
|
||||
logger(`[tonal] ${err.message}`, 'error');
|
||||
errorLogger(err, 'tonal');
|
||||
return; // will be removed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "dbpatch",
|
||||
"dependencies": {
|
||||
"csv": "^6.3.11"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fetch('https://codeberg.org/api/v1/repos/uzu/strudel/pulls?state=closed&page=1')
|
||||
.then((res) => res.json())
|
||||
.then((pulls) => {
|
||||
const r = pulls
|
||||
.filter((pull) => pull.merged)
|
||||
.sort((a, b) => new Date(b.closed_at) - new Date(a.closed_at))
|
||||
.map((pull) => `${pull.closed_at} ${pull.title} by ${pull.user.login || '?'} in: [#${pull.number}](${pull.url}) `)
|
||||
.join('\n');
|
||||
console.log(r);
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
@@ -8,3 +8,34 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpecialActionButton(props) {
|
||||
const { className, ...buttonProps } = props;
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
{...buttonProps}
|
||||
className={cx('bg-background p-2 max-w-[300px] rounded-md hover:opacity-50', className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActionInput({ label, className, ...props }) {
|
||||
return (
|
||||
<label className={cx('inline-flex items-center cursor-pointer', className)}>
|
||||
<input {...props} className="sr-only peer" />
|
||||
|
||||
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpecialActionInput({ className, ...props }) {
|
||||
return (
|
||||
<ActionInput
|
||||
{...props}
|
||||
className={className}
|
||||
label={<span className="bg-background p-2 max-w-[300px] rounded-md">{props.label}</span>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { errorLogger } from '@strudel/core';
|
||||
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
|
||||
import { SpecialActionInput } from '../button/action-button';
|
||||
|
||||
async function importScript(script) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(script);
|
||||
|
||||
reader.onload = () => {
|
||||
const text = reader.result;
|
||||
storePrebakeScript(text);
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
errorLogger(new Error('failed to import prebake script'), 'importScript');
|
||||
};
|
||||
}
|
||||
export function ImportPrebakeScriptButton() {
|
||||
const settings = useSettings();
|
||||
|
||||
return (
|
||||
<SpecialActionInput
|
||||
type="file"
|
||||
label="import prebake script"
|
||||
accept=".strudel"
|
||||
onChange={(e) => importScript(e.target.files[0])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||
import { confirmDialog } from '../../util.mjs';
|
||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
||||
import { ActionButton, SpecialActionButton } from '../button/action-button.jsx';
|
||||
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||
|
||||
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||
return (
|
||||
@@ -113,6 +115,7 @@ export function SettingsTab({ started }) {
|
||||
isTabIndentationEnabled,
|
||||
isMultiCursorEnabled,
|
||||
patternAutoStart,
|
||||
includePrebakeScriptInShare,
|
||||
} = useSettings();
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
@@ -204,6 +207,15 @@ export function SettingsTab({ started }) {
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
<FormItem label="Prebake">
|
||||
<ImportPrebakeScriptButton />
|
||||
<Checkbox
|
||||
label="Include prebake script in share"
|
||||
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
||||
value={includePrebakeScriptInShare}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="Keybindings">
|
||||
<ButtonGroup
|
||||
value={keybindings}
|
||||
@@ -313,8 +325,7 @@ export function SettingsTab({ started }) {
|
||||
</FormItem>
|
||||
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
||||
<FormItem label="Reset Settings">
|
||||
<button
|
||||
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"
|
||||
<SpecialActionButton
|
||||
onClick={() => {
|
||||
confirmDialog('Sure?').then((r) => {
|
||||
if (r) {
|
||||
@@ -325,7 +336,7 @@ export function SettingsTab({ started }) {
|
||||
}}
|
||||
>
|
||||
restore default settings
|
||||
</button>
|
||||
</SpecialActionButton>
|
||||
</FormItem>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,10 @@ import { prebake } from '@src/repl/prebake.mjs';
|
||||
const getSamples = (samples) =>
|
||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function SoundsTab() {
|
||||
const sounds = useStore(soundMap);
|
||||
|
||||
@@ -56,9 +60,7 @@ export function SoundsTab() {
|
||||
|
||||
// holds mutable ref to current triggered sound
|
||||
const trigRef = useRef();
|
||||
|
||||
// Used to cycle through sound previews on banks with multiple sounds
|
||||
let soundPreviewIdx = 0;
|
||||
const numRef = useRef(0);
|
||||
|
||||
// stop current sound on mouseup
|
||||
useEvent('mouseup', () => {
|
||||
@@ -66,6 +68,14 @@ export function SoundsTab() {
|
||||
trigRef.current = undefined;
|
||||
ref?.stop?.(getAudioContext().currentTime + 0.01);
|
||||
});
|
||||
useEvent('keydown', (e) => {
|
||||
if (!isNaN(Number(e.key))) {
|
||||
numRef.current = Number(e.key);
|
||||
}
|
||||
});
|
||||
useEvent('keyup', (e) => {
|
||||
numRef.current = 0;
|
||||
});
|
||||
return (
|
||||
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
|
||||
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||
@@ -115,25 +125,34 @@ export function SoundsTab() {
|
||||
const params = {
|
||||
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
|
||||
s: name,
|
||||
n: soundPreviewIdx,
|
||||
n: numRef.current,
|
||||
clip: 1,
|
||||
release: 0.5,
|
||||
sustain: 1,
|
||||
duration: 0.5,
|
||||
};
|
||||
soundPreviewIdx++;
|
||||
const onended = () => trigRef.current?.node?.disconnect();
|
||||
try {
|
||||
// Pre-load the sample by calling onTrigger with a future time
|
||||
// This triggers the loading but schedules playback for later
|
||||
const time = ctx.currentTime + 0.05;
|
||||
const ref = await onTrigger(time, params, onended);
|
||||
trigRef.current = ref;
|
||||
if (ref?.node) {
|
||||
connectToDestination(ref.node);
|
||||
// Attempt to play the sample and retry every 200ms until 10 attempts have been reached
|
||||
let errMsg;
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
try {
|
||||
// Pre-load the sample by calling onTrigger with a future time
|
||||
// This triggers the loading but schedules playback for later
|
||||
const time = ctx.currentTime + 0.05; // Give 50ms for loading
|
||||
const ref = await onTrigger(time, params, onended);
|
||||
trigRef.current = ref;
|
||||
if (ref?.node) {
|
||||
connectToDestination(ref.node);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
errMsg = err;
|
||||
}
|
||||
if (attempt == 9) {
|
||||
console.warn('Failed to trigger sound after 10 attempts' + (errMsg ? `: ${errMsg}` : ''));
|
||||
} else {
|
||||
await wait(200);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to trigger sound:', err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { aliasBank, registerSynthSounds, registerZZFXSounds, samples } from '@st
|
||||
import { registerSamplesFromDB } from './idbutils.mjs';
|
||||
import './piano.mjs';
|
||||
import './files.mjs';
|
||||
import { settingsMap } from '@src/settings.mjs';
|
||||
import { evaluate } from '@strudel/transpiler';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
|
||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
||||
import { getDrawContext } from '@strudel/draw';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import { evaluate, transpiler } from '@strudel/transpiler';
|
||||
import {
|
||||
getAudioContextCurrentTime,
|
||||
webaudioOutput,
|
||||
@@ -63,11 +63,10 @@ async function getModule(name) {
|
||||
const initialCode = `// LOADING`;
|
||||
|
||||
export function useReplContext() {
|
||||
const { isSyncEnabled, audioEngineTarget } = useSettings();
|
||||
const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings();
|
||||
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
|
||||
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
||||
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
||||
|
||||
const init = useCallback(() => {
|
||||
const drawTime = [-2, 2];
|
||||
const drawContext = getDrawContext();
|
||||
@@ -84,7 +83,12 @@ export function useReplContext() {
|
||||
pattern: silence,
|
||||
drawTime,
|
||||
drawContext,
|
||||
prebake: async () => Promise.all([modulesLoading, presets]),
|
||||
prebake: async () =>
|
||||
Promise.all([modulesLoading, presets]).then(() => {
|
||||
if (prebakeScript?.length) {
|
||||
return evaluate(prebakeScript ?? '');
|
||||
}
|
||||
}),
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
@@ -214,7 +218,13 @@ export function useReplContext() {
|
||||
editorRef.current.repl.evaluate(code);
|
||||
};
|
||||
|
||||
const handleShare = async () => shareCode(replState.code);
|
||||
const handleShare = async () => {
|
||||
let code = replState.code;
|
||||
if (includePrebakeScriptInShare) {
|
||||
code = prebakeScript + '\n' + code;
|
||||
}
|
||||
shareCode(code);
|
||||
};
|
||||
const context = {
|
||||
started,
|
||||
pending,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { evalScope, hash2code, logger } from '@strudel/core';
|
||||
import { code2hash, evalScope, hash2code, logger } from '@strudel/core';
|
||||
import { settingPatterns } from '../settings.mjs';
|
||||
import { setVersionDefaults } from '@strudel/webaudio';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
import { isTauri } from '../tauri.mjs';
|
||||
import './Repl.css';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs';
|
||||
|
||||
@@ -109,9 +108,8 @@ export function confirmDialog(msg) {
|
||||
});
|
||||
}
|
||||
|
||||
let lastShared;
|
||||
|
||||
//RIP due to SPAM
|
||||
// let lastShared;
|
||||
// export async function shareCode(codeToShare) {
|
||||
// // const codeToShare = activeCode || code;
|
||||
// if (lastShared === codeToShare) {
|
||||
@@ -146,9 +144,10 @@ let lastShared;
|
||||
// });
|
||||
// }
|
||||
|
||||
export async function shareCode() {
|
||||
export async function shareCode(codeToShare) {
|
||||
try {
|
||||
const shareUrl = window.location.href;
|
||||
const hash = '#' + code2hash(codeToShare);
|
||||
const shareUrl = window.location.origin + window.location.pathname + hash;
|
||||
if (isTauri()) {
|
||||
await writeText(shareUrl);
|
||||
} else {
|
||||
|
||||
@@ -45,11 +45,13 @@ export const defaultSettings = {
|
||||
isPanelOpen: true,
|
||||
togglePanelTrigger: 'click', //click | hover
|
||||
userPatterns: '{}',
|
||||
prebakeScript: '',
|
||||
audioEngineTarget: audioEngineTargets.webaudio,
|
||||
isButtonRowHidden: false,
|
||||
isCSSAnimationDisabled: false,
|
||||
maxPolyphony: 128,
|
||||
multiChannelOrbits: false,
|
||||
includePrebakeScriptInShare: true,
|
||||
};
|
||||
|
||||
let search = null;
|
||||
@@ -96,6 +98,7 @@ export function useSettings() {
|
||||
isPanelOpen: parseBoolean(state.isPanelOpen),
|
||||
userPatterns: userPatterns,
|
||||
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
|
||||
includePrebakeScriptInShare: parseBoolean(state.includePrebakeScriptInShare),
|
||||
patternAutoStart: isUdels()
|
||||
? false
|
||||
: state.patternAutoStart === undefined
|
||||
@@ -108,6 +111,8 @@ export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
||||
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
||||
|
||||
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);
|
||||
|
||||
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
||||
|
||||
const patternSetting = (key) =>
|
||||
|
||||
Reference in New Issue
Block a user