Try clicking the logo in the top left!
-
+
);
diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx
index 3d363f8a1..52fbf928c 100644
--- a/website/src/repl/components/panel/SoundsTab.jsx
+++ b/website/src/repl/components/panel/SoundsTab.jsx
@@ -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 (
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);
}
}}
>
diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs
index 8a6ccc7e0..53a2a6003 100644
--- a/website/src/repl/prebake.mjs
+++ b/website/src/repl/prebake.mjs
@@ -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;
diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx
index ac30fe342..8dcb6b754 100644
--- a/website/src/repl/useReplContext.jsx
+++ b/website/src/repl/useReplContext.jsx
@@ -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,
diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs
index ac27158f4..4a7cb26a8 100644
--- a/website/src/repl/util.mjs
+++ b/website/src/repl/util.mjs
@@ -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 {
diff --git a/website/src/settings.mjs b/website/src/settings.mjs
index 7c62b0ded..8ec7d455a 100644
--- a/website/src/settings.mjs
+++ b/website/src/settings.mjs
@@ -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) =>