Week 48: Smart Player with Strudel REPL

J.Gong

2025-11-19

5.34min

Week 48: Smart Player with Strudel REPL

I started this week trying to make some noise with Sonic Pi. It’s a fantastic tool, but we had a failure to communicate: it speaks Ruby, and I… well, I prefer JavaScript. 🙅‍♂️

After some digging, I stumbled upon Strudel. It’s a code REPL for music generation that brings the Tidal Cycles language right into the browser. The only catch? The official workshop is in German. Entschuldigung? 🥨

The “HarpTabs” Experiment 🧪

My initial brilliant idea 💡 was to take harmonica tabs from HarpTabs, map them to notes, and ask an AI to generate a playback script.

Here is the highly scientific conversion table I used to map hole numbers to notes:

Hole NumberBlow (Exhale) 🌬️Draw (Inhale) 😤
1CD
2EG
3GB
4CD
5EF
6GA
7CB
8ED
9GF
10CA

I fed this into the AI to generate a cover of “No Surprises” by Radiohead. Here is the code it spat out:

// "No Surprises" Melody
note(`<
  [d e g e e d c e g]
  [d e e d c f b]
  [b b c d e]
  [e g g e e d c e]
  [e g g e e d]
  [f c b b c d e]
  [e g g e e d]
  [c e g g e e e d c]
  [f e d c b b c d]
  [f e d c b b c d]
  [f e d c b b c d]
  [e d c e d]
  [e e e e e f]
  [a f f f f a e]
  [f e d c b b c d]
  [f e d c b b c d]
  [f e d c b b c d d]
  [e e e e f]
  [e e e e f f]
  [f e d c b b c d]
  [a b d a a]
  [a b d a g]
>`)
.s("piano") // Sets the sound to a piano
.slow(2)    // Slows it down to make the phrasing clearer (adjust as needed)

The result? Well… let’s just say Thom Yorke is safe. I’ve never heard this version of “No Surprises” before. 🙉

The Pivot: Building a Smart Player 🧠🎧

Realizing my career as a harmonica transcriber was over, I went hunting and found a fan-made repo collecting actually good Strudel scripts.

Since I was already messing around with vector search in my previous post about EntityDB, I decided to combine the two.

The Goal: Create a smart music player that understands natural language requests.

The Todo List:

  1. 🏷️ Label the songs (Let AI do the work).
  2. 💾 Embed the labels (Vector database magic).
  3. 🛠️ Add Tool Use (Teach the LLM to play DJ).

1. Labeling the Songs

I wasn’t going to listen to 50+ files and tag them manually. I asked Cursor to “hallucinate” (infer) descriptions based on the filenames. Here is the data structure:

