Skip to content

Shape a Logical Sound Envelope in C64 BASIC

Service attack, decay, sustain and release as bounded logical control levels with retained gate-edge history.

A sound can swell, settle and fade instead of switching straight between full level and silence. This routine computes that shape. A separate output path decides how the machine can turn those levels into sound.

C64 BASIC shows both retained behaviour checks passing.
The working caller completes with both saved checks equal to one.

Include and initialise

Reserve L/P/W/O/G/A/D/S/R and lines 3000–3390. There are no arrays or private scratch fields. Combine routines.bas and your caller through build.py, keeping END, STOP or a jump before the helper. C64/BBC use GOSUB; Spectrum uses GO SUB.

Call 3000 at a reset boundary. It clears all public fields. Set attack A, decay D and release R from one through fifteen, plus sustain S from zero through fifteen, afterwards. These are linear steps per service, not hardware chip rates or durations in seconds.

Service the gate

Supply gate G as a byte, zero off and nonzero held, then call 3100. L is the retained level zero through fifteen. P is the phase: zero off, one attack, two decay, three sustain or four release. W is normalised previous gate state, zero or one. O reports valid input.

Reusable numbered routines

routines.bas
3000 LET L=0: LET P=0: LET W=0: LET O=0: LET G=0: LET A=0: LET D=0: LET S=0: LET R=0: RETURN
3100 LET O=0
3110 IF L<0 OR L>15 OR L<>INT (L) THEN RETURN
3120 IF P<0 OR P>4 OR P<>INT (P) THEN RETURN
3130 IF W<>0 AND W<>1 THEN RETURN
3140 IF G<0 OR G>255 OR G<>INT (G) THEN RETURN
3150 IF A<1 OR A>15 OR A<>INT (A) THEN RETURN
3160 IF D<1 OR D>15 OR D<>INT (D) THEN RETURN
3170 IF S<0 OR S>15 OR S<>INT (S) THEN RETURN
3180 IF R<1 OR R>15 OR R<>INT (R) THEN RETURN
3200 LET O=1
3210 IF G=0 THEN GOTO 3250
3220 IF W=0 THEN LET L=0: LET P=1
3230 LET W=1: GOTO 3270
3250 LET W=0
3260 IF P<>0 THEN LET P=4
3270 IF P=0 THEN LET L=0: RETURN
3280 IF P=1 THEN GOTO 3330
3290 IF P=2 THEN GOTO 3360
3300 IF P=3 THEN LET L=S: RETURN
3310 LET L=L-R
3315 IF L<=0 THEN LET L=0: LET P=0
3320 RETURN
3330 LET L=L+A
3340 IF L>=15 THEN LET L=15: LET P=2
3350 RETURN
3360 IF L<=S THEN LET L=S: LET P=3: RETURN
3370 LET L=L-D
3380 IF L<=S THEN LET L=S: LET P=3
3390 RETURN
Phase One service does
Off Holds level zero
Attack Adds A, clamps at fifteen, then enters decay
Decay Moves down by D towards S, clamps there, then enters sustain
Sustain Sets level to S
Release Subtracts R, clamps at zero, then enters off

A rising gate resets L to zero and starts attack; that same call takes the first attack step. A low gate sends any active phase to release and takes the first release step immediately. Only one phase is serviced in a call: reaching the attack peak does not also take a decay step.

L/P/W and configuration must be integral and within their bounds, and G must be an integral byte. Invalid input sets O=0 and preserves envelope state and supplied inputs. Valid calls preserve G/A/D/S/R. If sustain is changed during decay, clamping to the new target can raise a level that was below it; choose live parameter-change policy deliberately.

Follow a held and released note

Normal A=4,D=3,S=6,R=2 produces held levels 4,8,12,15,12,9,6,6, then released levels 4,2,0,0. The custom caller attacks faster and releases earlier: A=8,D=4,S=5,R=3, with held levels 8,15,11, then 8,5,2,0 after release. Saved peak and first-release checks preserve events that a final zero would hide.

Example calls from the maintained caller

