Log In  
Audio synthesis: plucked string and delay
by luchak
Simple smoke sim
by luchak
RP-8 1.0
by luchak
:: Unfold ::

This code uses 11% CPU according to Pico-8, but if I delete the v[0]=1 lines in the if false block it uses 7% CPU:

if false then
for i=0,1023 do
goto _

Other observations:

  • If I put the v[0]=1 lines in a for loop with a huge iteration count this code uses 7% CPU. (No bug.)
  • If I put this code into _update() or _draw() it uses 7% CPU. (No bug.)
  • Adding / removing v[0]=5 lines bumps CPU by roughly 3% per line.
  • Replacing v[0] with v[1] makes no difference.
  • Pico-8 does act on this CPU info, the contents of the if false block will affect whether Pico-8 visibly stutters if I add draw code and the non-executing lines push CPU over 100%.
  • If I add a table u and do u[0]=5 in the second loop the bug still happens, no change in behavior.
  • The number of top loop v[0]=1 assignments only seems to matter between about 8 and 16 - the CPU usage seems to saturate outside of that range.
  • If I make v local, this behavior persists, but it seems to take more v[0]=1 lines to trigger it.
  • I can produce this behavior on both 0.2.5e and 0.2.5c, @freds72 reports also being able to repro on 0.2.4c

Since this only happens outside of _update() (I think??) I assume this isn't a big issue for most carts, but for tweetcarts etc. it does seem fairly important.

P#125085 2023-01-31 19:30 ( Edited 2023-01-31 20:16)

:: Unfold ::

I've already posted this on the Discord and a couple other places, but, well, why not post it here too? Randomly generated snowflakes for the season.

Cart #snowflake_generator_3d-0 | 2022-12-24 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

P#122953 2022-12-24 21:07 ( Edited 2022-12-24 21:08)

:: Unfold ::

This behavior feels possibly intentional, but I thought I'd check here just in case. If you run RP-8 with load #rp8, on loading carts, RP-8 gives the message unknown extcmd: set_title. If you ask it to open its folder with extcmd("folder"), it fails with a similar message. Both commands work fine if I save the cart to a .p8 and immediately re-load it. This is on 0.2.5c.

It'd be nice if folks could use load #rp8 to run RP-8 - even if this functionality doesn't end up enabled from Splore, perhaps it could work when loading carts by ID from the CLI? Also, is there some way to work around this and avoid showing the error message if the extcmd call isn't going to work?

At this point I'm wondering if I'll need to just point to Itch as the canonical download source for folks to get reliable results. That wouldn't be the end of the world, but offering a load #rp8 alternative would sure be nice!

P#117735 2022-09-20 22:19 ( Edited 2022-09-21 19:15)

:: Unfold ::

I've been working on a song in RP-8, and I've recently modified RP-8 to not just write song files to the clipboard, but also to timestamped save files on the desktop. After a long session of working on the song, I tried to save, and got a message about there being too many printh files.

I wouldn't have lost too much work - the whole reason I have this many files is that I've been checkpointing frequently - and I was able to get the data out of the clipboard, but it was still frustrating. This was a very long session, Pico-8 had probably been open for 12 hours or more while I'd been working off and on. Creating so many save files would certainly be a problem if Pico-8 had only been open for a few minutes, but over 12 hours I feel like it's not unreasonable?

I'd love for Pico-8 to reset its number of files / amount of data limits after some period of time, even an hour or two would be fine for my purposes.

I'd imagine this isn't super high prio, but it would be very nice. As I get closer to releasing RP-8 I'm starting to think more about awkward situations and edge cases I'll have to explain in the docs, and this would certainly be one of them. The really awkward thing about this one is that it undermines trust (since the user feels like they're losing work), and it's somewhat difficult to get around in a nice-feeling way...

I guess I could have a save counter and add some sort of visual warning and/or exit when you've created the last save possible in a session? Still awkward but better than nothing.

P#114478 2022-07-17 19:37 ( Edited 2022-07-17 19:38)

:: Unfold ::

As usual with PCM audio, be careful of your volume, and if this doesn't play back well in your browser, try it in desktop Pico-8. Earlier versions had filter instability problems that could result in loud sounds showing up out of nowhere - I think these are fixed now (I've let this play for over an hour with no problems) but I can't 100% promise it won't happen again.

