Crash-tan 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. Each
LAYER 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 INSTRUMENT

A 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 INSTRUMENT

SAMPLE 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 and
ABSOLUTE 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 i

One 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 gone

Every 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 a
LOADMUSIC 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 pack

A 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 plain
LOADINSTRUMENTS after a session of experimenting puts the authored bank back.

SAVEINSTRUMENTS "myband.cbi" NAME "My Band" AUTHOR "Dana"
SAVEINSTRUMENTS "lead.cbi" INSTRUMENT 41

SAVEINSTRUMENTS 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 in
LAYERSAMPLE$ 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 1

Binding 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 with
BASE64TOWAV 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 error
LOADWAV and LOADMUSIC give, with the same fix:

IF ME.ISCLIENT THEN
  INSTRUMENT 40 NAME "FM Bass"
    ' ...
  END INSTRUMENT
END IF

That 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 exact
INSTRUMENT-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.