Code
Ctrl/Cmd+Shift+Space/I · Ctrl/Cmd+Space/IChanges are saved to this virtual TFI file automatically. Pan, LFO, AMS/PMS, AM and CH3 settings are for audition only; TFI does not store them.
FX Monitor
Run code containing liveFx to begin.
Blue: input · Orange: output. Fixed scales; 2048-sample Hann FFT. Observation stops when this tab or page is hidden.
Waveform
Spectrum
Generating Playground code with an AI? Open For AI documentation for type definitions and working examples.
pg
pgis the main playground helper namespacepg.play("C4", { channel: CH1, duration: 0.2 })plays one notepg.sleep(0.25)waits in secondssleepSamples(735)waits in VGM-style 44.1kHz sample unitspg.liveLoop("bass", async () => {})keeps a named loop alivepg.setBpm(120),await pg.beat(1),await pg.nextBeat(),await pg.tween(1, (t) => {})pg.setMasterVolume(1.5),pg.getMasterVolume()await pg.livePrepare("main-fx", async ({ fx }) => ...)reuses prepared live state across runspg.scale("Eb2", "majorPentatonic", 2),pg.chord("B2", "minor")pg.noteToBlockFnum("C4"),pg.noteLerp("B2", "E3", 0.5),pg.lerp(0, 100, 0.5)pg.choose(array),pg.cycle(array),pg.cycle("bass", array)pg.rand(),pg.rrange(0.2, 0.8),pg.randInt(0, 7)pg.stopLoop(name),pg.stopAllLoops(),pg.stopAll()
midi (YM2612 mode)
midi.output("tetorica-ym2612", { channel: CH1 })creates an FM part;"tetorica-sega-psg"uses the three PSG tone voices (not noise). These are internal outputs, not external Web MIDI ports.channelis a MIDI part: numeric0..15orCH1..CH16. Omitted channel defaults to CH1. By default the engine shares six FM voices or three PSG tone voices automatically; a MIDI channel does not pin a physical voice.await midi.enableSoundChip("tetorica-ym2612", { roundRobin: false })selects fixed FM voices: CH1..CH6 map to physical CH1..CH6, one note each; CH7..CH16 do not sound.roundRobin: truerestores automatic allocation. Changing mode stops FM notes. Specify the mode at the start of your script; this option is YM2612-only.await lead.setVoice(FM_PRESETS["two-op-bell"])selects an FM voice;await lead.loadVoice("./lead.tfi")loads TFI or VGI from FILES. With channel omitted,setVoice()applies to all MIDI channels.await lead.noteOn("C4", { velocity: 100 })andawait lead.noteOff("C4")start and release a note. Notes also accept MIDI numbers0..127.await lead.play("C4", { velocity: 100, duration: 1 })plays for one beat at the current BPM. UsePromise.all([...])for chords.await lead.pitchBend(0.25)bends the part (-1..1, center0);await lead.setPitchBendRange(2)sets the range in semitones (default ±2). Bend affects notes belonging to that MIDI part.await lead.cc(number, value)sends a MIDI controller, values0..127:7volume,10pan (FM left/center/right; PSG ignores pan),11expression,64sustain (64 or higher = down),120all sound off,121reset controllers,123all notes off.- Import MIDI generates editable JavaScript using these APIs. The example list includes MIDI fixed CH liveLoop + CC / Bend and MIDI auto chord liveLoop + CC / Bend.
Live coding
- Top-level
awaitis available; pressRunagain to replace named loops context(alsopg.context) keeps values between runs untilStoplivePrepare("name", setup)creates a resource once and reuses it on later runsliveCleanup(["bass", "drums"], cleanup)runs cleanup after those loops disappearonKeyboardPressKey("play-c", event => ...)andonKeyboardReleaseKey(...)register replaceable keyboard handlers
Files, imports, and console
- Use the
FILESbuttons to create, import, rename, and delete project files await file("./notes.txt"),await file("./data.json", { type: "json" }),await file("./sound.wav", { type: "arrayBuffer" })read project files relative to the running fileconst { bass } = await import("./lib/bass.js")dynamically imports a project JavaScript module- Imported modules may use
exportand nested relativeimport(); use dynamicimport()instead of a top-level staticimport liveFx(name, {context, process(input, output, state, context), resetState?})registers JavaScript FX after native FX. Same-name Apply preserves state; Stop removes it. Input/output are arrays of channel Float32Arrays. The synchronous process runs in AudioWorklet: use context instead of captured variables, and avoid unbounded loops. Exceptions/non-finite output bypass that effect.fx.updateContext(name, patch)sends a context update;fx.removeLiveFx(name)removes it. Commands are queued, not sample-timestamped. Seeexamples/livefx/live-fx-distortion.js.log(value),console.log(value),console.warn(value), andconsole.error(value)write to the Console tabtfiToPreset(await file("./bass.tfi", { type: "arrayBuffer" }))converts a 42-byte TFI file forfm.setPreset()vgiToPreset(await file("./bass.vgi", { type: "arrayBuffer" }))converts a 43-byte VGI file forfm.setPreset()(binary files requiretype: "arrayBuffer")
RF5C164 PCM
await createSoundChip("rf5c164") creates an additional PCM chip routed through master FX. Physical channels are CH1..CH8. See examples/pcm/rf5c164-sine.js.
await pcm.loadMemory(bytes, address) accepts raw RF5C164 BIN bytes (Uint8Array or ArrayBuffer). await pcm.loadSample(source, {address, loopStart}) decodes WAV/FLAC and returns start, loopStart and step for setChannel. Decoder format support depends on the browser. Files must fit 64 KiB after conversion; no automatic resampling. Source may be a local URL, Blob or encoded file bytes. FILES data must first be read into bytes.
setChannel(ch, {start, loopStart, step, volume, pan: {left, right}}), setPitch(ch, step), keyOn(ch) and keyOff(ch) return promises. Start is 256-byte aligned; volume is 0..255 and pan values 0..15. keyOn retriggers. reset retains RAM; dispose releases the chip. Stop releases all added chips.
dac (YM2612 mode)
- DAC plays unsigned 8-bit PCM through physical CH6, replacing its FM output while enabled. Values are
0..255;128is the neutral level. fm.setPan(CH6, true, true)routes CH6 to both speakers.fm.setDacEnabled(true)enables DAC;fm.writeDac(128)writes one value;fm.setDacEnabled(false)restores FM output.const start = beginSampleSchedule()gets the current loop cycle start in 44,100 Hz sample units.dac.schedule(start, entries)queues[offsetSamples, value]pairs relative to that start. Use/** @type {Array<[number, number]>} */for an initially empty entries array.- Offsets use
44,100 units = 1 second, regardless of the audio device sample rate. One DAC value every 4 units gives an 11,025 Hz PCM stream. Use audio-clock scheduling rather thansetInterval()for steady playback. scheduleWritesSamples(start, entries)queues[offsetSamples, port, register, value]writes. Use it to time DAC enable/disable along with PCM.await sleepSamples(length)waits in the same 44,100 Hz units.await dac.load(name, data)loads packed records from aUint8ArrayorArrayBuffer: each 5-byte record contains a little-endian uint32 sample offset followed by one uint8 DAC value. This is not a WAV file or a raw PCM byte array.dac.playStream(name, { atSamples: start })schedules a loaded bank relative to the run's sample-clock origin; it does not wait for playback to finish. Enable DAC and set CH6 pan separately.await dac.loadBase64(name, encoded)loads the same packed format from Base64;dac.scheduleBase64(start, encoded)schedules packed records directly.- Open
examples/dac/dac-byte-stream.jsin FILES for the 440 Hz sine-wave example. For ordinary decoded audio playback, usesamplebelow.
sample and noise
await sample.load("sonic-pi/ambi-choir")loads a built-in sampleawait sample.loadFile("/samples/hit.wav")loads audio from the projectawait sample.play("name", { gain: 0.8, playbackRate: 0.5, pan: -0.5, loop: true, fadeIn: 0.1 })sample.stop("name"),sample.stopAll(),sample.list()noise.create({ type: "pink", gain: 0.1, pan: 0 })creates a controllable noise voice; types arewhite,pink,brown,gray, andclipcontrol(voice, { gain: 0.2, cutoff: 1800, q: 0.8, slide: 1 })changes a noise voice smoothlyvoice.stop()stops one voice;noise.stopAll()stops every noise voice
stream
await stream.load("bgm", "./bgm.mp3")prepares a browser media stream from a same-origin URL; suitable for long audio such as background music. Use an actual served URL, not a FILES virtual path.await stream.play("bgm", { gain: 0.5, loop: true })waits for playback to start, not for the track to finish.- Sample reverse playback:
await sample.play("name", { playbackRate: -1 }). Use -0.5 for half-speed reverse; zero is invalid. For reverse playback, offset counts seconds from the end. Loop bounds remain relative to the original file start. - Playback options include
playbackRate,offset(seconds),pan, andfadeIn(seconds). stream.pause("bgm")pauses;stream.play("bgm")resumes without an explicit offset.stream.stop("bgm")stops and rewinds.stream.unload("bgm")releases the entry;stream.list()andstream.isLoaded("bgm")inspect loaded entries.- Output passes through the master FX by default. Playback controls remain on the main thread, including in Worker mode. This API is separate from
dac.playStream().
fm
fmis the rawYM2612Synthlayerawait play("C4")plays one notepg.fmpoints to the same YM2612 layer- Channels are
CH1throughCH6; logical operators areOP1throughOP4 fm.setPreset(CH1, pg.presets["one-op-basic"])applies one presetfm.setOperators(CH1, [[OP1, { tl: 20 }], [OP3, { tl: 24 }]])applies partial settings in array order (YM2612). Repeated operators are allowed. Fields within each entry followsetOperator()'s register order.fm.setOperator(CH1, OP1, { multi: 1, tl: 20, ar: 24, d1r: 8, d2r: 4, sl: 6, rr: 8 })- Operator ranges:
multi 0..15,tl 0..127(lower is louder),ar/d1r/d2r 0..31,sl/rr 0..15 fm.setAlgo(CH1, 7, 0)sets algorithm and feedback (0..7)fm.setPan(CH1, true, true, 0, 0)sets left, right, AMS (0..3), and PMS (0..7)fm.setFrequency(CH1, 4, 553),fm.keyOn(CH1),fm.keyOff(CH1)expose the YM2612 note trigger flowwrite(register, value)sends one YM2612 register write on port 0write(port, register, value)sends one YM2612 register write with an explicit portfm.write(port, register, value)sends one YM2612 register writefm.writeAddress(port, register),fm.writeData(value)follow the YM2612 address/data flowfm.read(offset),fm.readStatus(),fm.getIrq()expose low-level chip state where available
psg
psgis the raw Sega PSG (SN76489-compatible) layer, mixed into the same output asfmpg.psgpoints to the same PSG layerpg.psgTone(channel, period, attenuation)writes one PSG tone note helper for channel0..2pg.psgNoise(mode, attenuation)writes one PSG noise helper using raw mode0..7psg.tone(PSG1, { note: "C4", volume: 0.5 }); channels arePSG1,PSG2, andPSG3psg.noise({ type: "white", rate: "medium", volume: 0.25 }); rates arelow,medium,high, andtone3psg.off(PSG1)andpsg.noiseOff()silence tone and noise channelspsg.write(value)sends one raw PSG register byte (tone/noise/attenuation, same format asdocs/demos/psg.html)psg.reset()resets only PSGpsg.resetAll()resets both PSG and YM2612
fx
fxis the master FX helper layerpg.fxpoints to the same FX helper layerfx.gain(...),fx.eq(...),fx.filter(...),fx.delay(...),fx.distortion(...),fx.compressor(...),fx.gate(...)fx.bitcrusher(...),fx.wobble(...),fx.flanger(...),fx.chorus(...),fx.reverb(...),fx.slicer(...)- FX run in native C/WASM. Each type supports up to 8 instances; the routing graph supports up to 32 nodes. Modulation rate / phase is in beats. EQ uses fixed 200 Hz / 1 kHz / 4 kHz bands.
fx.setChain([filter, delay])replaces the master FX chainfx.setChain([fx.parallel(fx.branch(filter), fx.branch(delay, reverb))])creates parallel branchesfx.clear()detaches the current master FX chain and clears its audio historyeq.bass.set(...),eq.mid.rampTo(...),eq.treble.set(...)filter.cutoff.set(...),filter.cutoff.rampTo(...)
Note
Names passed to liveLoop(), livePrepare(), and
keyboard handlers identify replaceable live state. Reuse the same
name when editing a part, and use different names for independent parts.
Shared Playground URLs may include JavaScript source via ?src=.
That source is loaded into the editor and waits for an explicit
Run. During execution, common network APIs are disabled.
This is a small safety guard, not a full security sandbox.
- Choose examples from the
examples/folders in FILES, then press Run. ?src=...loads base64-encoded JavaScript source into the editor?cassette=...loads a small Base64URL-encoded cassette zip?tfi=...&tfi-id=bassloads one URL TFI preset with idbass?tfi=...,...with?tfi-id=a,bloads multiple URL TFI presets?vgi=...&vgi-id=bassloads one URL VGI preset with idbass