Cart #acid_jam_512-3 | 2022-06-22 | Code ▽ | Embed ▽ | No License

An entry to the Pico-8 512-Char Jam. Kick, hat, acid.

Jam entry
Itch page

Update 1: Moved the delay time slightly off the beat. Also saved a few characters thanks to @SmellyFishstiks, which let me thicken the oscilloscope line.

Update 2: Improved visualization and stability.

Update 3: More improvements to visualization and filter stability to avoid horrible loud sounds coming out of nowhere after letting the cart play for a long time. Hopefully the last update!

P#113246 2022-06-17 05:03 ( Edited 2022-06-22 21:08)

:: Unfold ::

Based on a request for help in the Pico-8 Discord, I ended up working on a rope/string/ribbon/chain/etc. simulation:

Cart #rope_sim_demo-0 | 2022-05-30 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

The behavior of the rope changes substantially depending on the gravity and drag settings.

This sim tries to model the object as inelastically as possible, although it's far from perfect in that regard. The rope here has 128 segments, which is very difficult to handle without (a) instability, (b) excess springiness in the rope, or (c) tons of damping. So i ended up having to write a slightly clever constraint solver (multigrid for n log n instead of n^2 performance), and I chose a step schedule that uses almost 100% CPU at 60fps. But if you use fewer segments and tune the solver differently, going down to 16 segments or so and using a less conservative step schedule, like one suggested in the cart comments, you can get the CPU usage closer to 5% at 60fps. That might be more suitable for inclusion in a game.

The underlying technique is Position-Based Dynamics, in which the constraint solver works directly on positions, and then velocities are updated accordingly at the end of the timestep. The nice thing about this technique is that hard constraints can be really easy to add - just push your vertices to where you want them! The bad thing about this technique is that friction or other forces can be hard to model since velocities aren't really considered in the constraint solution step.

I've included a ground plane here to demonstrate how easy it is to get the rope to collide with objects - just project the rope vertices out of any objects it's in! (Assuming your steps are small enough that the rope is still near the surface, your objects tend to be pretty smooth, etc. etc.)

Still, this was fun to write and I think it's kind of fun to play around with.

There are still some shortcomings here: static friction with the ground is not modeled, rope self-collision isn't modeled either, the solver still allows for noticeable elasticity in the rope at high velocities, and the drag model is very simplistic and doesn't attempt to model internal forces in the rope.

The default parameters give you something that feels vaguely rope-like, for something that feels more like a light ribbon or streamer, try drag=0.05 and gravity=0.01.

P#112468 2022-05-30 04:18 ( Edited 2022-05-31 23:01)

:: Unfold ::

I was thinking about what's keeping Pico-8 from being a popular platform for scientific computing and high-end DSP ... and I realized: I can't think of a single FFT library for Pico-8! That must be the reason. So I wrote one:

Cart #pfft-1 | 2022-08-30 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

There are some comments at the top of the cart code on usage, if you want to copy/paste this into a project of your own for some reason. It's not too slow: you can do about 400 length-256 real FFTs per second, which is enough to get up to some audio shenanigans. (See next cart.) You can use the left/right arrow keys to switch modes between DFT, complex FFT, real FFT, and DCT in this demo, but be aware that if you happen to switch to DFT mode - it is sloooooooow and you may have to hold a button down for a while to get out of it. (DCT is also slow but ~3x faster than DFT.)

If you want to try out a somewhat-contrived audio application, check out this cart:

Cart #pfft_demo-2 | 2022-05-29 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

If you drag in a short Pico-8 friendly raw PCM file (max ~6 seconds, use Defy to do the conversion) this cart will play it on loop, and boost/cut frequencies according to whatever curve you draw with the mouse. All the scaling is linear, which is not very natural (log scale for both frequency and amplitude would be more conventional), but you can still create some obnoxious notches and resonances. Give it a try!

Now that I think about it, the make_hann() and make_stft() functions from the demo cart might be useful to other cart authors as well - those take care of the overlap-add process for FFT resynthesis in a fairly transparent way. I've added a comment to make_stft() that should hopefully help with usage. It handles windowing and overlapping in a simple but fairly inflexible way. There is no attempt at any kind of fancy phase consistency logic. (The demo should probably be windowing the synthesized audio as well before summing it - maybe I'll fix that eventually.)


