""" linnfmt - the Linn 9000 disk format, shared by the other tools. Everything here was recovered by reading real disks; see README.md for how each field was pinned down and which are still unknown. 720K disk, 737280 bytes, no filesystem, 4096-byte blocks. Block 0 holds the directory, so block N lives at N * 4096 and blocks 1..179 are available for audio. directory: 64 slots of 32 bytes 0-5 name, 6 ASCII chars, space padded 6-9 E5 E5 E5 E5 on every live entry 10-11 51 FF on 633 of 676 samples seen; 51 E5 on 19; a handful of others. Tracks nothing measurable. UNKNOWN. 12 always 00 13-14 total length in bytes, u16 LE (header included) 16-31 block numbers, one byte each, 00 terminates. 16 max, which is why no sample can exceed 65536 bytes. Blocks need not be contiguous - the factory disk fragments several samples. a slot whose first byte is E5 is free each sample's FIRST block opens with a 16-byte header 0-1 length of the audio following the header, u16 LE (always the directory length minus 16) 2-7 the name again, 6 ASCII chars 8-15 UNKNOWN. Byte 8 is 0x1a on a quarter of samples, and the field does not correlate with pitch, size or block count. We copy a known-good pattern from a real factory sample. later blocks of the same sample are pure audio, no header. audio: 8-bit mu-law, sign in bit 7, exponent in 6-4, mantissa in 3-0, stored WITHOUT the bit inversion G.711 telephony applies. 0x00 and 0x80 both decode to 0, which is why samples end on 0x80 and pad with 0x00. """ import struct BLOCK, DIR_SLOTS, ENTRY, HDR = 4096, 64, 32, 16 DISK, MAX_BLOCKS, EMPTY = 737280, 179, 0xE5 MAX_SAMPLE = 16 * BLOCK # 65536 bytes, the 16-block ceiling RATE = 32000 # not stored on disk; see README DIR_FLAG = (0x51, 0xFF) HDR_TAIL = bytes((0x1A, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00)) BIAS, CLIP = 0x84, 32635 _EXP = ([0]*2 + [1]*2 + [2]*4 + [3]*8 + [4]*16 + [5]*32 + [6]*64 + [7]*128) def ulaw_decode(b): t = (((b & 0x0F) << 3) + BIAS) << ((b & 0x70) >> 4) return BIAS - t if b & 0x80 else t - BIAS def ulaw_encode(s): sign = 0x80 if s < 0 else 0 s = min(abs(int(s)), CLIP) + BIAS e = _EXP[(s >> 7) & 0xFF] return sign | (e << 4) | ((s >> (e + 3)) & 0x0F) DECODE = [ulaw_decode(b) for b in range(256)] def clean_name(s): """-> 6 upper-case chars the Linn will show properly.""" out = "".join(c for c in s.upper() if c.isalnum() or c in "_#") return (out[:6] or "SAMPLE").ljust(6) def entries(img): """-> [(name, total_len, [blocks], flag)] for every live directory slot.""" out = [] for i in range(DIR_SLOTS): e = img[i * ENTRY:(i + 1) * ENTRY] if len(e) < ENTRY or e[0] == EMPTY: continue name = e[0:6].decode("ascii", "replace").rstrip() if not name: continue total = struct.unpack(" mu-law bytes for one sample, header stripped.""" raw = b"".join(img[b * BLOCK:(b + 1) * BLOCK] for b in blocks)[:total] data = raw[HDR:] if trim: end = len(data) while end and data[end - 1] in (0x00, 0x80): end -= 1 data = data[:end] return data def wav_bytes(ulaw_data, rate=RATE): pcm = b"".join(struct.pack("