mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-12 06:08:34 +00:00
Merge pull request #919 from tidalcycles/blog-improvements
Blog improvements
This commit is contained in:
commit
6ef9ca18e9
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@ -490,6 +490,9 @@ importers:
|
||||
'@astrojs/react':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)(vite@5.0.11)
|
||||
'@astrojs/rss':
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2
|
||||
'@astrojs/tailwind':
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(astro@4.0.8)(tailwindcss@3.4.0)
|
||||
@ -890,6 +893,13 @@ packages:
|
||||
- vite
|
||||
dev: false
|
||||
|
||||
/@astrojs/rss@4.0.2:
|
||||
resolution: {integrity: sha512-Hb9GKAyvsn5EUjZtB6SniesBScMQe7SQinEHLY5EFa74QEvgcWaXTmA0Mb0P3vqDSN3d/NTYbGivprrSAawfnA==}
|
||||
dependencies:
|
||||
fast-xml-parser: 4.3.3
|
||||
kleur: 4.1.5
|
||||
dev: false
|
||||
|
||||
/@astrojs/tailwind@5.1.0(astro@4.0.8)(tailwindcss@3.4.0):
|
||||
resolution: {integrity: sha512-BJoCDKuWhU9FT2qYg+fr6Nfb3qP4ShtyjXGHKA/4mHN94z7BGcmauQK23iy+YH5qWvTnhqkd6mQPQ1yTZTe9Ig==}
|
||||
peerDependencies:
|
||||
@ -7182,6 +7192,13 @@ packages:
|
||||
/fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
/fast-xml-parser@4.3.3:
|
||||
resolution: {integrity: sha512-coV/D1MhrShMvU6D0I+VAK3umz6hUaxxhL0yp/9RjfiYUfAv14rDhGQL+PLForhMdr0wq3PiV07WtkkNjJjNHg==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
strnum: 1.0.5
|
||||
dev: false
|
||||
|
||||
/fastq@1.15.0:
|
||||
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
|
||||
dependencies:
|
||||
@ -12637,6 +12654,10 @@ packages:
|
||||
acorn: 8.11.3
|
||||
dev: true
|
||||
|
||||
/strnum@1.0.5:
|
||||
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
|
||||
dev: false
|
||||
|
||||
/strong-log-transformer@2.1.0:
|
||||
resolution: {integrity: sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"@astro-community/astro-embed-youtube": "^0.4.3",
|
||||
"@astrojs/mdx": "^2.0.3",
|
||||
"@astrojs/react": "^3.0.9",
|
||||
"@astrojs/rss": "^4.0.2",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@docsearch/css": "^3.5.2",
|
||||
"@docsearch/react": "^3.5.2",
|
||||
@ -23,21 +24,21 @@
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@nanostores/persistent": "^0.9.1",
|
||||
"@nanostores/react": "^0.7.1",
|
||||
"@strudel/codemirror": "workspace:*",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/csound": "workspace:*",
|
||||
"@strudel/desktopbridge": "workspace:*",
|
||||
"@strudel/hydra": "workspace:*",
|
||||
"@strudel/midi": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/osc": "workspace:*",
|
||||
"@strudel/repl": "workspace:*",
|
||||
"@strudel/serial": "workspace:*",
|
||||
"@strudel/soundfonts": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*",
|
||||
"@strudel/xen": "workspace:*",
|
||||
"@strudel/codemirror": "workspace:*",
|
||||
"@strudel/desktopbridge": "workspace:*",
|
||||
"@strudel/hydra": "workspace:*",
|
||||
"@strudel/repl": "workspace:*",
|
||||
"@supabase/supabase-js": "^2.39.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
|
||||
type Props = CollectionEntry<'blog'>['data'];
|
||||
type Props = { post: CollectionEntry<'blog'> };
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
@ -9,7 +9,7 @@ import { format } from 'date-fns';
|
||||
---
|
||||
|
||||
<article
|
||||
class="prose max-w-none prose-headings:font-sans prose-headings:font-black prose-headings:text-slate-900 dark:prose-headings:text-gray-200 dark:text-gray-400 dark:prose-strong:text-gray-400 dark:prose-code:text-slate-400 dark:prose-a:text-gray-300 prose-a:text-slate-900 prose-blockquote:text-slate-800 dark:prose-blockquote:text-slate-400"
|
||||
class="prose max-w-none prose-headings:font-sans prose-headings:font-black prose-headings:text-slate-900 dark:prose-headings:text-gray-200 dark:text-gray-400 dark:prose-strong:text-gray-400 dark:prose-code:text-slate-400 dark:prose-a:text-gray-300 prose-a:text-slate-900 prose-blockquote:text-slate-800 dark:prose-blockquote:text-slate-400 border-b-4 border-lineHighlight pt-4"
|
||||
>
|
||||
<div class="pb-2">
|
||||
<div class="md:flex justify-between">
|
||||
|
||||
@ -10,7 +10,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<link rel="alternate" type="application/rss+xml" title={`RSS Feed for strudel.cc`} href="/rss.xml" />
|
||||
<link rel="icon" type="image/svg+xml" href={`${baseNoTrailing}/favicon.ico`} />
|
||||
|
||||
<meta
|
||||
@ -24,7 +24,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
<base href={BASE_URL} />
|
||||
|
||||
<!-- Scrollable a11y code helper -->
|
||||
<script src{`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||
<script {`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
|
||||
|
||||
<script src="/src/pwa.ts"></script>
|
||||
<!-- this does not work for some reason: -->
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
import { SITE, OPEN_GRAPH, Frontmatter } from '../config';
|
||||
import { SITE, OPEN_GRAPH, type Frontmatter } from '../config';
|
||||
|
||||
export interface Props {
|
||||
frontmatter: Frontmatter;
|
||||
@ -25,11 +25,3 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
|
||||
<meta property="og:image:alt" content={imageAlt} />
|
||||
<meta name="description" property="og:description" content={frontmatter.description ?? SITE.description} />
|
||||
<meta property="og:site_name" content={SITE.title} />
|
||||
|
||||
<!-- Twitter Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content={OPEN_GRAPH.twitter} />
|
||||
<meta name="twitter:title" content={formattedContentTitle} />
|
||||
<meta name="twitter:description" content={frontmatter.description ?? SITE.description} />
|
||||
<meta name="twitter:image" content={canonicalImageSrc} />
|
||||
<meta name="twitter:image:alt" content={imageAlt} />
|
||||
|
||||
@ -1,4 +1,19 @@
|
||||
export function Icon({ type }) {
|
||||
if (type === 'skip') {
|
||||
// !Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.
|
||||
return (
|
||||
<svg
|
||||
fillRule="evenodd"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="16"
|
||||
width="10"
|
||||
viewBox="0 0 320 512"
|
||||
>
|
||||
<path d="M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416V96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4l192 160L256 241V96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V271l-11.5 9.6-192 160z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
{
|
||||
@ -31,6 +46,13 @@ export function Icon({ type }) {
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
),
|
||||
skip: (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416V96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4l192 160L256 241V96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V271l-11.5 9.6-192 160z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
),
|
||||
}[type]
|
||||
}
|
||||
</svg>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { silence, getPunchcardPainter, noteToMidi } from '@strudel/core';
|
||||
import { silence, getPunchcardPainter, noteToMidi, _mod } from '@strudel/core';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import { getAudioContext, webaudioOutput } from '@strudel/webaudio';
|
||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
|
||||
import { StrudelMirror } from '@strudel/codemirror';
|
||||
// import { prebake } from '@strudel/repl';
|
||||
import { prebake } from '../repl/prebake.mjs';
|
||||
@ -10,14 +10,16 @@ import { loadModules } from '../repl/util.mjs';
|
||||
import Claviature from '@components/Claviature';
|
||||
import useClient from '@src/useClient.mjs';
|
||||
|
||||
let prebaked, modulesLoading;
|
||||
let prebaked, modulesLoading, audioLoading;
|
||||
if (typeof window !== 'undefined') {
|
||||
prebaked = prebake();
|
||||
modulesLoading = loadModules();
|
||||
audioLoading = initAudioOnFirstClick();
|
||||
}
|
||||
|
||||
export function MiniRepl({
|
||||
tune: code,
|
||||
tune,
|
||||
tunes,
|
||||
hideHeader = false,
|
||||
canvasHeight = 100,
|
||||
onTrigger,
|
||||
@ -26,6 +28,7 @@ export function MiniRepl({
|
||||
claviature,
|
||||
claviatureLabels,
|
||||
}) {
|
||||
const code = tunes ? tunes[0] : tune;
|
||||
const id = useMemo(() => s4(), []);
|
||||
const canvasId = useMemo(() => `canvas-${id}`, [id]);
|
||||
const shouldDraw = !!punchcard || !!claviature;
|
||||
@ -75,7 +78,7 @@ export function MiniRepl({
|
||||
}
|
||||
return pat;
|
||||
},
|
||||
prebake: async () => Promise.all([modulesLoading, prebaked]),
|
||||
prebake: async () => Promise.all([modulesLoading, prebaked, audioLoading]),
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
@ -91,6 +94,14 @@ export function MiniRepl({
|
||||
const containerRef = useRef();
|
||||
const client = useClient();
|
||||
|
||||
const [tuneIndex, setTuneIndex] = useState(0);
|
||||
const changeTune = (index) => {
|
||||
index = _mod(index, tunes.length);
|
||||
setTuneIndex(index);
|
||||
editorRef.current?.setCode(tunes[index]);
|
||||
editorRef.current?.evaluate();
|
||||
};
|
||||
|
||||
if (!client) {
|
||||
return <pre>{code}</pre>;
|
||||
}
|
||||
@ -119,6 +130,28 @@ export function MiniRepl({
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
{tunes && (
|
||||
<div className="flex">
|
||||
<button
|
||||
className={
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
|
||||
}
|
||||
onClick={() => changeTune(tuneIndex - 1)}
|
||||
>
|
||||
<div className="rotate-180">
|
||||
<Icon type="skip" />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
|
||||
}
|
||||
onClick={() => changeTune(tuneIndex + 1)}
|
||||
>
|
||||
<Icon type="skip" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-auto relative p-1">
|
||||
|
||||
81
website/src/examples.mjs
Normal file
81
website/src/examples.mjs
Normal file
@ -0,0 +1,81 @@
|
||||
export const examples = [
|
||||
`// "coastline" @by eddyflux
|
||||
await samples('github:eddyflux/crate')
|
||||
setcps(.75)
|
||||
let chords = chord("<Bbm9 Fm9>/4").dict('ireal')
|
||||
stack(
|
||||
stack( // DRUMS
|
||||
s("bd").struct("<[x*<1 2> [~@3 x]] x>"),
|
||||
s("~ [rim, sd:<2 3>]").room("<0 .2>"),
|
||||
n("[0 <1 3>]*<2!3 4>").s("hh"),
|
||||
s("rd:<1!3 2>*2").mask("<0 0 1 1>/16").gain(.5)
|
||||
).bank('crate')
|
||||
.mask("<[0 1] 1 1 1>/16".early(.5))
|
||||
, // CHORDS
|
||||
chords.offset(-1).voicing().s("gm_epiano1:1")
|
||||
.phaser(4).room(.5)
|
||||
, // MELODY
|
||||
n("<0!3 1*2>").set(chords).mode("root:g2")
|
||||
.voicing().s("gm_acoustic_bass"),
|
||||
chords.n("[0 <4 3 <2 5>>*2](<3 5>,8)")
|
||||
.set(x).anchor("D5").voicing()
|
||||
.segment(4).clip(rand.range(.4,.8))
|
||||
.room(.75).shape(.3).delay(.25)
|
||||
.fm(sine.range(3,8).slow(8))
|
||||
.lpf(sine.range(500,1000).slow(8)).lpq(5)
|
||||
.rarely(ply("2")).chunk(4, fast(2))
|
||||
.gain(perlin.range(.6, .9))
|
||||
.mask("<0 1 1 0>/16")
|
||||
)
|
||||
.late("[0 .01]*4").late("[0 .01]*2").size(4)`,
|
||||
`// "broken cut 1" @by froos
|
||||
|
||||
await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
samples({
|
||||
'slap': 'https://cdn.freesound.org/previews/495/495416_10350281-lq.mp3',
|
||||
'whirl': 'https://cdn.freesound.org/previews/495/495313_10350281-lq.mp3',
|
||||
'attack': 'https://cdn.freesound.org/previews/494/494947_10350281-lq.mp3'
|
||||
})
|
||||
|
||||
setcps(1.25)
|
||||
|
||||
note("[c2 ~](3,8)*2,eb,g,bb,d").s("sawtooth")
|
||||
.noise(0.3)
|
||||
.lpf(perlin.range(800,2000).mul(0.6))
|
||||
.lpenv(perlin.range(1,5)).lpa(.25).lpd(.1).lps(0)
|
||||
.add.mix(note("<0!3 [1 <4!3 12>]>")).late(.5)
|
||||
.vib("4:.2")
|
||||
.room(1).roomsize(4).slow(4)
|
||||
.stack(
|
||||
s("bd").late("<0.01 .251>"),
|
||||
s("breaks165:1/2").fit()
|
||||
.chop(4).sometimesBy(.4, ply("2"))
|
||||
.sometimesBy(.1, ply("4")).release(.01)
|
||||
.gain(1.5).sometimes(mul(speed("1.05"))).cut(1)
|
||||
,
|
||||
s("<whirl attack>?").delay(".8:.1:.8").room(2).slow(8).cut(2),
|
||||
).reset("<x@30 [x*[8 [8 [16 32]]]]@2>".late(2))`,
|
||||
`// "acidic tooth" @by eddyflux
|
||||
setcps(1)
|
||||
stack(
|
||||
note("[<g1 f1>/8](<3 5>,8)")
|
||||
.clip(perlin.range(.15,1.5))
|
||||
.release(.1)
|
||||
.s("sawtooth")
|
||||
.lpf(sine.range(400,800).slow(16))
|
||||
.lpq(cosine.range(6,14).slow(3))
|
||||
.lpenv(sine.mul(4).slow(4))
|
||||
.lpd(.2).lpa(.02)
|
||||
.ftype('24db')
|
||||
.rarely(add(note(12)))
|
||||
.room(.2).shape(.3).postgain(.5)
|
||||
.superimpose(x=>x.add(note(12)).delay(.5).bpf(1000))
|
||||
.gain("[.2 1@3]*2") // fake sidechain
|
||||
,
|
||||
stack(
|
||||
s("bd*2").mask("<0@4 1@16>"),
|
||||
s("hh*8").gain(saw.mul(saw.fast(2))).clip(sine)
|
||||
.mask("<0@8 1@16>")
|
||||
).bank('RolandTR909')
|
||||
)`,
|
||||
];
|
||||
@ -32,7 +32,18 @@ const posts = (await getCollection('blog')).sort((a, b) => compareDesc(a.data.da
|
||||
<LeftSidebar currentPage={currentPage} />
|
||||
</aside>
|
||||
<PageContent>
|
||||
{posts.map((post) => <BlogPost post={post} />)}
|
||||
<div class="border-b-4 border-lineHighlight py-4">
|
||||
<h1>Strudel Blog</h1>
|
||||
<p>
|
||||
Welcome to the Strudel Blog, where we will keep you updated with the latest changes and things happening
|
||||
in the strudelsphere. You can subscribe to this blog using <a target="_blank" href="/rss.xml"
|
||||
>this rss link</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-8">
|
||||
{posts.map((post) => <BlogPost post={post} />)}
|
||||
</div>
|
||||
</PageContent>
|
||||
<aside class="fixed right-0 h-full overflow-auto pr-4 pl-0 pb-16 hidden xl:block" title="Table of Contents">
|
||||
<RightSidebar
|
||||
|
||||
19
website/src/pages/rss.xml.js
Normal file
19
website/src/pages/rss.xml.js
Normal file
@ -0,0 +1,19 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = (await getCollection('blog')).filter((p) => !p.data.draft);
|
||||
const options = {
|
||||
title: 'Strudel Blog',
|
||||
description:
|
||||
'The Strudel Blog will keep you updated with the latest changes and things happening in the strudelsphere.',
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
link: `/${post.slug}/`,
|
||||
title: post.data.title,
|
||||
pubDate: post.data.date,
|
||||
description: post.data.description,
|
||||
})),
|
||||
};
|
||||
return rss(options);
|
||||
}
|
||||
@ -4,6 +4,7 @@ layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { examples } from '../../examples.mjs';
|
||||
|
||||
# Welcome
|
||||
|
||||
@ -29,40 +30,13 @@ The best place to actually make music with Strudel is the [Strudel REPL](https:/
|
||||
- teaching: focussing on a low barrier of entry, Strudel is a good fit for teaching music and code at the same time.
|
||||
- integrate into your existing music setup: either via MIDI or OSC, you can use Strudel as a really flexible sequencer
|
||||
|
||||
## Example
|
||||
## Examples
|
||||
|
||||
Here is an example of how strudel can sound:
|
||||
Here are some examples of how strudel can sound:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`stack(
|
||||
// drums
|
||||
s("bd,[~ <sd!3 sd(3,4,2)>],hh*8")
|
||||
.speed(perlin.range(.8,.9)), // random sample speed variation
|
||||
// bassline
|
||||
"<a1 b1\*2 a1(3,8) e2>"
|
||||
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.superimpose(add(.05)) // add second, slightly detuned voice
|
||||
.note() // wrap in "note"
|
||||
.decay(.15).sustain(0) // make each note of equal length
|
||||
.s('sawtooth') // waveform
|
||||
.gain(.4) // turn down
|
||||
.cutoff(sine.slow(7).range(300,5000)), // automate cutoff
|
||||
// chords
|
||||
"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand')
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.note() // wrap in "note"
|
||||
.s('sawtooth') // waveform
|
||||
.gain(.16) // turn down
|
||||
.cutoff(500) // fixed cutoff
|
||||
.attack(1) // slowly fade in
|
||||
)
|
||||
.slow(3/2)`}
|
||||
/>
|
||||
<MiniRepl client:idle tunes={examples} />
|
||||
|
||||
To hear more, go to the [Strudel REPL](https://strudel.cc/) and press shuffle to hear a random example pattern.
|
||||
These examples cannot fully encompass the variety of things you can do, so [check out the showcase](/intro/showcase/) for some videos of how people use Strudel.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user