2022/08/29: added DCT and inverse DCT functions. These are very short, faster than DFT, and may be a good choice for some applications involving lossy compression of real-valued data.

P#112429 2022-05-29 05:04 ( Edited 2022-08-30 02:00)

:: Unfold ::

One of the things that's been driving me nuts while working on RP-8 has been how unstable the PCM audio output has felt. I can check (via logs or asserts) that, at 60fps, stat(108) is always above 256 and stat(1) never exceeds 0.9, and I'll still get occasional crackles or small time skips in the output. If I record the audio out with extcmd('audio_rec') these issues generally appear there as well - most often as small intervals of 0 samples. The one or two dropout cases I've been able to inspect in Audacity made it look like the dropout was in the low 10s of milliseconds range. I have not yet been able to track down a skip on the Audacity waveform display.

I've been trying to chase down the bugs in my code, but today I instead tried RP-8 - specifically the current BBS version, #rp8-2 - on two machines that aren't my normal development machine. It was rock solid. Flawless. (Or, well, nearly flawless, just heard a dropout on the Intel Mac after leaving it running for 15 minutes, but the ARM Mac shows problems much earlier and more frequently than that.) My main development machine is an ARM Mac, and the two other machines I tried today were a Windows box and an Intel Mac. The only conclusion I can draw right now is that there's something wonky about Pico-8's PCM audio out on ARM Macs that causes it to skip and/or drop frames.

These issues will happen even what feels like it should be easy for the machine to handle - no other CPU-intensive apps running, Pico-8 is the foreground app, etc. I have not heard these problems with Pico-8's builtin music and SFX. I'm also seeing parallel issues on the web: I thought Pico-8's PCM output was super crackly on all Mac browsers, but RP-8 runs fine in Chrome on the Intel Mac I'm trying out right now.

If there's more information I can give, I'm happy to try to help debug this!

edit: Realized I was behind on OS patches - moved up to 12.3.1 and I'm still definitely getting skips forward, though mystery dropouts seem perhaps rarer? Browser audio has not improved. Not sure how much here is under Pico-8's control, but it's certainly experiencing more audio issues with Pico-8 than with anything else I use.

edit 2: managed to get a skip captured with extcmd - hard to determine exactly how much got sliced out, but it was a small fraction of a kick drum hit.

P#110383 2022-04-18 04:00 ( Edited 2022-04-19 03:10)

:: Unfold ::

Hello! I thought I'd try to (slightly) clean up and post some utility functions I've found useful while working on RP-8. Maybe they'll be useful to others, maybe not. Let's see!

Cart #rp8utils-0 | 2022-04-16 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

