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.
Press Run to observe audio.
Oscilloscope
Spectrum
Waveform: ±1, 20 ms. Spectrum: −100 to 0 dBFS, 4096-point FFT, up to 15 updates/sec. These plots show the mixed output after effects and master volume. Pause stops analysis while audio keeps playing.
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
Tetorica Shell — type help for commands.
Shell
- Enter
- Execute
- ↑ / ↓
- History
- Tab
- Complete path
- Ctrl+C
- Stop script
- Ctrl+L
- Clear output
Try js /scripts/example.js hello or play /index.js. Type help for commands.
Stop Script ends the shell script. Music stops with the toolbar STOP.
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)hzToBlockFnum(440)/pg.hzToBlockFnum(440)converts Hz to{block, fnum}forfm.setFrequency(channel, block, fnum). Optional second argument: chip clock in Hz (default 7670454), not the audio sample rate.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()
Go to Definition (F12 / Ctrl or Cmd + click) opens library declarations read-only. View implementation opens the corresponding JavaScript source.
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/basic/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")
Sound chips
const chip = await useSoundChip(name)reuses a chip until Stop. For YM2612, YM2203, and YM2610, it returns the default chip when selected; otherwise it creates an independent chip.const chip = await createSoundChip(name)creates a fresh independent chip routed through master FX.createSoundChip("nes", {fds: true})enables FDS. Usechip.pulse,chip.triangle,chip.noise,chip.dmc, andchip.fds; seeexamples/nes/.- Supported names:
ym2612,ym2203,ym2610,ym2608,rf5c164,gameboy,segapsg,ym2151,pwm,nes, anddx7. const dx7 = await useSoundChip("dx7")creates a 16-voice MSFA DX7 with the self-authored INIT SINE voice. Usedx7.noteOn(60, 100)anddx7.noteOff(60)with MIDI note numbers, without a channel argument.- Import a DX7 SysEx file into FILES, then
await dx7.loadSysex(await file("/presets/my-bank.syx", {type:"arrayBuffer"})). Choosedx7.selectVoice(0)fromdx7.voices. The internal rate is fixed at 44,100 Hz; there is nosetClock(). Seeexamples/dx7/dx7-init-sine.js. - Control additional chips through the returned object. Global
play()andwrite()target the default chip; Stop disposes additional chips. - Concurrent
useSoundChip()requests share initialization. Options other than an empty object are not supported.
YM2151 (OPM)
const opm = await useSoundChip("ym2151")reuses an independent eight-channel FM chip;createSoundChip("ym2151")creates a fresh instance.opm.setAlgo(0, 7)selects an algorithm;opm.setPan(0, true, true)enables both outputs. Channels are numeric0..7.opm.setOperator(0, 3, { mul: 1, tl: 32, ar: 31, rr: 15 })edits an operator. Indices0..3follow register orderM1, C1, M2, C2; these are OPM parameters, not YM2612 presets.opm.setNote(0, "A4")sets pitch;opm.keyOn(0)starts the envelope;opm.keyOff(0)releases it. Notes acceptC#0..C8or MIDI integers13..108.opm.setPitch(0, 0x4a, 0)sets raw key code and key fraction (0..63). The clock is fixed at3579545 Hz.opm.setNoise(true, 12)replaces CH8's last operator with noise (rate0..31);opm.writeRegister(register, value)accesses all registers, including LFO.opm.reset()resets this chip;opm.dispose()releases it. Openexamples/x68000/ym2151-tone.jsfor a complete voice setup.
Sega PSG (independent chip)
const chip = await useSoundChip("segapsg")reuses an independent Sega PSG;createSoundChip("segapsg")creates a fresh instance.chip.tone(0, { note: "C4", attenuation: 4 })starts a tone on channel0..2;chip.off(0)silences it.chip.noise({ type: "white", rate: "medium", attenuation: 8 })starts noise;chip.noiseOff()silences it.- Uses the same API as
psg. Bothreset()andresetAll()reset only this instance.dispose()releases it; Stop releases all additional chips. - Open
examples/genesis/segapsg-tone-noise.jsin FILES for an example.
YM2608 ADPCM-B sample loading
YM2608 ADPCM-B can also load decoded PCM or PCM/Float WAV with
await ym2608.adpcm.loadSample(source, {address: 0}).
It converts the sample, uploads memory and selects the range/rate; use
setVolume(), setPan() and keyOn() to play.
Open examples/pc98/ym2608-adpcm-load-sample.js in FILES.
RF5C164 PCM
const pcm = await createSoundChip("rf5c164")creates an eight-channel PCM chip. Physical channels areCH1..CH8.await pcm.loadMemory(bytes, address)loads raw RF5C164 BIN bytes from aUint8ArrayorArrayBuffer.await pcm.loadSample(source, { address, loopStart })decodes WAV/FLAC and returnsstart,loopStart, andstepforsetChannel(). Format support depends on the browser.- Sample sources may be a local URL, Blob, or encoded file bytes. Read FILES data with
await file("./sound.wav", { type: "arrayBuffer" })first. Converted samples must fit 64 KiB; no automatic resampling. await pcm.setChannel(ch, { start, loopStart, step, volume, pan: { left, right } })configures a channel. Start is 256-byte aligned; volume is0..255, pan values0..15.await pcm.setPitch(ch, step)changes pitch;await pcm.keyOn(ch)starts or retriggers playback;await pcm.keyOff(ch)stops it.pcm.reset()retains sample RAM;pcm.dispose()releases the chip.- Open
examples/genesis/rf5c164-sine.jsin FILES for an example.
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/basic/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