{
    "songs": [
        {
            "url": "/strudel-songs-collection/acertainbuzz.js",
            "description": "Fizzy drone with a certain buzzing energy."
        },
        {
            "url": "/strudel-songs-collection/ameliewaltz.js",
            "description": "Gentle Parisian-styled waltz for an Amelie moment."
        },
        {
            "url": "/strudel-songs-collection/anniesroom.js",
            "description": "Cozy chamber groove inspired by Annie's quiet room."
        },
        {
            "url": "/strudel-songs-collection/anothersatellite.js",
            "description": "Shimmering orbiting arpeggios like another satellite."
        },
        {
            "url": "/strudel-songs-collection/appealingtovenus.js",
            "description": "Sultry pulse that feels like serenading Venus."
        },
        {
            "url": "/strudel-songs-collection/aztecchallenge.js",
            "description": "Tribal percussion sprint evoking an Aztec challenge."
        },
        {
            "url": "/strudel-songs-collection/bennington.js",
            "description": "Emotive rock textures nodding to Bennington's voice."
        },
        {
            "url": "/strudel-songs-collection/bigship.js",
            "description": "Slow-building pads that feel like steering a big ship."
        },
        {
            "url": "/strudel-songs-collection/bluemonday.js",
            "description": "Punchy electro beat paying homage to Blue Monday."
        },
        {
            "url": "/strudel-songs-collection/breakfastline.js",
            "description": "Bright morning loop perfect for a breakfast line."
        },
        {
            "url": "/strudel-songs-collection/budsandspawn.js",
            "description": "Organic pulses bursting like buds and spawn."
        },
        {
            "url": "/strudel-songs-collection/bugfromheaven.js",
            "description": "Glitchy chirps of a celestial bug from heaven."
        },
        {
            "url": "/strudel-songs-collection/bustybeez.js",
            "description": "Chunky bass swarm buzzing like busty bees."
        },
        {
            "url": "/strudel-songs-collection/byebyespirit.js",
            "description": "Airy farewell motif whispering bye-bye spirit."
        },
        {
            "url": "/strudel-songs-collection/cabinet.js",
            "description": "Boxy percussive knocks echoing inside a cabinet."
        },
        {
            "url": "/strudel-songs-collection/cadenza.js",
            "description": "Virtuosic flourish patterned after a cadenza."
        },
        {
            "url": "/strudel-songs-collection/changetempowitharrows.js",
            "description": "Instructional click track to change tempo with arrows."
        },
        {
            "url": "/strudel-songs-collection/cinghiale.js",
            "description": "Earthy rumble charging like a wild cinghiale boar."
        },
        {
            "url": "/strudel-songs-collection/clandeisiciliani.js",
            "description": "Folksy dance figure for clan dei Siciliani."
        },
        {
            "url": "/strudel-songs-collection/clubbed.js",
            "description": "Thick four-on-the-floor groove built for being clubbed."
        },
        {
            "url": "/strudel-songs-collection/disto.js",
            "description": "Overdriven lines dripping with distortion."
        },
        {
            "url": "/strudel-songs-collection/edenontheair.js",
            "description": "Floating pads like Eden broadcast on the air."
        },
        {
            "url": "/strudel-songs-collection/elpueblo.js",
            "description": "Festive percussive chant honoring el pueblo."
        },
        {
            "url": "/strudel-songs-collection/enjoythesilence.js",
            "description": "Minimalist vortex referencing Enjoy the Silence."
        },
        {
            "url": "/strudel-songs-collection/epicbiopic.js",
            "description": "Cinematic swells suitable for an epic biopic."
        },
        {
            "url": "/strudel-songs-collection/eversoclosely.js",
            "description": "Intimate pulses brushing ever so closely."
        },
        {
            "url": "/strudel-songs-collection/happybirthday.js",
            "description": "Classic birthday melody rendered in Strudel style."
        },
        {
            "url": "/strudel-songs-collection/happybirthdayramones.js",
            "description": "Ramones-flavored blast of Happy Birthday."
        },
        {
            "url": "/strudel-songs-collection/heymoon.js",
            "description": "Dreamy nocturnal groove waving hey moon."
        },
        {
            "url": "/strudel-songs-collection/humanperformance.js",
            "description": "Angular rhythms inspired by Human Performance."
        },
        {
            "url": "/strudel-songs-collection/hydraswap.js",
            "description": "Shapeshifting layers that swap heads like a hydra."
        },
        {
            "url": "/strudel-songs-collection/ilredelmondo.js",
            "description": "Regal motif saluting il re del mondo."
        },
        {
            "url": "/strudel-songs-collection/jitterbug.js",
            "description": "Snappy swing loop built for the jitterbug."
        },
        {
            "url": "/strudel-songs-collection/jitterbugreverse.js",
            "description": "Backwards jitterbug pulses for a reversed swing."
        },
        {
            "url": "/strudel-songs-collection/lovegoeson.js",
            "description": "Warm chords repeating as love goes on."
        },
        {
            "url": "/strudel-songs-collection/madeallup.js",
            "description": "Playful loop confessing it was made all up."
        },
        {
            "url": "/strudel-songs-collection/magicandecstasy.js",
            "description": "Sparkling arps mixing magic and ecstasy."
        },
        {
            "url": "/strudel-songs-collection/mammalschilling.js",
            "description": "Laid-back beat picturing mammals chilling."
        },
        {
            "url": "/strudel-songs-collection/markovchain.js",
            "description": "Generative blips wandering like a Markov chain."
        },
        {
            "url": "/strudel-songs-collection/mouthbreather.js",
            "description": "Heavy breathing bassline for the mouth breather."
        },
        {
            "url": "/strudel-songs-collection/mouthbreathercomplex.js",
            "description": "Extended version of the mouth breather complex."
        },
        {
            "url": "/strudel-songs-collection/nudelimprov1.js",
            "description": "Sparse nude-limbed improv sketch number one."
        },
        {
            "url": "/strudel-songs-collection/oddeven.js",
            "description": "Alternating clicks exploring odd-even patterns."
        },
        {
            "url": "/strudel-songs-collection/oh.js",
            "description": "Minimal exclamation loop simply saying oh."
        },
        {
            "url": "/strudel-songs-collection/oldmacdonald.js",
            "description": "Playful rendition of Old MacDonald."
        },
        {
            "url": "/strudel-songs-collection/omalley.js",
            "description": "Crooning shuffle tipping a hat to O'Malley."
        },
        {
            "url": "/strudel-songs-collection/pumpupthejam.js",
            "description": "High-energy throwback pumping up the jam."
        },
        {
            "url": "/strudel-songs-collection/pyramidsong.js",
            "description": "Swaying rhythm honoring Pyramid Song."
        },
        {
            "url": "/strudel-songs-collection/rhythmofthenight.js",
            "description": "Italo-disco sparkle reliving Rhythm of the Night."
        },
        {
            "url": "/strudel-songs-collection/satiesfaction.js",
            "description": "Satie-esque meditation chasing satisfaction."
        },
        {
            "url": "/strudel-songs-collection/shanghai.js",
            "description": "Glittering tones evoking neon Shanghai."
        },
        {
            "url": "/strudel-songs-collection/shedontusejelly.js",
            "description": "Alt-rock wobble referencing She Don't Use Jelly."
        },
        {
            "url": "/strudel-songs-collection/sparky.js",
            "description": "Bright staccato sparks from a sparky loop."
        },
        {
            "url": "/strudel-songs-collection/stacktricks.js",
            "description": "Layered sequences stacking rhythmic tricks."
        },
        {
            "url": "/strudel-songs-collection/strangerthings.js",
            "description": "Analog suspense channeling Stranger Things."
        },
        {
            "url": "/strudel-songs-collection/strudelman.js",
            "description": "Playful hero theme for Strudelman."
        },
        {
            "url": "/strudel-songs-collection/strudelwall.js",
            "description": "Solid chord wall baked from strudel layers."
        },
        {
            "url": "/strudel-songs-collection/strumtest1.js",
            "description": "First strumming test pattern."
        },
        {
            "url": "/strudel-songs-collection/strumtest2.js",
            "description": "Second strumming test with added motion."
        },
        {
            "url": "/strudel-songs-collection/strumtest3.js",
            "description": "Third strumming test exploring variations."
        },
        {
            "url": "/strudel-songs-collection/swimandsleep.js",
            "description": "Lulling tide letting you swim and sleep."
        },
        {
            "url": "/strudel-songs-collection/swimmingsnake.js",
            "description": "Slippery lines coiling like a swimming snake."
        },
        {
            "url": "/strudel-songs-collection/tablaturetest.js",
            "description": "Prototype picking figure for a tablature test."
        },
        {
            "url": "/strudel-songs-collection/tarantella.js",
            "description": "Whirling folk rhythm straight from a tarantella."
        },
        {
            "url": "/strudel-songs-collection/ventocaldo.js",
            "description": "Warm desert breeze suggested by vento caldo."
        },
        {
            "url": "/strudel-songs-collection/verminmangle.js",
            "description": "Gritty textures mangling unruly vermin."
        },
        {
            "url": "/strudel-songs-collection/veronicainecstasy.js",
            "description": "Dramatic synths placing Veronica in ecstasy."
        },
        {
            "url": "/strudel-songs-collection/veronicazigzag.js",
            "description": "Zippy zig-zag sequence tailored for Veronica."
        },
        {
            "url": "/strudel-songs-collection/vine.js",
            "description": "Looping riff curling upward like a vine."
        },
        {
            "url": "/strudel-songs-collection/waltzno2.js",
            "description": "Melancholic sway reminiscent of Waltz No. 2."
        },
        {
            "url": "/strudel-songs-collection/woodeneye.js",
            "description": "Rustic knocks peering through a wooden eye."
        }
    ]
}