There are three possibly-useful functions here, plus their support code. Note that in the name of token conservation, all of these functions have essentially no error checking and can blow up in various exciting ways if you feed them bad data (or if they have bugs). The code also isn't the cleanest - maybe I'll tidy it up eventually, but I don't think it's completely unreadable right now. Anyway, we've got:

  • stringify(), 114 tokens. This serializes structured data into a string - RP-8 uses this to save songs. Escapes binary. Both the string format and the binary escaping are completely nonstandard, but the string format at least looks vaguely similar to Lua literals.
  • parse(), 286 tokens (can be cut to 246 if you don't want eval). This takes a string and transforms it into structured data. RP-8 uses this to load songs, as well as to set up some of its internal data. Uses the same weird format and binary escaping as stringify(), although it also supports some variations and syntactical sugar. You can probably cut more tokens if you don't need to support large input strings or binary unescaping.
  • eval(), two versions - 428 tokens for interpreted, and 556 for "compiled", plus you need parse(). (Very loose use of the word "compile" here...) This evaluates a script in a vaguely LISP-formatted mini-language with Lua-ish behavior, and returns the result. RP-8 uses this to save tokens by encoding bulky logic that doesn't need to run fast, like UI init. This can help save tokens. Note that the token costs here are pretty squishy, since they depend on what builtins you want to define.

While this eval may help you save tokens, if you're really serious you should probably consider external build tools and bytecode. (For more on this, check out what @carlc27843 did with Picoscript in his unbelievable Nebulus cart.) RP-8 uses a condensed version of the interpreted eval - 382 tokens, but that small token savings causes some questionable behavior with multivals and closures, so I'm posting something better behaved, if less well tested, here.

The cart contains these three functions, some tests for the two versions of eval (none for parse or stringify, sorry), and a small and highly artificial performance comparison between the two versions of eval and native Pico-8 Lua code.


Here is a real-world example of parse()-formatted data, including using backticks to embed data from eval():

  tl=`(timeline_new $default_patch),
  pat_patch=`(copy $default_patch),

And here is an example of eval()-executable script:

(set paste_state (fn ()
 (audio_wait 2)
 (let pd (stat 4))
 (if (not (eq $pd "")) (seq
  (set state (or (state_load $pd) $state))
  (@= $seq_helper state $state)

... which is equivalent to:

 local pd=stat(4)
 if pd != "" then
  state=state_load(pd) or state

Script usage

The script has mostly LISP-ish syntax, though the behavior is more Lua-ish, including (hopefully correct) support for multivalues in function argument lists and returns.

The biggest syntactical quirk is that you must prepend the character $ to each variable name you want to be looked up instead of treated as a literal string. Since the parser operates independently of eval and does not pass any side information about the strings it finds, the alternative (all strings are treated as variable lookups) would require double-quoting of some strings, like "'hello world" or (' "hello world") ... I found this even more distasteful, so the $ prefixes are the fix for now. The one place where $ is not required is function invocations: you can't call a string, so it's clear that a lookup is required. If you find this confusing, I don't blame you, but perhaps some of the tests in the cart will make things clearer?

Possibly-incomplete list of script forms/builtins/keywords/whatever (I'm not a LISP person...):

  • (e1 e2 ... en) - if e1 evaluates to a function or the name of a function, call that function with the values of e2 through en as its arguments. For example, (print "hello world" 0 0 8) will print "hello world" in red. The argument evaluation also respects Lua's multival semantics, or at least it mostly should.
  • (fn (a1 a2 ...) e1 e2 ... en) - define a function. The args list is implicitly quoted. The function returns the value of en, the last expression.
  • (seq e1 e2 ... en) - evaluate expressions sequentially and return the value of the last one, en.
  • (' e1) - quote a value. e1 will be returned literally, without being evaluated at all. Use this to keep a table from being interpreted as an expression.
  • (if e1 e2 e3) - if e1 evaluates to a truthy value, evaluate and return e2, else evaluate and return e3.
  • (for e1 e2 e3) - executes for i=e1,e2 do e3(i) end
  • (set e1 e2) - sets the global named by the value of e1 to the value of e2. (Maybe a better name for this one?)
  • (let e1 e2) - sets the local variable named by the value of e1 to the value of e2. Note that this behaves like Lua's local assignment and not like let in many LISP dialects.
  • (@ e1 e2) - property access. Returns e1[e2]. There is an alternative form (@ e1 e2 e3) that returns e1[e2][e3].
  • (@= e1 e2 e3) - Sets e1[e2]=e3. Does this name make any sense with set and let? No, it does not.
  • (~ e1 e2) - returns e1-e2. Uses ~ instead of - because - looks like a number to the parser.
  • (cat e1 e2) - returns e1..e2
  • (len e1) - returns #e1
  • +, *, eq, gt, or are all present. Many other operators and keywords are missing. (There's no and, no while, etc.) They're not hard to add if you need them! If you care about tokens you should also delete anything you're not using.

What is this "compiled" nonsense?

Ok, yeah, I maybe should have a better word for this - but I don't. Essentially, the script can be evaluated in two different ways.

First, it can be interpreted straightforwardly by walking the parse tree on each execution, dispatching to different functions or builtin operators as required. This can be done in relatively few tokens (especially if you ditch multival support), and mostly doesn't care what's a builtin vs. an external function.

Second, it can be executed in two steps - first, the parse tree can be walked to produce a closure. This closure invokes other similar closures, each of which takes a dictionary of locals as an argument. The reason I've been using the word "compile" for this step is that it is a transformation on the code to a new form based only on its statically observable properties, with no knowledge of runtime values. The second execution step is then to invoke the top-level closure (this may be done many times with different locals).

The "compiled" method was motivated by @carlc27843's Picoscript, I definitely wouldn't have thought to try it otherwise.

This second method is much faster than the naively-interpreted version - about 4x as fast in the small test that's in this cart. How much faster it might be in any other case depends on how much the script can use builtins vs. function calls. Function call overhead will be very similar - most of the arg and return packing/unpacking logic is essentialy identical. So if you're mostly just dispatching function calls, both versions should perform about the same, however, if you can mostly stick to builtins, you could potentially see bigger speedups.


If you have clever speedups, bug reports, bug fixes, or ways to save tokens, let me know! There have to be all kinds of horrible bugs lurking in here.


The script eval functions are larger and slower than the ones I use in RP-8, in order to give them more consistent behavior with regard to variable scopes and multivals. If you're feeling adventurous you can modify the code to take some shortcuts here.

P#110311 2022-04-16 10:00 ( Edited 2022-07-16 21:33)

:: Unfold ::

RP-8 is a synthesizer and groovebox inspired by Propellerhead Software's classic soft synth, ReBirth RB-338. It has everything you need to make entire tracks: two paraphonic synthesizers, a drum machine, a pile of effects to process your sound, and an integrated song mode sequencer to pull everything together. The audio is lo-fi, 8 bits at 5.5kHz, giving the output a distinctively crunchy sound.

If you're nostalgic for the early era of soft synths, a fan of minimalist computing, an acid squelch connoisseur, or just someone who likes to discover new sounds: I made RP-8 for you.

You can find this cart, including native builds, at https://luchak.itch.io/rp8, or try it out below. Note that this cart works much better natively than on the web - see the Web section under Compatibility Notes below for more info.

Cart #rp8-21 | 2023-03-05 | Code ▽ | Embed ▽ | No License


See the manual, which includes a step-by-step tutorial. RP-8 also has in-app tooltips (enable with T) and a hotkey reference / help screen (view with H).


  • 2023 March 4 (1.0) - much more versatile foldback distortion, new demo song, finished docs (now using mkdocs instead of pandoc), tons of sound adjustments and tuning.
  • 2022 September 19 (beta 9) - antialiased oscillators, toned down filter harshness, file menu, "paste sound", tuning changes.
  • 2022 September 11 (beta 8) - vastly improved synth filters, secondary drum sounds, help screen, more hotkeys, song naming, paste-to-control, overdrive bias -> overdrive shape, many tweaks and optimizations. (beta 8b) Updated user guide, next/previous bar hotkeys.
  • 2022 August 6 (beta 7) - overdrive bias control, improved kick decay range, improved hat sound, performance optimizations. (beta 7a) Toasts and UI bugfixes. (beta 7b) Visual improvements, better FM ratio knob scaling, better save compatibility, more hotkeys.
  • 2022 July 21 (beta 6) - lots of constant tweaking and sound adjustments. More filter overdrive, more low end, punchier kick, lots more.
  • 2022 July 17 (beta 5) - removed synth attack knob since it's not being used, reduced low cut on the synth oscillators. (beta 5a) Tweaked overdrive gain compensation. (beta 5b) Fixed page button.
  • 2022 July 15 (beta 4) - removed sample loading and playback, save song files to both desktop and clipboard, drop song file to load, improved synth knob graphics, variable pattern lengths (with awful UI), 2-op FM drum track (control ratio with tune knob, control pitch with chromatic drum sequencing), individual drum track FX bypass, chromatic drum sequencing, second synth oscillator with chromatic sequencing.
  • 2022 April 25 (beta 3) - better handling of changing delay time, level compensation for filter resonance, drum sound tweaks, minor UI improvements, early draft of a manual!
  • 2022 April 16 (beta 2) - fixes to overdrive, output saturation, and the synth filters. Filter resonance goes much higher now, overdrive kicks in more smoothly, and output saturation is gentler.


  • Two vaguely 303-inspired monosynths
  • One vaguely 808-inspired drum machine
  • 16 step pattern-based sequencer with slide and accent steps
  • Pattern mode for real-time tweaking and song mode for recording your work
  • Almost every control on the screen can be automated in song mode
  • Per-synth overdrive insert effect
  • Tempo-synced delay with 16th, triplet, and dotted-16th lengths
  • Routable pattern-controlled lowpass/bandpass filter (like ReBirth's PCF)
  • Master compressor to get the most out of your 8 bits of dynamic range
  • Soft saturation on the output in case you try to get too much dynamic range
  • Audio export
  • Save/load functionality using the system clipboard (doubles as an easy checkpoint feature)
  • Tooltips for most controls (can be turned on/off)
  • Pico-8's PCM output filter is disabled so you can pretend that you have high frequencies

Known Issues

  • Occasional sound dropouts on M1 and M2 Macs, especially when running in the background. This appears to be a PICO-8 issue.
  • Rarely, when re-enabling sequencing on a synth, a slide may not trigger correctly or a non-slide note may be played as a slide.

Compatibility Notes


Most features still work, but pointer lock does not (so large knob movements are difficult), audio recording time is limited, and some browser/platform combinations may have very glitchy audio. On some browsers PICO-8 may interpret pasted documents as keypresses instead of loading them into its clipboard buffer.

Future RP-8 Versions

Future versions may tweak the sound in various ways - if preserving your songs exactly is important to you, please keep your old RP-8 versions around. I'll try to avoid making major changes, though.


... to the #music-sfx channel on the PICO-8 Discord, and @packbat in particular for testing, feedback, discussion, and encouragement. And to @thisismypassword for the essential-to-this-project shrinko8 minifier.

P#109822 2022-04-07 09:01 ( Edited 2023-03-05 13:56)

:: Unfold ::

(spoiler: turns out I was scribbling over memory I shouldn't have been ... including the RNG state)

I think I've discovered a strange issue with rnd() and mouse position:

Here, I'm just playing a snare hit over and over again. The snare is created by mixing a sine wave with noise, where the noise is generated by rnd(). (With separate envelopes applied to each component.) Notice that every time the mouse exits the top of the screen, the character of the sound changes, becoming more tonal.

I am virtually certain this is because rnd() is doing something strange. If I modify my audio chunk synthesis function (runs ~1x/frame, generating ~100 samples) to put srand(any_constant_value) at the beginning, I'll get the tonal effect every time. If I instead put this at the beginning:

function audio_dochunk()

... then I never get the tonal effect, which is the result I would expect. I have confirmed that this does not happen when the mouse exits the screen in any other direction.

I've attached the cart for reference. Not sure if this'll work on the web - I'm seeing this on a Mac.

Cart #rp8-0 | 2022-03-30 | Code ▽ | Embed ▽ | No License

(Also, web PCM is always wonky for me, and drag controls here turn on mouse capture, which also appears to not work on BBS embeds? So knobs etc. won't work unless you download and run locally.)

P#109395 2022-03-30 06:00 ( Edited 2022-03-30 06:38)

:: Unfold ::

This follows the classic Stable Fluids paper by Jos Stam. Use your mouse.

Cart #simple_smoke-12 | 2022-01-13 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

A few things that would make this better:

  • More optimization
  • Higher resolution (if possible)
  • monotonic cubic interpolation for advection (likely too expensive)
  • MAC grid (NOPE - saves a lot of time to backtrace all quantities from cell centers instead. A MAC grid would mitigate some of the boundary artifacts here, but it's just too slow.)
  • vorticity confinement (expensive, but probably the next most important thing)
  • better smoke vis - bilinear interpolation? (almost certainly too expensive)
  • higher resolution for smoke than for velocity?
  • other forces: gravity / heat / etc.
  • better code quality (it's kind of a mess, sorry)


  • Alternating Gauss-Seidel direction
  • Control number of pressure iterations with left/right
  • Some optimizations thanks to @freds72 (I'm loving the hack for newgrid)


  • @freds72 pointed out a few more optimizations and bugs (thanks!)
  • Merged all advection together and integrated density decay - saves tons of CPU.
  • Got rid of velocity diffusion - this also saved lots of CPU.
  • Lots more micro-optimizations (pre-multiply dt, split up multiple assignments, treat fg/bg colors as a single unit, etc.)
  • Increased resolution due to all the optimizations! Formerly, at 30fps, this could do 20x20 at 9 pressure iterations. Now it can do 24x24 at 10 iterations.
  • Smoke gets introduced a little slower and fades a little slower.


  • Sacrificed a little CPU (and a pressure iteration) for some tracer particles.

V0.4 (not posted yet)

  • More verbose CPU time breakdown if desired
  • Unroll pressure loop 4x to save about 6% CPU

Bonus GIF because it's neat: two vortex pairs approach each other, collide, vortices re-pair and move to the sides.

P#104799 2022-01-11 06:34 ( Edited 2022-01-15 03:10)

:: Unfold ::


A demake of a software groovebox from decades past...

Pattern mode is nearly complete! There's a mixer section, pattern storage, and a few editing tools. Good thing, too, since I think I'm coming up at the limits of what the Pico-8 CPU can reliably handle.

WARNING: Be careful if you play this cart on the web! Audio playback performs poorly on some OS/browser combinations and you may get loud crackling or popping noises. Turn your volume down before playing for the first time. If you have browser trouble, try it in native Pico-8. Or in a forthcoming native app export that I'll probably put up somewhere!

Cart #pb0x-4 | 2022-01-03 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA


  • Arrow keys select buttons/knobs/etc.
  • Z/X change control values. (Z is down, X is up)
  • The autorepeat rate speeds up while you hold a button down.
  • Step buttons for drums: gray is off, red is an accented hit, yellow is a soft/unaccented hit.
  • Step buttons for synths: gray is off, red is a normal note, yellow is a slide. Bright red/yellow are accents.
  • Note buttons: the little dots indicate octaves.
  • You can save/load patterns to/from the clipboard in the pause menu. There's basically no error checking so you can easily crash/freeze the cart this way if you load in a misformatted or invalid pattern. Also, this seems to not work in the web player.
  • You can also clear the sequence from the pause menu.


More editing features are sorely needed, and the device enable toggles (mute buttons or pattern enable? not sure) are not yet implemented. Otherwise, pattern mode is more or less done, leaving me with around 3k tokens for song mode and parameter automation.

The code is by and large a complete mess, though the sound driver is maybe of tolerable quality.

What's missing:

  • Tuning: sound quality, parameter ranges/scaling and synth accent behavior need help too.
  • Copy/paste on PiRC
  • Device mute or pattern enable toggles (not sure which one to do)
  • Probably some other editing tools: copy/paste notes only, clear single device only, etc.
  • Highpass filter on delay feedback? (if CPU allows)
  • Highpass filter on overdrive input for drums? Overdrive on the drums isn't super useful right now.
  • Master HP/LP filter or selectable filter insert. Only if CPU allows. Even then, I'll be lucky to squeeze 1 in, doing this per-part is extremely unlikely. Or perhaps a master high/low shelving EQ? ... but were would the controls go? Or, wait, what if I scrap overdrive for drums (since it sounds awful there) and do a filter on drums only? Synths already have a filter, after all.... It could just be a simple non-oversampled biquad, hopefully I have at least enough CPU for that.
  • Song mode and automation (the big one)

Please do not expect saved patterns to work at all in future versions.



  • Initial version


  • Added second synth
  • Better sequence loaded on start
  • Added clear sequence option to pause menu


  • Added drum machine (BD/SD/CH only)
  • Master compressor
  • Mockup for mixer/transport/pattern selection UI
  • Various tweaks and fixes


  • Finished drum machine - PiRC
  • Fixed compressor somewhat
  • Added mixer with overdrive (insert) and delay (send) per part
  • Removed overdrive/level controls from devices (those are on the mixer now)
  • Tempo, shuffle, and basic transport controls
  • Delay time and feedback controls
  • Reworked input handler to save CPU
  • Copy/paste pattern controls on PBL (still needed on PiRC)
  • Transpose pattern control on PBL
  • Increased max key repeat rate to 60Hz
P#104006 2022-01-01 09:58 ( Edited 2022-01-04 03:14)

:: Unfold ::

I just learned about the PCM interface, so I thought I'd do a little adventure into sound synthesis.

This cart is an experiment in building up an audio system that can run dataflow graphs built up from smaller generator and effect components. Here, I've used it to generate some plucked string sounds, mix those together, and then apply some delay to the result. The audio system needs some work and probably should implement audio processors as objects instead of coroutines (as coroutines it's annoying to send control signals once a processor has been created), but it's still kinda fun.

The code has a bunch of other areas that could use cleanup too, but coroutines vs. objects is probably the one big design issue that should get sorted out.

The one big audio-system-side thing that really helped here was trying to generate at least 128 samples per frame so long as I wasn't too far ahead of the audio system's needs. This really helped smooth out maximum CPU usage, since without that tweak I was sometimes generating 256 samples and sometimes 0.

On the plucked string side, with such a low sample rate it was absolutely necessary to include the tuning allpass filter in the delay loop. I've also included decay stretching as well as decay shortening ... yes, they're sort of at odds, but mixing them gives you a range of different sounds. I haven't done any pick direction/position filtering, string stiffness, bridge effects, sympathetic vibrations, dynamics filtering so louder notes are brighter, etc. But those could all be done here if anyone were so inclined!

The cart works well on my machine, but I'm very curious about whether it will sound okay on the web. I've tried to keep the volume down, but please be careful about your levels when you hit play.

Cart #strings_and_fx-1 | 2021-12-20 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

As for the "composition", I started with a standard guitar tuning, but each string loops through a sequence of interval offsets from that tuning, advancing one step each time it gets hit. Not my favorite but it sort of works, I think.

Update: I added some ways to tweak the sound (stretch and damp) as well as the speed of the balls (dt). Up and down arrows select parameters, left and right change them. It's definitely more interesting this way.... For stretch and damp - low stretch values kill high frequencies faster, whereas low damp values tend to reduce all frequencies equally.

P#103133 2021-12-19 22:33 ( Edited 2022-06-22 15:17)

:: Unfold ::

Cart #notepool-0 | 2021-12-11 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

Here's a little toy that makes sound. Balls bounce around the screen and make different noises when they collide with each other. You can nudge the white ball around with the arrow keys. I tried to add some very minor lighting effects on the balls and to make the sounds respond to the force imparted on each collision.

The code is, of course, a mess.

P#102373 2021-12-11 06:43

:: Unfold ::

Cart #44846 | 2017-10-02 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

I started playing around with the shallow water equations and got to something that's maybe a little bit fun. Arrow keys make waves, Z switches boundaries between (approximately) open and reflecting. My implementation mostly follows https://graphics.ethz.ch/Downloads/Publications/Tutorials/2008/Mue08/coursenotes.pdf .

Current limitations/known bugs:

  • Dissipates energy rapidly, although you'll probably only notice this if the boundaries are reflective.
  • No handling of dry regions. Weird things may happen if depth goes to 0!
  • Weird oscillations around both (nearly-)dry regions and shocks/big elevation changes. Not sure yet how much of this is attributable to bugs vs. limitations of the discretization I'm using.
  • Liquid volume is not conserved. Not really a meaningful concept with open boundaries, but pretty noticeable with reflective boundaries.
  • No wave breaking etc. Shallow water equations are a heightfield model and I don't think I'll get around to full liquids on PICO-8 any time soon. :)

I hope this cart is fun to play with! I'm hoping to fix a few of the items above, clean up the graphics a bit, and maybe find a way to build a little bit of a game around these dynamics.

P#44847 2017-10-02 04:09 ( Edited 2017-10-02 20:21)

:: Unfold ::

Cart #39954 | 2017-04-26 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

Started playing around with 2D sims - fluids seem hard to do at reasonable resolution given CPU constraints, but waves are just fine. More natural boat control and bilinear interpolation for wave rendering (more detail! less blocky!) both might make this more fun.

I'm also wondering if there's a racing game here where you can interact with the waves and they aren't just part of the scenery.

P#39955 2017-04-26 06:06 ( Edited 2017-05-08 05:35)

:: Unfold ::

Cart #39927 | 2017-04-25 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

A little cloud-based audio toy. Move your cloud around for different sounds. Would love to know what might make it more fun!

P#39928 2017-04-25 02:20 ( Edited 2017-04-25 09:06)

:: Unfold ::

Cart #39846 | 2017-04-23 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

This is a little fire experiment I've started playing with. It uses basically the same lookup table setup as https://hackernoon.com/pico-8-lighting-part-1-thin-dark-line-8ea15d21fed7 , although that may not have turned out to be absolutely neccessary.

What I'm happy with:

  • controls
  • performance
  • overall look

What could use work:

  • sound could respond to player actions
  • smoke could drift a little more interestingly

What might be fun to add:

  • setting other things on fire
  • scorch marks or some other permanent trace

Thoughts are more than welcome!

P#39847 2017-04-23 05:33 ( Edited 2017-04-23 09:33)

Follow Lexaloffle:          
Generated 2023-03-30 10:37:45 | 0.124s | Q:77