Crash-tan
Web is now free free! (1.61.0)
Crash BASIC 1.61.0 Release Notes
written by Crash-tan
An instrument is now something you write. The whole synth — every layer,
envelope, filter, LFO, FM operator, sample and wavetable the Instrument Editor
can set — has a block in the language, a statement that edits one property
while the game runs, a function that reads it back, and a way to save what you
built as a .cbi that the IDE opens.
And one thing for everyone, at the end: the web player no longer shows the
splash. Any game, any tier, anywhere it is embedded.
INSTRUMENT … END INSTRUMENT
Until now an instrument came from exactly one place: instruments.cbi, authored
in the IDE and loaded at startup. If you wanted a sound the editor didn't have
open in front of you, you left the game to go make it. Now:
INSTRUMENT 40 NAME "FM Bass"
VOLUME(0.9)
ENVELOPE ATTACK(0.001) DECAY(0.2) SUSTAIN(0.45) RELEASE(0.06)
LAYER SINE VOLUME(1)
LAYER SINE VOLUME(1) FMTARGET(0) FMDEPTH(1.4) FEEDBACK(2.5)
FMENVELOPE ATTACK(0.001) DECAY(0.1) SUSTAIN(0.55) RELEASE(0.05)
END LAYER
END INSTRUMENT
PLAY "I40 o2 l8 c c g c"An instrument is a stack of layers under one shared modulation shell. EachLAYER is a waveform, a WAV sample or a wavetable at its own volume, detune
and octave — or, with FMTARGET, a modulator of another layer instead of a
sound of its own. ENVELOPE, FILTER, FILTERENVELOPE, VIBRATO, TREMOLO,PITCHENVELOPE, VOLUME and PAN wrap the whole stack. A LAYER that needs
sub-declarations opens a body and closes with END LAYER; one that doesn't
fits on a line.
Numbers 0 to 66 are the built-ins. Declaring one of those replaces it for this
program; any higher number is a new instrument. It is the same model a .cbi
uses, and the two meet in the middle: a block can adjust an instrument that
came from instruments.cbi, and nothing you do in code rewrites that file.
Only what you mention changes. A block that sets just ENVELOPE on
instrument 5 keeps every layer, filter and LFO the built-in had — it is an
edit, not a redefinition:
INSTRUMENT 5
ENVELOPE ATTACK(0.25) DECAY(0.2) SUSTAIN(0.7) RELEASE(0.4)
END INSTRUMENTA block that lists LAYER lines replaces the layer stack outright, which is
what makes running the same block twice safe. A level restart does not double
the layers.
The full modifier list is under INSTRUMENT in the reference. Sampled and
wavetable layers look like this:
INSTRUMENT 12 NAME "Strings"
ENVELOPE ATTACK(0.2) DECAY(0.2) SUSTAIN(0.85) RELEASE(0.5)
LAYER VOLUME(1)
SAMPLE "wav/strings.wav" BASENOTE(48) LOOPMODE AUTO PITCHMODE STRETCH
END LAYER
END INSTRUMENT
INSTRUMENT 30 NAME "Supersaw"
LAYER VOLUME(1)
WAVETABLE "wt/saw.wav" POSITION(0.35) MORPH CROSSFADE
UNISON VOICES(7) DETUNE(24) BLEND(0.8) SPREAD(1.0) PHASERANDOM(0.5)
END LAYER
END INSTRUMENTSAMPLE names a file in your game folder, not a LOADWAV handle — that is
what a .cbi stores, and it is what makes the instrument saveable. A tip for
anything with a baked-in decay, a piano note say: PITCHMODE STRETCH shifts
the pitch and leaves the length alone. RESAMPLE speeds the recording up
along with the pitch, so a note an octave up dies twice as fast.
Every value is an expression
This is the part that makes it a language feature rather than a file format
with different punctuation. Anywhere the syntax shows a number, a string or a
path, you can write any expression. The named waveforms and modes — SINE,LOWPASS, AUTO, STRETCH, CROSSFADE — each also accept a string
expression, so LAYER wave$ and FILTER kind$ work. The PERCUSSION andABSOLUTE flags take a parenthesised expression, so PERCUSSION(isDrum)
sets one from a value.
So an instrument can be data. A drum kit from a table:
DIM names$(3) AS STRING
DIM freq(3) AS FLOAT
DIM decay(3) AS FLOAT
names$(0) = "Kick" : freq(0) = 55 : decay(0) = 0.30
names$(1) = "Tom" : freq(1) = 110 : decay(1) = 0.22
names$(2) = "Rim" : freq(2) = 320 : decay(2) = 0.08
FOR i = 0 TO 2
INSTRUMENT 70 + i NAME names$(i)
PERCUSSION(TRUE) FIXEDFREQ(freq(i))
ENVELOPE ATTACK(0.001) DECAY(decay(i)) SUSTAIN(0.0) RELEASE(0.05)
PITCHENVELOPE FROM(freq(i) * 4) TO(freq(i)) TIME(decay(i) / 2) ABSOLUTE
LAYER SINE VOLUME(1)
END INSTRUMENT
NEXT iOne thing to know: a bare NOISE is read as the waveform, not as a variable
called NOISE. Pass a variable when you mean one. LOOP ON has always worked
the same way.
Change it while it plays
The block declares. These edit, one property at a time, and they are meant for
a game loop:
SET INSTRUMENT 41 FILTER LOWPASS CUTOFF(800 + charge * 40) RESONANCE(3.0)
SET INSTRUMENT 40 ENVELOPE ATTACK(0.3) ' decay, sustain, release untouched
SET INSTRUMENT LAYER 40, 1 FMDEPTH(depth) ' layer 1 of instrument 40
CREATE INSTRUMENT LAYER 40 SQUARE VOLUME(0.5) DUTY(0.25)
DELETE INSTRUMENT LAYER 40, 1
DELETE INSTRUMENT 40 ' back to the built-in, or goneEvery one of these is a partial update. Anything you don't mention keeps
its value, and that holds inside a grouped modifier too — ENVELOPE ATTACK(0.3)
moves the attack and nothing else. SET INSTRUMENT on a built-in you have not
touched starts from the factory definition, so opening a filter on instrument 3
is one line.
It is SET INSTRUMENT LAYER rather than SET LAYER because SET LAYER is
already a graphics plane. Layers count from 0 in the order they were declared,
which is the same number FMTARGET uses. Deleting a layer shifts the ones
after it down, and anything an FMTARGET pointed at moves with them.
When the change is heard. Edits go to the audio engine automatically,
before the next note that needs them — a PLAY, a PLAYMUSIC, or aLOADMUSIC that pre-renders a song against the instruments it will use. You
never have to think about when to push. Notes already sounding finish on the
settings they started with.
APPLY INSTRUMENT n pushes early. You rarely want it; it exists for the moment
before a busy one — a sampled kit built during a loading screen, so its WAV
reaches the engine before the level starts rather than on the first hit.
Load a bank, save a bank
LOADINSTRUMENTS ' instruments.cbi, the startup bank
LOADINSTRUMENTS "packs/16-bit.cbi" ' a themed pack for the ice level
LOADINSTRUMENTS "packs/nes.cbi" INSTRUMENT 40 ' one patch out of a packA bank merges. Instruments it defines take effect; numbers it doesn't
mention keep what they had. INSTRUMENT n takes a single instrument out of a
file and leaves the rest of it alone. Any WAV a bank's instruments reference
is loaded with it. Reading the same file twice is free — the second call does
nothing rather than discarding edits you made in between — and a plainLOADINSTRUMENTS after a session of experimenting puts the authored bank back.
SAVEINSTRUMENTS "myband.cbi" NAME "My Band" AUTHOR "Dana"
SAVEINSTRUMENTS "lead.cbi" INSTRUMENT 41SAVEINSTRUMENTS writes the instruments you defined or changed — a bank
records what you made, not the factory set — as a .cbi the IDE's Instrument
Editor opens and LOADINSTRUMENTS loads. That is the loop: design a patch in
code, tune it with SET INSTRUMENT while the game runs, save it, and ship it
as an asset next time. The file lands in your platform's save location, the
same place SAVETEXT writes. Referenced WAVs are not copied; a .cbi
stores paths, exactly as a hand-authored one does, so keep the audio where the
paths point.
Read it back
Twenty-two functions read the instrument bank — the interpreter's own copy, so
they see an edit the moment it is made and work with no audio device at all.INSTRUMENTEXISTS(n) is the one that never errors on an unknown number; the
rest are loud about an instrument or layer that isn't there, so guard with it
when you aren't sure.
| Instrument | Layer |
|---|---|
INSTRUMENTNAME$, INSTRUMENTVOLUME, INSTRUMENTPAN, INSTRUMENTPERCUSSION |
LAYERWAVE$, LAYERVOLUME, LAYERDETUNE, LAYEROCTAVE, LAYERDUTY |
INSTRUMENTATTACK, INSTRUMENTDECAY, INSTRUMENTSUSTAIN, INSTRUMENTRELEASE |
LAYERFMTARGET, LAYERFMDEPTH, LAYERFEEDBACK |
INSTRUMENTLAYERCOUNT, CUSTOMINSTRUMENTCOUNT, CUSTOMINSTRUMENTNUMBER |
LAYERSAMPLE$, LAYERBASENOTE |
CUSTOMINSTRUMENTCOUNT() and CUSTOMINSTRUMENTNUMBER(i) walk everything you
have defined or changed — the set a SAVEINSTRUMENTS would write.
SETINSTRUMENTWAV is part of the instrument now
Binding a WAV with SETINSTRUMENTWAV used to live only inside the audio
engine. The instrument you were hearing and the instrument on disk were two
different things, and there was no way to write the first one down.
Now the binding is a property of the instrument like any other. It shows inLAYERSAMPLE$ and LAYERBASENOTE, and SAVEINSTRUMENTS writes it into the.cbi as that layer's sample, so a bank you save reproduces the sound you
were hearing:
LOADWAV "samples/piano.wav", "piano"
SETINSTRUMENTWAV 1, "piano" BASEPITCH "C4"
SAVEINSTRUMENTS "piano.cbi" INSTRUMENT 1Binding to a built-in makes it one of your custom instruments, keeping
everything else the built-in had. The exported file records the WAV's path,
taken from the file the handle was loaded from — so a handle made withBASE64TOWAV or BASE64TOMP3, which has no file behind it, plays correctly
but cannot be saved. SAVEINSTRUMENTS says so, naming the instrument and the
handle, rather than writing a bank that would reload as a bare oscillator.
Instruments are client-side
Every instrument statement — the block, the edits, LOADINSTRUMENTS,SAVEINSTRUMENTS — is a resource load as far as CrashNet is concerned. A
server has no synth and no sound, so calling one there is the same errorLOADWAV and LOADMUSIC give, with the same fix:
IF ME.ISCLIENT THEN
INSTRUMENT 40 NAME "FM Bass"
' ...
END INSTRUMENT
END IFThat is stricter than MUSIC … END MUSIC, which is only gated at LOADMUSIC.
A song edit is inert data; an instrument block with a SAMPLE line reads a
file, and a rule that depended on which modifiers you happened to use would be
a rule nobody could remember.
The .cbi format is in the reference
Instrument File Format in the language reference now documents the bank
format field by field, beside Music File Format. Every field has an exactINSTRUMENT-block equivalent, so what you can author in a file you can build
in code and back again.
One limitation, stated plainly: the format defines three wavetable sources and
the engine plays one of them. A wav source — a WAV of whole single-cycle
frames — works. wavAnalyzed and inline are reserved in the schema and not
yet rendered, so INSTRUMENT has no syntax for them. When they play, it will.
The web player shows no splash
Crash BASIC in the browser is free, and now it looks it. The web player — the
Arcade, the IDE's run window, a game embedded on your own site, and the web
export you upload to itch.io — no longer opens with the "Powered by Crash
BASIC" splash. Not for three seconds, not for two, not on any tier, and not
depending on where the page is hosted. Your game starts when the player starts.
This is deliberate, and it is a gift. There is nothing to buy, no stamp to
apply and no domain to allow-list. The exported shell doesn't even read the
licence tier any more, because there is nothing left for it to decide.
The native players are unchanged. Desktop, mobile and console builds still
open with the splash on the Free tier, and removing it there is still what a
Studio licence or a device key buys.