diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 62839ecd..75ef0278 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -20,12 +20,14 @@ import { useSettings, getViewingPattern, setViewingPattern, + getNextCloneName, } from '../settings.mjs'; import { Header } from './Header'; import Loader from './Loader'; import { Panel } from './panel/Panel'; import { useStore } from '@nanostores/react'; import { prebake } from './prebake.mjs'; +import * as tunes from './tunes.mjs'; import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import './Repl.css'; @@ -44,7 +46,6 @@ if (typeof window !== 'undefined') { clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width); isIframe = window.location !== window.parent.location; } - export function Repl({ embedded = false }) { const isEmbedded = embedded || isIframe; const { panelPosition, isZen } = useSettings(); @@ -74,13 +75,23 @@ export function Repl({ embedded = false }) { setReplState({ ...state }); }, afterEval: ({ code }) => { - updateUserCode(code); - // setPending(false); setLatestCode(code); - const viewingPatternID = getViewingPattern(); + let pattern = getViewingPattern(); window.location.hash = '#' + code2hash(code); - if (viewingPatternID != null) { - setActivePattern(viewingPatternID); + const isExamplePattern = !!tunes[pattern]; + + if (isExamplePattern) { + const codeHasChanged = code !== tunes[pattern]; + if (codeHasChanged) { + // fork example + pattern = getNextCloneName(pattern); + setViewingPattern(pattern); + updateUserCode(pattern, code); + } + setActivePattern(pattern); + } else { + setActivePattern(pattern); + updateUserCode(pattern, code); } }, bgFill: false, @@ -149,6 +160,7 @@ export function Repl({ embedded = false }) { // payload = {reset?: boolean, code?: string, evaluate?: boolean, patternID?: string } const handleUpdate = async (payload) => { const { reset = false, code = null, evaluate = true, patternID = null } = payload; + if (reset) { clearCanvas(); resetLoadedSounds(); diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx index 59c1f421..a5ded1f0 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/panel/PatternsTab.jsx @@ -24,8 +24,8 @@ function PatternButton({ showOutline, onClick, label, showHiglight }) { @@ -54,15 +54,15 @@ export function PatternsTab({ context }) { const { userPatterns } = useSettings(); const activePattern = useActivePattern(); const viewingPattern = useViewingPattern(); - const isExample = useMemo(() => activePattern && !!tunes[activePattern], [activePattern]); + // const isExample = useMemo(() => activePattern && !!tunes[activePattern], [activePattern]); const onPatternClick = (key, data) => { - const { code } = data; - context.handleUpdate({ patternID: key, code, evaluate: false }); + // display selected pattern code in the window + context.handleUpdate({ patternID: key, code: data.code, evaluate: false }); }; const examplePatterns = {}; Object.entries(tunes).forEach(([key, code]) => (examplePatterns[key] = { code })); - + const isExample = examplePatterns[viewingPattern] != null; return (
@@ -124,7 +124,12 @@ export function PatternsTab({ context }) {

Examples

- +
); diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index 5f68ee93..ef847774 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -5,6 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th */ export const swimming = `// Koji Kondo - Swimming (Super Mario World) + +setCps(1) + stack( seq( "~", @@ -69,6 +72,8 @@ stack( export const giantSteps = `// John Coltrane - Giant Steps +setCps(1) + let melody = note( "[F#5 D5] [B4 G4] Bb4 [B4 A4]", "[D5 Bb4] [G4 Eb4] F#4 [G4 F4]", @@ -99,6 +104,9 @@ stack( .pianoroll({fold:1})`; export const zeldasRescue = `// Koji Kondo - Princess Zelda's Rescue + +setCps(1) + stack( // melody \`[B3@2 D4] [A3@2 [G3 A3]] [B3@2 D4] [A3] @@ -128,6 +136,9 @@ export const caverave = `// "Caverave" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos + +setCps(1) + const keys = x => x.s('sawtooth').cutoff(1200).gain(.5) .attack(0).decay(.16).sustain(.3).release(.1); @@ -174,6 +185,8 @@ export const sampleDrums = `samples({ hh: 'hh/000_hh3closedhh.wav' }, 'https://loophole-letters.vercel.app/samples/tidal/') +setCps(1) + stack( "".color('#F5A623'), "hh*4".color('#673AB7'), @@ -183,6 +196,9 @@ stack( `; export const barryHarris = `// adapted from a Barry Harris excercise + +setCps(1) + "0,2,[7 6]" .add("<0 1 2 3 4 5 7 8>") .scale('C bebop major') @@ -196,6 +212,8 @@ export const blippyRhodes = `// "Blippy Rhodes" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + samples({ bd: 'samples/tidal/bd/BT0A0D0.wav', sn: 'samples/tidal/sn/ST0T0S3.wav', @@ -239,6 +257,8 @@ export const wavyKalimba = `// "Wavy kalimba" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + samples({ 'kalimba': { c5:'https://freesound.org/data/previews/536/536549_11935698-lq.mp3' } }) @@ -269,6 +289,8 @@ export const festivalOfFingers = `// "Festival of fingers" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + const chords = ""; stack( chord(chords).dict('lefthand').voicing().struct("x(3,8,-1)") @@ -292,6 +314,8 @@ export const undergroundPlumber = `// "Underground plumber" // @by Felix Roos // @details inspired by Friendship - Let's not talk about it (1979) :) +setCps(1) + samples({ bd: 'bd/BT0A0D0.wav', sn: 'sn/ST0T0S3.wav', hh: 'hh/000_hh3closedhh.wav', cp: 'cp/HANDCLP0.wav', }, 'https://loophole-letters.vercel.app/samples/tidal/') @@ -319,6 +343,8 @@ export const bridgeIsOver = `// "Bridge is over" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos, bassline by BDP - The Bridge Is Over +setCps(1) + samples({mad:'https://freesound.org/data/previews/22/22274_109943-lq.mp3'}) stack( stack( @@ -339,6 +365,8 @@ export const goodTimes = `// "Good times" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + const scale = cat('C3 dorian','Bb2 major').slow(4); stack( n("2*4".add(12)).off(1/8, add(2)) @@ -361,6 +389,8 @@ export const echoPiano = `// "Echo piano" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + n("<0 2 [4 6](3,4,2) 3*2>").color('salmon') .off(1/4, x=>x.add(2).color('green')) .off(1/2, x=>x.add(6).color('steelblue')) @@ -371,6 +401,9 @@ n("<0 2 [4 6](3,4,2) 3*2>").color('salmon') .pianoroll()`; export const sml1 = `// Hirokazu Tanaka - World 1-1 + +setCps(1) + stack( // melody note(\`< @@ -409,6 +442,8 @@ export const randomBells = `// "Random bells" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + samples({ bell: { c6: 'https://freesound.org/data/previews/411/411089_5121236-lq.mp3' }, bass: { d2: 'https://freesound.org/data/previews/608/608286_13074022-lq.mp3' } @@ -432,6 +467,8 @@ export const waa2 = `// "Waa2" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + note( "a4 [a3 c3] a3 c3" .sub("<7 12 5 12>".slow(2)) @@ -450,6 +487,8 @@ export const hyperpop = `// "Hyperpop" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + const lfo = cosine.slow(15); const lfo2 = sine.slow(16); const filter1 = x=>x.cutoff(lfo2.range(300,3000)); @@ -505,6 +544,8 @@ export const festivalOfFingers3 = `// "Festival of fingers 3" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + n("[-7*3],0,2,6,[8 7]") .echoWith( 4, // echo 4 times @@ -528,6 +569,8 @@ export const meltingsubmarine = `// "Melting submarine" // @by Felix Roos await samples('github:tidalcycles/Dirt-Samples/master/') +setCps(1) + stack( s("bd:5,[~ ],hh27(3,4,1)") // drums .speed(perlin.range(.7,.9)) // random sample speed variation @@ -609,6 +652,7 @@ bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0 sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'], hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'], }, 'github:tidalcycles/Dirt-Samples/master/'); +setCps(1) note("<8(3,8) <7 7*2> [4 5@3] 8>".sub(1) // sub 1 -> 1-indexed .layer( @@ -631,6 +675,7 @@ export const chop = `// "Chop" // @by Felix Roos samples({ p: 'https://cdn.freesound.org/previews/648/648433_11943129-lq.mp3' }) +setCps(1) s("p") .loopAt(32) @@ -656,6 +701,8 @@ export const orbit = `// "Orbit" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + stack( s("bd ") .delay(.5) @@ -673,6 +720,8 @@ export const belldub = `// "Belldub" // @by Felix Roos samples({ bell: {b4:'https://cdn.freesound.org/previews/339/339809_5121236-lq.mp3'}}) +setCps(1) + // "Hand Bells, B, Single.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org stack( // bass @@ -712,6 +761,7 @@ export const dinofunk = `// "Dinofunk" samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3', dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}}) setVoicingRange('lefthand', ['c3','a4']) +setCps(1) stack( s('bass').loopAt(8).clip(1), @@ -736,6 +786,8 @@ export const sampleDemo = `// "Sample demo" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + stack( // percussion s("[woodblock:1 woodblock:2*2] snare_rim:0,gong/8,brakedrum:1(3,8),~@3 cowbell:3") @@ -754,6 +806,8 @@ export const holyflute = `// "Holy flute" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + "c3 eb3(3,8) c4/2 g3*2" .superimpose( x=>x.slow(2).add(12), @@ -795,6 +849,8 @@ export const amensister = `// "Amensister" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + await samples('github:tidalcycles/Dirt-Samples/master') stack( @@ -833,6 +889,8 @@ export const juxUndTollerei = `// "Jux und tollerei" // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + note("c3 eb3 g3 bb3").palindrome() .s('sawtooth') .jux(x=>x.rev().color('green').s('sawtooth')) @@ -848,6 +906,8 @@ export const csoundDemo = `// "CSound demo" // @license with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @by Felix Roos +setCps(1) + await loadCsound\` instr CoolSynth iduration = p3 @@ -885,6 +945,7 @@ export const loungeSponge = `// "Lounge sponge" // @by Felix Roos, livecode.orc by Steven Yi await loadOrc('github:kunstmusik/csound-live-code/master/livecode.orc') +setCps(1) stack( chord("/2").dict('lefthand').voicing() @@ -905,6 +966,7 @@ export const arpoon = `// "Arpoon" // @by Felix Roos await samples('github:tidalcycles/Dirt-Samples/master') +setCps(1) n("[0,3] 2 [1,3] 2".fast(3).lastOf(4, fast(2))).clip(2) .offset("<<1 2> 2 1 1>") diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 2b93b619..3f7307a1 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -110,3 +110,4 @@ export async function shareCode(codeToShare) { logger(message); } } + diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 8872bdc7..0d38d6b8 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -1,7 +1,6 @@ import { persistentMap, persistentAtom } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel.cycles/core'; -import * as tunes from './repl/tunes.mjs'; import { defaultAudioDeviceName } from './repl/panel/AudioDeviceSelector'; import { logger } from '@strudel.cycles/core'; @@ -126,11 +125,10 @@ export function newUserPattern() { const date = new Date().toISOString().split('T')[0]; const todays = Object.entries(userPatterns).filter(([name]) => name.startsWith(date)); const num = String(todays.length + 1).padStart(3, '0'); - const defaultNewPattern = 's("hh")'; const name = date + '_' + num; - addUserPattern(name, { code: defaultNewPattern }); - setActivePattern(name); - return name; + const code = 's("hh")'; + setUserPatterns({ ...userPatterns, [name]: { code } }); + setViewingPattern(name); } export function clearUserPatterns() { @@ -173,26 +171,9 @@ export function renamePattern(pattern) { setViewingPattern(newName); } -export function updateUserCode(code) { +export function updateUserCode(pattern, code ) { const userPatterns = getUserPatterns(); - let activePattern = getActivePattern(); - // check if code is that of an example tune - const [example] = Object.entries(tunes).find(([_, tune]) => tune === code) || []; - if (example && (!activePattern || activePattern === example)) { - // select example - setActivePattern(example); - return; - } - if (!activePattern) { - // create new user pattern - activePattern = newUserPattern(); - setActivePattern(activePattern); - } else if (!!tunes[activePattern] && code !== tunes[activePattern]) { - // fork example - activePattern = getNextCloneName(activePattern); - setActivePattern(activePattern); - } - setUserPatterns({ ...userPatterns, [activePattern]: { code } }); + setUserPatterns({ ...userPatterns, [pattern]: { code } }); } export function deletePattern(pattern) {