diff --git a/my-patterns/README.md b/my-patterns/README.md new file mode 100644 index 00000000..f504d328 --- /dev/null +++ b/my-patterns/README.md @@ -0,0 +1,16 @@ +# my-patterns + +This directory can be used to save your own patterns. + +0. fork the strudel repo +1. Save one or more .txt files here. +2. and run `npm run repl` +3. open `http://localhost:3000/my-patterns` ! + +## deploy + +1. in your fork, go to settings -> pages and select "Github Actions" as source +2. edit `website/public/CNAME` to contain `.github.io/strudel` +3. edit `website/astro.config.mjs` to use site: `.github.io/strudel` and base `/strudel` +4. go to Actions -> "Build and Deploy" and click "Run workflow" +5. view your patterns at `.github.io/strudel/my-patterns` diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 122bb449..34ce1d4b 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, toMidi, getDrawContext, freqToMidi } from './index.mjs'; +import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs'; const scale = (normalized, min, max) => normalized * (max - min) + min; const getValue = (e) => { @@ -12,13 +12,19 @@ const getValue = (e) => { if (typeof e.value !== 'object') { value = { value }; } - let { note, n, freq } = value; + let { note, n, freq, s } = value; if (freq) { - note = freqToMidi(freq); + return freqToMidi(freq); } - value = note ?? n ?? e.value; - if (typeof value === 'string') { - value = toMidi(value); + note = note ?? n; + if (typeof note === 'string') { + return toMidi(note); + } + if (typeof note === 'number') { + return note; + } + if (s) { + return '_' + s; } return value; }; @@ -143,7 +149,7 @@ Pattern.prototype.pianoroll = function ({ maxMidi = max; valueExtent = maxMidi - minMidi + 1; } - foldValues = values.sort((a, b) => a - b); + foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; }, }, @@ -215,7 +221,8 @@ export function pianoroll({ maxMidi = max; valueExtent = maxMidi - minMidi + 1; } - foldValues = values.sort((a, b) => a - b); + // foldValues = values.sort((a, b) => a - b); + foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; ctx.fillStyle = background; diff --git a/packages/mini/krill-parser.js b/packages/mini/krill-parser.js index c4a5d09f..2bc565f7 100644 --- a/packages/mini/krill-parser.js +++ b/packages/mini/krill-parser.js @@ -32,7 +32,7 @@ function peg$padEnd(str, targetLength, padString) { } peg$SyntaxError.prototype.format = function(sources) { - var str = "peg error: " + this.message; + var str = "Error: " + this.message; if (this.location) { var src = null; var k; @@ -271,8 +271,8 @@ function peg$parse(input, options) { var peg$f4 = function(a) { return { weight: a} }; var peg$f5 = function(a) { return { replicate: a } }; var peg$f6 = function(p, s, r) { return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } }; - var peg$f7 = function(a) { return { operator : { type_: "stretch", arguments_ :{ amount:a } } } }; - var peg$f8 = function(a) { return { operator : { type_: "stretch", arguments_ :{ amount:"1/"+a } } } }; + var peg$f7 = function(a) { return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'slow' } } } }; + var peg$f8 = function(a) { return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'fast' } } } }; var peg$f9 = function(a) { return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }; var peg$f10 = function(a) { return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }; var peg$f11 = function(s, o) { return new ElementStub(s, o);}; diff --git a/packages/mini/krill.pegjs b/packages/mini/krill.pegjs index 6302614d..87e9df3f 100644 --- a/packages/mini/krill.pegjs +++ b/packages/mini/krill.pegjs @@ -116,10 +116,10 @@ slice_bjorklund = "(" ws p:number ws comma ws s:number ws comma? ws r:number? ws { return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } } slice_slow = "/"a:number - { return { operator : { type_: "stretch", arguments_ :{ amount:a } } } } + { return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'slow' } } } } slice_fast = "*"a:number - { return { operator : { type_: "stretch", arguments_ :{ amount:"1/"+a } } } } + { return { operator : { type_: "stretch", arguments_ :{ amount:a, type: 'fast' } } } } slice_fixed_step = "%"a:number { return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } } diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 00ecb24a..745f0f85 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -23,8 +23,12 @@ const applyOptions = (parent) => (pat, i) => { if (operator) { switch (operator.type_) { case 'stretch': { - const speed = Fraction(operator.arguments_.amount).inverse(); - return reify(pat).fast(speed); + const legalTypes = ['fast', 'slow']; + const { type, amount } = operator.arguments_; + if (!legalTypes.includes(type)) { + throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`); + } + return reify(pat)[type](amount); } case 'bjorklund': return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation); @@ -74,32 +78,32 @@ function resolveReplications(ast) { // could this be made easier?! ast.source_ = ast.source_.map((child) => { const { replicate, ...options } = child.options_ || {}; - if (replicate) { - return { - ...child, - options_: { ...options, weight: replicate }, - source_: { - type_: 'pattern', - arguments_: { - alignment: 'h', - }, - source_: [ - { - type_: 'element', - source_: child.source_, - location_: child.location_, - options_: { - operator: { - type_: 'stretch', - arguments_: { amount: Fraction(replicate).inverse().toString() }, - }, + if (!replicate) { + return child; + } + return { + ...child, + options_: { ...options, weight: replicate }, + source_: { + type_: 'pattern', + arguments_: { + alignment: 'h', + }, + source_: [ + { + type_: 'element', + source_: child.source_, + location_: child.location_, + options_: { + operator: { + type_: 'stretch', + arguments_: { amount: replicate, type: 'fast' }, }, }, - ], - }, - }; - } - return child; + }, + ], + }, + }; }); } diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index 3717937e..b5adfeb0 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -339,8 +339,8 @@ exports[`renders tunes > tune: blippyRhodes 1`] = ` "[ 2/3 → 43/60 | note:G3 s:rhodes clip:1 room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]", "[ 5/6 → 53/60 | note:G3 s:rhodes clip:1 room:0.5 delay:0.3 delayfeedback:0.4 delaytime:0.08333333333333333 gain:0.5 ]", "[ (0/1 → 2/3) ⇝ 4/3 | note:c2 gain:0.3 s:sawtooth cutoff:600 ]", - "[ 0/1 ⇜ (2/3 → 1/1) ⇝ 4/3 | note:c2 gain:0.3 s:sawtooth cutoff:600 ]", "[ (0/1 → 2/3) ⇝ 4/3 | note:36.02 gain:0.3 s:sawtooth cutoff:600 ]", + "[ 0/1 ⇜ (2/3 → 1/1) ⇝ 4/3 | note:c2 gain:0.3 s:sawtooth cutoff:600 ]", "[ 0/1 ⇜ (2/3 → 1/1) ⇝ 4/3 | note:36.02 gain:0.3 s:sawtooth cutoff:600 ]", ] `; @@ -8127,10 +8127,10 @@ exports[`renders tunes > tune: loungeSponge 1`] = ` exports[`renders tunes > tune: meltingsubmarine 1`] = ` [ - "[ (0/1 → 1/1) ⇝ 3/2 | s:bd speed:0.7519542165100574 ]", - "[ (3/4 → 1/1) ⇝ 3/2 | s:sd speed:0.7931522866332671 ]", - "[ 3/8 → 3/4 | s:hh speed:0.7285963821098448 ]", - "[ (3/4 → 1/1) ⇝ 9/8 | s:hh speed:0.77531205091027 ]", + "[ (0/1 → 1/1) ⇝ 3/2 | s:bd:5 speed:0.7519542165100574 ]", + "[ (3/4 → 1/1) ⇝ 3/2 | s:sd:1 speed:0.7931522866332671 ]", + "[ 3/8 → 3/4 | s:hh27 speed:0.7285963821098448 ]", + "[ (3/4 → 1/1) ⇝ 9/8 | s:hh27 speed:0.77531205091027 ]", "[ (0/1 → 1/1) ⇝ 3/2 | n:33.129885541275144 decay:0.15 sustain:0 s:sawtooth gain:0.4 cutoff:3669.6267869262615 ]", "[ (0/1 → 1/1) ⇝ 3/2 | n:33.17988554127514 decay:0.15 sustain:0 s:sawtooth gain:0.4 cutoff:3669.6267869262615 ]", "[ (0/1 → 1/1) ⇝ 3/2 | n:55.129885541275144 s:sawtooth gain:0.16 cutoff:500 attack:1 ]", diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 83038fbc..6714f548 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -31,6 +31,7 @@ export default defineConfig({ tailwind(), ], site: `https://strudel.tidalcycles.org`, + base: '', }); /* diff --git a/website/src/pages/embed.astro b/website/src/pages/embed.astro new file mode 100644 index 00000000..de8d2c2d --- /dev/null +++ b/website/src/pages/embed.astro @@ -0,0 +1,14 @@ +--- +import HeadCommon from '../components/HeadCommon.astro'; +import { Repl } from '../repl/Repl.jsx'; +--- + + + + + Strudel REPL + + + + + diff --git a/website/src/pages/my-patterns/[name].png.js b/website/src/pages/my-patterns/[name].png.js new file mode 100644 index 00000000..aa8aabf3 --- /dev/null +++ b/website/src/pages/my-patterns/[name].png.js @@ -0,0 +1,29 @@ +import { createCanvas } from 'canvas'; +import { pianoroll } from '@strudel.cycles/core'; +import { evaluate } from '@strudel.cycles/transpiler'; +import '../../../../test/runtime.mjs'; +import { getMyPatterns } from './list.json'; + +export async function get({ params, request }) { + const patterns = await getMyPatterns(); + const { name } = params; + const tune = patterns[name]; + const { pattern } = await evaluate(tune); + const haps = pattern.queryArc(0, 4); + const canvas = createCanvas(800, 800); + const ctx = canvas.getContext('2d'); + pianoroll({ time: 4, haps, ctx, playhead: 1, fold: 1, background: 'transparent', playheadColor: 'transparent' }); + const buffer = canvas.toBuffer('image/png'); + return { + body: buffer, + encoding: 'binary', + }; +} +export async function getStaticPaths() { + const patterns = await getMyPatterns(); + return Object.keys(patterns).map((name) => ({ + params: { + name, + }, + })); +} diff --git a/website/src/pages/my-patterns/index.astro b/website/src/pages/my-patterns/index.astro new file mode 100644 index 00000000..5c7d65ad --- /dev/null +++ b/website/src/pages/my-patterns/index.astro @@ -0,0 +1,32 @@ +--- +import { getMyPatterns } from './list.json'; + +import { Content } from '../../../../my-patterns/README.md'; + +const myPatterns = await getMyPatterns(); +--- + + + { + Object.keys(myPatterns).length === 0 && ( +
+ +
+ ) + } +
+ { + Object.entries(myPatterns).map(([name, tune]) => ( + +
+ {name} +
+ +
+ )) + } +
+ diff --git a/website/src/pages/my-patterns/list.json.js b/website/src/pages/my-patterns/list.json.js new file mode 100644 index 00000000..6d86e384 --- /dev/null +++ b/website/src/pages/my-patterns/list.json.js @@ -0,0 +1,15 @@ +export function getMyPatterns() { + const my = import.meta.glob('../../../../my-patterns/**', { as: 'raw', eager: true }); + return Object.fromEntries( + Object.entries(my) // + .filter(([name]) => name.endsWith('.txt')) // + .map(([name, raw]) => [name.split('/').slice(-1), raw]), // + ); +} + +export async function get() { + const all = await getMyPatterns(); + return { + body: JSON.stringify(all), + }; +} diff --git a/website/src/repl/Header.jsx b/website/src/repl/Header.jsx index c9de3b75..1ea539fc 100644 --- a/website/src/repl/Header.jsx +++ b/website/src/repl/Header.jsx @@ -1,5 +1,5 @@ import AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon'; -import CommandLineIcon from '@heroicons/react/20/solid/CommandLineIcon'; +import ArrowPathIcon from '@heroicons/react/20/solid/ArrowPathIcon'; import LinkIcon from '@heroicons/react/20/solid/LinkIcon'; import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon'; @@ -9,10 +9,9 @@ import React, { useContext } from 'react'; // import { ReplContext } from './Repl'; import './Repl.css'; -const isEmbedded = window.location !== window.parent.location; - export function Header({ context }) { const { + embedded, started, pending, isDirty, @@ -25,32 +24,37 @@ export function Header({ context }) { isZen, setIsZen, } = context; + const isEmbedded = embedded || window.location !== window.parent.location; // useContext(ReplContext) return ( diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index ff6be87b..53c744d8 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -22,6 +22,7 @@ import { Footer } from './Footer'; import { Header } from './Header'; import { prebake } from './prebake.mjs'; import * as tunes from './tunes.mjs'; +import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; initAudioOnFirstClick(); @@ -101,7 +102,8 @@ const { code: randomTune, name } = getRandomTune(); export const ReplContext = createContext(null); -export function Repl() { +export function Repl({ embedded = false }) { + const isEmbedded = embedded || window.location !== window.parent.location; const [view, setView] = useState(); // codemirror view const [lastShared, setLastShared] = useState(); const [activeFooter, setActiveFooter] = useState(''); @@ -232,6 +234,7 @@ export function Repl() { } }; const context = { + embedded, started, pending, isDirty, @@ -269,7 +272,16 @@ export function Repl() { {error && (
{error.message || 'Unknown Error :-/'}
)} -