← Listening study

FANCO / SIREN / a visual teardown

A piano note.
Eight moving sine waves.

What the current approximation really does, where its 31 kB went, and which parts could become a tiny instrument description instead.

Hard A4 · ~441 HzQuarter-width · reduced 440 Hz openingSnapshot · October 22, 2026

8 + 1sines + noise
8output slots
4delay lines
240 Hztable updates

Yes, the first hard-note program was only 307 bytes. The present one is 31,146 bytes — not because it contains a recording or a second synthesizer, but because it spells out eight amplitude controls 1,920 times.

Next design: sparse updates, smooth chip-side motion — interactive reactivity lab →

How small can these curves be? Adjust precision, destinations, and CPU playback in the compression lab →

Loading the audited program data…

01 / Storage is not complexity

The 30,720-byte elephant.

Original hard A4

307 bytes

Eight sine generators. Each has its own 9-byte native envelope. Fixed output gains and pan. No hammer noise, no reverb, no amplitude streaming.

Run the original program

Current listening candidate

31,146 bytes

The same eight-partial idea, but measured amplitude motion, one noise modulator, stereo reverb, narrower output, and the recent opening adjustment.

Run this exact candidate

These are existing previews, not an envelope-only A/B: pan, hammer and reverb also differ. Similar perceived piano character does not mean the programs produce identical waveforms.

98.63% table · 1.37% everything else. No PCM audio is stored in either program.
1,920 frames × 8 partials × 2 bytes = 30,720 bytes
8 seconds × 240 updates/second = 1,920 frames
Exact assembled file accounting — not estimates
Component Original Current
Total 307 B 31,146 B

File size is not the whole machine's memory use. SIREN already has internal delay buffers (a 16 MiB shared pool in this prototype), oscillator/envelope state, and a host renderer. These are not embedded in the note's program. A compact patch would still use the chip.

02 / No hidden FM piano

Add. Shape. Pan. Resonate.

Generators 0–7 produce sine waves at slightly inharmonic frequencies. Their pitches do not wiggle in this candidate. The large table changes their output gains. Generator 8 is a short noise source that amplitude-modulates only generator 0.

Current SIREN piano signal flow Eight sine generators pass through eight time-varying gains and pans into stereo output. Generator eight modulates the first sine with a short noise envelope. A pre-pan mono send feeds four cross-coupled delay lines, whose taps add to the stereo output. 1,920 × 8 gain words CPU writes at 240 Hz 8 sine generators fixed f₁ … f₈ 0–7 · inharmonic partials 8 output gain multipliers gᵢ[k] / 4095 held between register latches 8 pan positions quarter-width setting L / R sum 48 kHz audio G8 · colored noise 9-byte hammer envelope no direct output slot AM into G0 only pre-pan mono send 4 × 2-tap delay lines damping + cross-feedback quiet stereo tail
All nine generators use the existing SIREN engine. The candidate uses no phase FM, no self-feedback, no shimmer generators, and none of the three general filters.
sᵢ(t) = Eᵢ(t) · sin(2πfᵢt + φᵢ)
xᵢ(t) = gᵢ[k] / (4095 · 3) · sᵢ(t)

Eᵢ is the chip's own envelope. Here the eight carriers have a short 2 ms compact-ADSR attack and then hold at full sustain; the time-table gains provide the audible decay. Initial phase is restarted on trigger. The first sine also has the AM factor described below.

Three clocks, not one

240 Hz: CPU reads the next row.
600 Hz: SIREN latches parameter changes.
48,000 Hz: oscillators, envelopes and delay processing generate samples.

The 240 Hz stream is not a 240 Hz recording. Table values are held, not smoothly interpolated by the player; the chip latch adds timing quantization.

Static pitches, dynamic levels

f = 440 · 2(pitch − 32768)/3072

Musical pitch registers use 256 steps per semitone. Frequencies below are decoded from the actual program, not copied from the original measurement estimates.

Current partial configuration
Partial Hz vs i × f₁ Signed pan Send / 255 Peak gain / 4095

03 / What the extra data buys

Eight evolving loudness curves.

Solid curves come from the actual current table. Dashed curves are the original 307-byte program's native envelopes, evaluated using SIREN's segment equations. Both are normalized to their own peak so we can compare shape, not absolute mix level.

Current gain tableOriginal native envelope

