tl;dr: I've created a live-coding environment with Canvas, Web Audio and setTimeout, here's a demo: https://lambda.quest/?gist=8eb332073b3b29ace1e2855dfdbad98f

About the project

Lambda Quest is a live-coding environment in your browser.

When building Lambda Quest, I decided to go with native in-browser ES6 modules, without any compilation. It's really refreshing to just write modern JS and not have to deal with Webpack.

Scheme + Emscripten

The core of the app is a Scheme interpreter called Gambit. It's originally a C program that was compiled to JavaScript via Emscripten. It runs in a WebWorker and communicates with the main thread via postMessage.

To interact with Scheme, I've added the Monaco Editor. It's the same open-source editor that powers the VSCode. Whenever you edit the Scheme code, and it's syntactically correct, it will re-evaluate it live. So the results will be immediately visible on a Canvas.

Canvas

Speaking of Canvas, the rendering is fully asynchronous. Scheme puts Canvas method calls into an async queue. There's a requestAnimationFrame loop running in the background that picks up any pushes to the queue. This makes animations possible through things like `(sleep 0.5)`.

Web Audio support

In addition to Canvas, I've implemented Web Audio support. So you can live-code music now!

Adding Web Audio wasn't trivial because the Gambit interpreter is running in a Web Worker. So I have to send messages between the worker and the main JS thread to access any browser APIs.

Canvas was easy to implement, because it's mostly procedural calls like `fillRect`, `stroke` etc. So I'm just sending commands from Lisp to JS.

Web Audio, on the other hand, is about creating trees of audio nodes. E.g. you create an Oscillator node and connect it to a Gain node, and finally connect the Gain node to a Destination node (output). Also you can set parameters on each node (e.g. the frequency of an oscillator).

This all means that Scheme needs a way to reference created nodes. However, I cannot send a node directly in a message from JS. Only atomic values like strings and numbers. To circumvent this limitation, I've created a registry of nodes in JS which can be accessed by id.

Scheme, when calling a Web Audio method, provides an id to store the result in. Also if it passes an id in one of the arguments, JavaScript looks it up in the registry and makes the call on the corresponding node.

setTimeout as a Scheme macro

Web Audio is not fun if you can't program timed events for it. E.g. a sequence of notes, or a volume envelope that changes over time. So I had to make the Scheme code asynchronous.

Scheme, just like JavaScript, is single-threaded and "synchronous". It needs a second process to tell it when to run a delayed call to achieve asynchronous behavior.

I've implemented a macro – https://github.com/katspaugh/lambda.quest/blob/main/scheme/w... – (which is btw my first macro ever) to put a Scheme callback in a JS `setTimeout`.

Macros, for me, is such a mind-blowing thing. It was amazing to code one purposefully, to achieve a practical goal.

GitHub API and OAuth

You can save your Scheme creations as gists on GitHub. I've implemented GitHub OAuth with a serverless worker on CloudFlare. The worker, of course, is also written in JS. All my projects are hosted on CloudFlare btw, it's amazing.

Preact

Finally, I've used Preact and HTM for a React-like UI rendering. HTM is basically JS template strings that look like JSX and spit out DOM trees. Pretty neat, although a bit hard to edit.

Github

All the code is open-source (MIT) and can be read on GitHub: https://github.com/katspaugh/lambda.quest

Pull requests and feedback are very welcome. Thanks for reading!