# Ryder's Linn 9000 Tools Read, extract, and build sample disks for the Linn 9000 — including writing `.hfe` images straight onto a Gotek / FlashFloppy USB stick. The disk format was reverse-engineered from real disks: two floppies read on a plain USB drive, plus thirteen library disks off a vendor Gotek stick. 676 samples across 15 disks were used to check every field below. --- ## 1. The format 720K disk, 737,280 bytes. No filesystem. **4096-byte blocks**, and block *N* sits at `N * 4096`, so the directory occupies block 0 and blocks 1–179 hold audio. ### Directory — block 0, 64 slots of 32 bytes | offset | meaning | |---|---| | 0–5 | name, 6 ASCII chars, space padded. First byte `E5` = free slot | | 6–9 | `E5 E5 E5 E5` on every live entry | | 10–11 | `51 FF` on 633 of 676 samples, `51 E5` on 19, a few others. **Unknown** | | 12 | always `00` | | 13–14 | total length in bytes, u16 LE, header included | | 16–31 | block numbers, one byte each, `00` terminates | The block list is 16 bytes wide, which is why **no sample can exceed 65,536 bytes** (2.05 s). Blocks need not be contiguous — the factory disk fragments several samples, e.g. `CRASH1` uses 13–18 and 129–134. ### Sample header — first 16 bytes of a sample's first block | offset | meaning | |---|---| | 0–1 | length of the audio that follows, u16 LE (= directory length − 16) | | 2–7 | the name again, 6 ASCII chars | | 8–15 | **Unknown.** Byte 8 is `0x1A` on a quarter of samples. Does not correlate with pitch, size or block count | Later blocks of the same sample are pure audio, no header. ### Audio — 8-bit µ-law Sign in bit 7, exponent in bits 6–4, mantissa in bits 3–0. This is G.711 µ-law **stored without the bit inversion telephony applies** — decode the byte directly. `0x00` and `0x80` both decode to 0, which is why samples end on `0x80` and are padded with `0x00`: in this encoding that join is silent. Companding is why the machine sounds better than 8 bits suggests — it gives roughly 14 bits of range. --- ## 2. How each field was pinned down Anything below that reads like a guess *is* one, and is labelled Unknown above. Everything else was measured. **Block size = 4096.** For every sample on both disks the length field falls in `(4096·(n−1), 4096·n]` where *n* is the number of block bytes listed, and `ceil(size/4096) == n` for all 51 samples on the first two disks. No other block size fits. **Base offset = `N * 4096`.** With this, every one of 51 samples begins on a non-zero byte. With `0x800 + (N−1)*4096` they average ~1000 bytes of leading silence — that base is 2048 bytes early. **Block order is right.** Across 101 block joins the waveform discontinuity is a median 0.5–1.0× an ordinary sample-to-sample step, i.e. the seams are inaudible. This test is encoding-independent, so it held even while the encoding was still wrong. **Header is 16 bytes.** Per-byte entropy across 676 samples sits near 5.9 through offset 15 then jumps to 7.28 at offset 16 and stays there. It is also self-consistent: `16 + length field == directory length` on 51/51. **Audio is µ-law.** Measured as total harmonic distortion on the sustained `BAS808` tone: | decoding | THD | |---|---| | **µ-law, direct** | **0.2%** | | A-law complement | 19.8% | | sign-magnitude linear | 24.2% | | two's complement | 41.5% | | µ-law complement (the telephony way) | 159.4% | µ-law measured lower on all 11 tonal samples tested across both disks, and later on independent vendor media (`CONGA2`, 0.4%). **Sample rate is NOT stored** anywhere in the format. The default of 32,000 Hz comes from the 808 kick on disk 3: its fundamental is 585 samples per cycle, and a TR-808 kick is tuned near 55 Hz (A1), giving ~32.2 kHz. Linn documented the machine as sampling anywhere from 11 to 37 kHz, so different sounds may genuinely differ — but no header byte correlates with measured pitch (|r| ≤ 0.10 for bytes 8, 9 and 10 across 43 kick-family samples), so there is nothing to read. Pitch scales linearly: if a sound plays a fifth low, multiply `--rate` by 1.5. --- ## 3. The tools All need Python 3 and numpy. `linnfmt.py` is the shared format module. ### `linn9000.py` — read a disk ``` ./linn9000.py list disk.img ./linn9000.py info disk.img ./linn9000.py extract disk.img -o out/ [--rate 32000] [--no-trim] ``` Trailing silence is trimmed by default; it is often most of the file (808KIK stores 8160 bytes of which only 3456 are sound). ### `linn9000build.py` — folder of WAVs to a disk image ``` ./linn9000build.py MYKIT/ -o mykit.img [--gain kit|sample|none] ``` Resamples to 32 kHz mono and µ-law encodes. `--gain kit` (default) applies one gain to the whole folder so the balance between sounds is preserved; `--gain sample` normalises each independently, which flattens the kit. Limits are enforced and reported, not silently applied: 64 samples, 65,536 bytes (2.05 s) each, 716 KB total, 6-character upper-case names. ### `hfe2img.py` / `img2hfe.py` — Gotek round trip ``` ./hfe2img.py disk.hfe -o disk.img ./img2hfe.py disk.img -o disk.hfe ``` `hfe2img` decodes the MFM cell stream: find the A1 sync marks (written with a deliberately illegal clock so they cannot occur in data), then take every other cell as a data bit. `img2hfe` does **not** synthesise tracks. It patches a real formatted blank from the drive vendor (`reference/blank-linn9000-720k.hfe`), replacing each sector's 512 data bytes and recomputing the CRC, so every gap, sync pattern, address mark and timing detail stays exactly as a working disk has them. Verified: `img → hfe → img` is byte-exact, all 1440 sectors recovered, and all 1440 data-field CRCs valid — the same CRC code also validates the vendor's own blank. ### `linn9000edit.py` — browser kit builder, port 8003 ``` ./linn9000edit.py /path/to/wavs [/path/to/disk/images] open http://localhost:8003 ``` Or run every tool from one page and one port with `mpctools.py` in the project root, which mounts this at `/linn/`, the MPC60 editor at `/mpc60/` and the Squid slicer at `/squid/`. The pages use relative URLs, so they work either way with no change. The WAV library and the disk-image folder are **separate** paths, each with its own picker — click either path to open a native folder chooser. Lays out the machine's 18 voices in the panel's own order, three rows of six: ``` TOM 1 TOM 2 RIDE 1 RIDE 2 HI CONGA LO CONGA SIDESTICK SNARE TOM 3 CRASH 1 COWBELL CLAPS HI HAT BASS TOM 4 CRASH 2 TAMBOURINE CABASA ``` **Load an existing disk.** Pick any `.img` or `.hfe` in the library and press Load onto pads. Samples are matched to voices by name, scored so that an exact match beats a prefix, which beats the stem appearing anywhere in the name, and earlier aliases beat later ones. The factory disk fills 17 of 18 pads correctly (`BASS7`, `SNARE1`, `HIHAT1`, `SSTK1`, `CABAS1` and so on); prefixed banks like the 808 disk still fill 10 via the substring rule (`808KIK`, `08SNR2`). Everything unmatched stays in the list for you to place by hand. Building from a loaded disk copies the mu-law straight across with no resampling, so the audio is bit-identical — verified 17/17 on the factory disk. **Previews.** Click a file in the list to hear it; click a filled pad to hear that. WAV previews are rendered *through* the conversion, so what you hear is what actually lands on the disk — 32 kHz, mu-law — not the original file. **Library.** The path at the top can be changed at any time; press Set to re-scan. Click a file then a pad to assign, or drag a WAV straight onto a pad. The × on a pad clears it. Build writes both `.img` and `.hfe`. **About pads:** the directory stores only 64 *named samples* — there is no pad-assignment field anywhere in the format, and the Linn assigns sounds to pads from its own front panel. The grid is an organiser: it names each sample after the voice you drop it on, so the machine's list reads like a kit. It cannot pre-assign the pads for you. --- ## 4. Getting a kit onto the machine 1. Put your WAVs in a folder (any rate/depth; they get converted). 2. `./linn9000build.py MYKIT/ -o mykit.img` 3. `./img2hfe.py mykit.img -o mykit.hfe` 4. Copy `mykit.hfe` to the Gotek USB stick. 5. Select it on the drive, wait ~2 s for it to load, then load sounds on the Linn as you would from a floppy. --- ## 5. Traps - **2.05 s is a hard ceiling** per sample. It is the 16-byte block list, not a soft limit — nothing can raise it. - **Names are 6 characters**, upper case. Duplicates collide, so the builder renames rather than overwrite. - **Don't reuse block 0.** It is the directory. - **Sample rate is a guess, uniformly applied.** If everything is off, it will be off by the same interval, and `--rate` fixes all of it at once. - **The vendor's own notes are a Prophet document** with the title changed. Its claim that these disks "cannot be read natively on a PC or MAC" is not true of the Linn 9000 — they are standard IBM MFM, 9×512, and a plain USB floppy drive reads them. ## 6. Still unknown - Directory bytes 10–11, and header bytes 8–15. - Whether the machine ever writes a non-sequential allocation itself, or whether the fragmentation on the factory disk came from editing.