demo.bas, excerpt
70 GOSUB 3000: LET AA=0: LET BB=0
80 LET A=4: LET D=3: LET S=6: LET R=2
90 LET G=1: GOSUB 3100: GOSUB 2000
100 LET G=1: GOSUB 3100: GOSUB 2000
110 LET G=1: GOSUB 3100: GOSUB 2000
120 LET G=1: GOSUB 3100: GOSUB 2000
130 IF L=15 AND P=2 THEN LET AA=1
140 LET G=1: GOSUB 3100: GOSUB 2000
150 LET G=1: GOSUB 3100: GOSUB 2000
160 LET G=1: GOSUB 3100: GOSUB 2000
170 LET G=1: GOSUB 3100: GOSUB 2000
180 LET G=0: GOSUB 3100: GOSUB 2000
190 IF L=4 THEN LET BB=1
200 LET G=0: GOSUB 3100: GOSUB 2000
210 LET G=0: GOSUB 3100: GOSUB 2000
220 LET G=0: GOSUB 3100: GOSUB 2000
230 PRINT "ENVE RESULTS";AA;BB
240 POKE MB+995,AA: POKE MB+996,BB: POKE MB+997,GD: POKE MB+998,WC-256*INT (WC/256): POKE MB+999,INT (WC/256): POKE MB+1000,239: STOP

Line 2000, AA/BB/JJ (Z on Spectrum) and the mailbox are observations. A game retains L/P/W, supplies gate state and services at its chosen cadence. An output backend maps L to an owned voice or gain. This proof checks the control routine in the interpreter; it does not claim audible output, a verified device mapping or a musical timing budget.

Choose the output and timing policy

A logical range of sixteen levels is not a promise that every machine has a matching independent volume control. Some paths can map it directly, others need another strategy or cannot provide the intended shape. Do not overwrite shared master volume or a hardware-envelope setting without an ownership policy.

A hardware envelope may save service work when its behaviour fits. This software version offers explicit control and repeatable shape, at the cost of interpreted updates. Curved stages, durations, velocity, legato and retrigger-from-current-level require different rules. Separate voices need separate renamed state and line references.

Mistake and repair

The wrong caller clears W before every service. A held gate continually looks new, so attack restarts from zero and never develops the intended shape. Peak and first-release checks both fail, although all variants eventually reach silence. Retain gate history independently of servicing the note. Repaired source and loaded program match normal exactly.

C64 BASIC shows two failed checks from the deliberate caller mistake.
The wrong caller runs to completion, but both saved behaviour checks are zero.

Cost and Maths for Games

Each valid call performs fixed validation and one bounded phase update. It does no allocation, device write, interrupt setup or wait. Service frequency, interpreter cost and output mapping together determine what is practical; tokenised size alone is not an audio-quality or frame-time guarantee.

Maths for Games connects clamping, piecewise linear curves and state transitions. The Craft connects onset, sustain and release to the character of a sound.

Global names and line numbers

BASIC numeric names, arrays and numbered lines are global. To combine helpers, rename public and private fields and renumber the helper together with every GO SUB/GO TO reference. Keep each state block under one owner. The maintained build.py rejects duplicate line numbers; it does not automatically namespace or renumber routines.

Machine and measured size

C64 BASIC V2 recognises only the first two significant characters of numeric variable names. Short names make the ownership visible; check for collisions before including another component. This helper uses scalar numeric fields and allocates no arrays. The diagnostic uses a mailbox at $C000, outside the normal BASIC heap.

The numbered helper occupies 669 tokenised program bytes on this interpreter, including line headers. This excludes the caller, test tables, numeric scalar/array storage, interpreter stacks and ROM. Tokenised size is not machine-code size or a hardware-cycle count. No real-time or whole-game performance budget is inferred from the diagnostic. The caller’s AA/BB, JJ and RAM mailbox are observation machinery, not required game state.

Execution evidence

Working, custom, deliberately wrong and exact repaired callers executed through C64’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP programs. C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters every numbered source line through MOS/BASIC. Actual loaded program bytes, complete state packets, positive completion/count/guards and visible result pixels are checked. After the positive completion marker, the observer advances two complete display frames before capturing, so a partially scanned final character cannot masquerade as a finished picture. Firmware is not distributed.

An independent mathematical/order model checks the trace and rejects altered source, configuration, loaded program, state, completion and image evidence. Host exhaustive checks complement the native boundary/sequential examples; those host ranges are not claimed as natively exhaustive. The supplied diagnostic callers deliberately end with STOP; the interpreter report is expected. The wrong caller prints 0,0 and the normal/custom/repaired callers print 1,1.

Public CI checks retained evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-interfaces.py logical-envelope commodore-64 --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-interfaces.py logical-envelope.

Explore further

Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.