2. Embedding the Labels

This part was pleasantly simple. EntityDB provides a straightforward insert API, so we just loop through our AI-generated descriptions and store them.

export async function loadPlaylist() {
    const playlist = await requestPlayList()
    const db = $db.get()
    if (!db) {
        throw new Error("Database not initialized. Call createDB() first.")
    }

    for (const song of playlist) {
        const key = await db.insert({
            text: song.description,
            metadata: { id: song.url }
        })
        console.log({key, song})
    }
}

3. React + Strudel = ❤️

Strudel doesn’t provide a React component out of the box (rude), so I had to MacGyver one. I created a wrapper that injects the strudel-repl custom element into a DIV.

import { useCallback } from "react";
import { $song } from "../stores/playlist";

export default function Strudel() {
    const onLoad = useCallback((elem: HTMLElement|null) => {
        if (elem) {
            const editor = document.createElement('strudel-repl');
            editor.setAttribute(
                'code',
                $song.get() ?? ''
            );
            elem.appendChild(editor);

            // Cleanup logic
            return () => {
                if (elem) {
                    elem.removeChild(editor);
                }
            };
        }
    }, []);

  return (
    <div ref={onLoad}>
    </div>
  );
}

The Result 🎉

The rest of the plumbing connects to the LLM logic from my previous EntityDB post. I can now ask for “spooky vibes” or “something for a Parisian cafe,” and the tool finds the matching Strudel script and plays it instantly.