Vertical axis: amplitude dB relative to each curve's peak, clipped at −60 dB. The current plots show gain words, not the oscillator waveform or reverb output. Times are nominal table times; actual writes/latches add small delays. Original envelopes use ideal segment math with sample-rounded durations.

How the original 9-byte envelopes work

Each record contains four pairs (target, time) plus a flags byte. In the original held note, the first three stages are an attack and two decays; stage four is the release path if gate-off occurs. The original test program does not issue gate-off.

T(c) = round(48 · 2(c−1)/16) / 48,000 seconds; T(0) = 0
Linear: e(u) = e₀ + (target − e₀) · u
Exponential: e(u) = target + (e₀ − target) · exp(−4.6u)
u = elapsed / T; snap exactly to target at the segment end
All 72 original envelope bytes — eight records, nine bytes each
Partial Attack target/time Decay 1 target/time Decay 2 target/time Release target/time Flags

Two smooth decays can describe much of a piano note in very little data. They cannot reproduce arbitrary beating-like rises and dips. That is why we temporarily replaced the compact envelopes with measured controls: to learn which motion is audible before designing a smaller model.

How the larger table was derived from the recording

Offline analysis isolates each measured partial using complex demodulation and a 30 Hz low-pass filter. It estimates stereo RMS amplitude, subtracts a pre-onset noise-power estimate, compensates pan, and samples the result at 240 Hz. It does not preserve the recording's stereo phase trajectories.

zᵢ,c(t) = LPF₃₀Hz { 2 · recording_c(t) · exp(−j2πfᵢt) }
Aᵢ(t) = √max( (|zᵢ,L|² + |zᵢ,R|²)/2 − noiseᵢ, 0 )

Then come an 8 ms start ramp, an 8-second cutoff with a short fade, gain quantization, and the later listening tweaks. Current quarter-width uses compensated gains/pans/sends. The favorite reduces the first partial by 3 dB through 200 ms, fading that reduction to zero by 500 ms. The actual resulting words are in the inspector below.

Measurements supply the starting curve; listening chooses the useful details. The current favorite includes changes selected by ear, including its narrower stereo image and gentler 440 Hz opening.

04 / The only generator-to-generator modulation

A brief noise breath, multiplied.

Generator 8 makes colored xorshift noise. Its envelope rises to full level in 2 ms, then decays exponentially toward zero for about 30.65 ms. It has no output slot: it modulates the first sine instead.

h(t) = Ehammer(t) · coloredNoise(t)
s₁(t) ← s₁(t) · [223/255 + (32/255) · h(t)]

AM depth is 32, not full ring modulation. When the noise ends, the carrier remains at 223/255 of its unmodulated value; the fitted table compensates for this. Reducing the first partial also reduces the hammer component carried by it.

The entire hammer envelope

255,17, 0,80, 0,0, 0,0, 2

Two active segments, then zero. Flags = 2 makes segment two exponential. No extra sampled hammer recording.

Noise generation and color math
α = 1 − (235/255) · 0.999
nLP[m] = nLP[m−1] + α · (n[m] − nLP[m−1])
coloredNoise = nLP · min(8, √((2−α)/α))

The underlying pseudorandom value is updated on the noise generator's oscillator wrap. Its pitch register is 44000 in musical format; the internal increment is capped at half the sample rate. This shapes a short transient, not a physical hammer simulation.

05 / The space around the note

A narrow body. Four quiet echoes.

Each output slot pans one partial directly to left and right, and also sends an unpanned amount into a mono delay bus. Four delay lines feed one another. Their two taps return signed amounts to left and right.

Ldry = Σ xᵢ · [1 − max(pᵢ,0)/127]
Rdry = Σ xᵢ · [1 + min(pᵢ,0)/127]
bus = Σ xᵢ · sendᵢ/255

Quarter-width, not a new effect

M = (L + R)/2; S = (L − R)/2
L′ = M + 0.25S
R′ = M − 0.25S

Implemented approximately using existing pan/gain/send values and delay tap levels — not a separate stereo processor. Register rounding makes it slightly approximate. This does not recreate the real recording's evolving stereo phases.

Feedback matrix

Each main tap contributes ±32/128 to each destination line. In matrix form: F = 0.25 H. H has norm 2, so this feedback mixing has gain 0.5 before damping. Feedback reaches destinations one audio sample later.

vⱼ[m] = (128/255) · bus[m] + feedbackⱼ[m]
zⱼ[m] = (180/256) · zⱼ[m−1] + (76/256) · vⱼ[m]
wetL,R[m] = Σⱼ,ₜ delayedⱼ,ₜ[m] · tapGainⱼ,ₜ,L,R/128
outputL,R = clip(0.5 · (dryL,R + wetL,R), −1, 1)
Exact current delay settings — gains are signed bytes divided by 128
Line / tap Position code Samples Milliseconds L R Feedback

Tap positions use the implementation's 16-bit minifloat decoder and fractional-sample interpolation. The current SOUND.txt calls them Q4; that wording is stale. This walkthrough follows the actual kernel. No allpass mode or delay modulation is enabled in this candidate. The standard mixer scaling and master are shown above; the listening WAV additionally uses one fixed offline playback gain.

06 / Open the supposedly mysterious 32 kB

It is a spreadsheet of gain knobs.

Every row contains eight little-endian 16-bit words, one per output slot. The upper twelve bits are a gain from 0 to 4095. The lower four bits repeat the source generator number, 0–7.

word[k,i] = (gain[k,i] << 4) | i
gain = word >> 4; source = word & 15
address[k,i] = 0x1086 + 16k + 2i

Those repeated source nibbles alone consume 7,680 bytes. They save the player from repacking a word at each update. They are not musical information.

Actual current control table, 32 rows at a time
Row Nominal time G0 G1 G2 G3 G4 G5 G6 G7

Download every word + all patch settings as JSON — an inspectable export, not the binary size of the program.

The whole per-row player loop
irq:
    cmp r,0
    bz done
    mov a,[p++]
    mov.w [0x200],a    ; gain/source for partial 1
    mov a,[p++]
    mov.w [0x205],a    ; partial 2
    mov a,[p++]
    mov.w [0x20a],a    ; partial 3
    mov a,[p++]
    mov.w [0x20f],a    ; partial 4
    mov a,[p++]
    mov.w [0x214],a    ; partial 5
    mov a,[p++]
    mov.w [0x219],a    ; partial 6
    mov a,[p++]
    mov.w [0x21e],a    ; partial 7
    mov a,[p++]
    mov.w [0x223],a    ; partial 8
    dec r
done:
    reti

The start code sets p to the table and r to 1,920. After eight seconds the zero-gain final row remains active. The IRQ still runs but no longer advances the table. The measured 51,849 guest instructions cover a 16-second render, including this idle tail; that number is not the host renderer's CPU cost.

07 / A model instead of a transcript

Keep the gesture.
Spend bytes on the shape.

The broad dips and recoveries make these envelopes interesting. Rather than store every update, describe a few landmarks and let the chip move between them. The compact original already carries the basic piano character; now we want a small description that also keeps the curves' personality.

Compress the existing controls

23,040 B: pack only the twelve gain bits.
15,360 B: one quantized byte per gain, still at 240 Hz.
Much less: sparse breakpoints or fitted native envelopes.

First option removes redundant bits exactly; the byte gains and reduced curves introduce approximation. A decoder has code/state/CPU costs that must be counted.

Describe the nature of each partial

8 partials × 4 bytes = 32 bytes per anchor. One possible four-parameter starting model is initial level, fast decay time, slow decay time, and slow-tail mixture.

Aᵢ(t) = aᵢ · attack(t) ·
[(1−bᵢ)e−t/τfᵢ + bᵢe−t/τsᵢ]

Two decays capture the broad falling shape. To keep the dips and recoveries too, use compact time/level landmarks or a shared shape vocabulary. The vocabulary has its own storage cost.

The 32 bytes would be the varying harmonic-envelope payload, not an entire instrument. Pitch ratios/inharmonicity, shared attack/release behavior, hammer, pan, reverb, decoding code, and per-voice state still need explicit homes. One byte per partial can index a shared vocabulary, but it cannot encode arbitrary independent histories for free.

We have A3, A4 and A5 references — nominally 220, 440 and 880 Hz — at three dynamics. At 32 bytes each, those nine anchors would occupy 288 bytes, plus the shared description and player. None of that compact interpolation is implemented yet.

note coordinate = log₂(f / fanchor)
interpolate decoded levels and time scales — not arbitrary shape IDs

Interpolation between measured anchors is different from extrapolating outside them. Extrapolation needs bounded rules and listening checks. A single smooth decay model will not represent every instrument, arbitrary nonharmonic spectrum, or articulation. We should fit the current favorite first, then choose the smallest model that keeps what matters to your ears.