diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 694c0f8..8b446bd 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 22 +#define MONTAUK_BUILD_NUMBER 23 diff --git a/programs/src/music/Makefile b/programs/src/music/Makefile index 382faa1..42cb45d 100644 --- a/programs/src/music/Makefile +++ b/programs/src/music/Makefile @@ -52,7 +52,7 @@ LDFLAGS := \ # ---- Source files ---- -SRCS := main.cpp visualizer.cpp minimp3_impl.cpp stb_truetype_impl.cpp +SRCS := main.cpp visualizer.cpp minimp3_impl.cpp flac.cpp stb_truetype_impl.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- diff --git a/programs/src/music/flac.cpp b/programs/src/music/flac.cpp new file mode 100644 index 0000000..1a9ce7a --- /dev/null +++ b/programs/src/music/flac.cpp @@ -0,0 +1,625 @@ +/* + * flac.cpp + * Native FLAC decoder for the Music player + * Copyright (c) 2026 Daniel Hammer + */ + +#include "flac.hpp" + +#include + +extern "C" { +#include +} + +namespace flac { + +// ============================================================================ +// CRC tables (CRC-8 poly 0x07 over frame headers, CRC-16 poly 0x8005 over +// whole frames) +// ============================================================================ + +static uint8_t g_crc8[256]; +static uint16_t g_crc16[256]; +static bool g_crc_ready = false; + +static void init_crc_tables() { + if (g_crc_ready) return; + for (int i = 0; i < 256; i++) { + uint8_t c8 = (uint8_t)i; + for (int b = 0; b < 8; b++) + c8 = (c8 & 0x80) ? (uint8_t)((c8 << 1) ^ 0x07) : (uint8_t)(c8 << 1); + g_crc8[i] = c8; + + uint16_t c16 = (uint16_t)(i << 8); + for (int b = 0; b < 8; b++) + c16 = (c16 & 0x8000) ? (uint16_t)((c16 << 1) ^ 0x8005) : (uint16_t)(c16 << 1); + g_crc16[i] = c16; + } + g_crc_ready = true; +} + +static uint8_t crc8(const uint8_t* p, uint64_t n) { + uint8_t c = 0; + for (uint64_t i = 0; i < n; i++) c = g_crc8[c ^ p[i]]; + return c; +} + +static uint16_t crc16(const uint8_t* p, uint64_t n) { + uint16_t c = 0; + for (uint64_t i = 0; i < n; i++) + c = (uint16_t)((c << 8) ^ g_crc16[(c >> 8) ^ p[i]]); + return c; +} + +// ============================================================================ +// Bit reader (MSB first) +// ============================================================================ + +struct BitReader { + const uint8_t* data; + uint64_t size; // bytes + uint64_t pos; // bits + bool overrun; +}; + +// The next 64 bits at the cursor; at least the top 57 are real stream bits +// (or zero padding past the end, which callers catch through `overrun`). +static inline uint64_t peek64(const BitReader& br) { + uint64_t byte = br.pos >> 3; + uint64_t v = 0; + if (byte + 8 <= br.size) { + uint64_t raw; + memcpy(&raw, br.data + byte, 8); + v = __builtin_bswap64(raw); + } else { + for (int i = 0; i < 8; i++) { + v <<= 8; + if (byte + i < br.size) v |= br.data[byte + i]; + } + } + return v << (br.pos & 7); +} + +static inline void advance(BitReader& br, uint64_t bits) { + br.pos += bits; + if (br.pos > br.size * 8) br.overrun = true; +} + +static inline uint32_t read_bits(BitReader& br, int n) { + if (n == 0) return 0; + uint32_t v = (uint32_t)(peek64(br) >> (64 - n)); + advance(br, n); + return v; +} + +static inline int32_t read_signed(BitReader& br, int n) { + if (n == 0) return 0; + uint32_t v = read_bits(br, n); + return (int32_t)(v << (32 - n)) >> (32 - n); +} + +static inline int64_t read_signed64(BitReader& br, int n) { + if (n <= 32) return read_signed(br, n); + uint64_t hi = read_bits(br, n - 32); + uint64_t v = (hi << 32) | read_bits(br, 32); + return (int64_t)(v << (64 - n)) >> (64 - n); +} + +static inline uint32_t read_unary(BitReader& br) { + uint32_t count = 0; + for (;;) { + uint64_t v = peek64(br); + if (v) { + int z = __builtin_clzll(v); + advance(br, (uint64_t)z + 1); + return count + (uint32_t)z; + } + advance(br, 56); + count += 56; + if (br.overrun) return 0; + } +} + +// ============================================================================ +// Frame header +// ============================================================================ + +static bool parse_header(const Decoder& dec, uint64_t offset, FrameHeader& h) { + if (offset >= dec.size) return false; + uint64_t avail = dec.size - offset; + + // A header is at most 16 bytes; parse from a zero-padded copy so the + // field reads need no bounds checks, then make sure it really fit. + uint8_t p[16] = {}; + memcpy(p, dec.data + offset, avail < 16 ? avail : 16); + + if (p[0] != 0xFF || (p[1] & 0xFE) != 0xF8) return false; + bool variable = p[1] & 1; + uint8_t bs_code = p[2] >> 4; + uint8_t sr_code = p[2] & 0x0F; + uint8_t ch_code = p[3] >> 4; + uint8_t ss_code = (p[3] >> 1) & 7; + if ((p[3] & 1) || bs_code == 0 || sr_code == 15 || ch_code > 10 || ss_code == 3) + return false; + + // UTF-8-style coded frame number (fixed) or sample number (variable) + int i = 4; + uint8_t b0 = p[i++]; + uint64_t num; + int extra; + if (!(b0 & 0x80)) { num = b0; extra = 0; } + else if ((b0 & 0xE0) == 0xC0) { num = b0 & 0x1F; extra = 1; } + else if ((b0 & 0xF0) == 0xE0) { num = b0 & 0x0F; extra = 2; } + else if ((b0 & 0xF8) == 0xF0) { num = b0 & 0x07; extra = 3; } + else if ((b0 & 0xFC) == 0xF8) { num = b0 & 0x03; extra = 4; } + else if ((b0 & 0xFE) == 0xFC) { num = b0 & 0x01; extra = 5; } + else if (b0 == 0xFE) { num = 0; extra = 6; } + else return false; + if (!variable && extra > 5) return false; + for (int e = 0; e < extra; e++) { + uint8_t b = p[i++]; + if ((b & 0xC0) != 0x80) return false; + num = (num << 6) | (b & 0x3F); + } + + uint32_t bs; + if (bs_code == 1) bs = 192; + else if (bs_code <= 5) bs = 576u << (bs_code - 2); + else if (bs_code == 6) bs = (uint32_t)p[i++] + 1; + else if (bs_code == 7) { bs = (((uint32_t)p[i] << 8) | p[i + 1]) + 1; i += 2; } + else bs = 256u << (bs_code - 8); + + if (sr_code == 12) i += 1; + else if (sr_code == 13 || sr_code == 14) i += 2; + + if ((uint64_t)i + 1 > avail) return false; + if (crc8(p, i) != p[i]) return false; + i++; + + static constexpr uint8_t ss_bits[8] = {0, 8, 12, 0, 16, 20, 24, 32}; + uint8_t bps = ss_code ? ss_bits[ss_code] : dec.info.bits_per_sample; + uint8_t channels = ch_code < 8 ? (uint8_t)(ch_code + 1) : 2; + + if (channels != dec.info.channels) return false; + if (bs > dec.chan_cap) return false; + + h.block_size = bs; + h.header_bytes = (uint32_t)i; + h.channel_assign = ch_code; + h.channels = channels; + h.bits_per_sample = bps; + if (variable) { + h.sample_start = num; + } else { + // Fixed-blocksize streams number frames; every frame but the last + // has the stream's block size. + uint64_t fixed = (dec.info.min_block == dec.info.max_block) ? dec.info.max_block : bs; + h.sample_start = num * fixed; + } + return true; +} + +// ============================================================================ +// Subframes +// ============================================================================ + +static bool decode_residual(BitReader& br, int32_t* out, uint32_t bs, uint32_t order) { + uint32_t method = read_bits(br, 2); + if (method > 1) return false; + int param_bits = method == 0 ? 4 : 5; + uint32_t escape = method == 0 ? 15 : 31; + + uint32_t po = read_bits(br, 4); + uint32_t parts = 1u << po; + if (bs & (parts - 1)) return false; + uint32_t psize = bs >> po; + if (psize < order) return false; + + int32_t* dst = out + order; + for (uint32_t p = 0; p < parts; p++) { + uint32_t n = psize - (p == 0 ? order : 0); + uint32_t k = read_bits(br, param_bits); + if (k == escape) { + int nbits = (int)read_bits(br, 5); + for (uint32_t j = 0; j < n; j++) *dst++ = read_signed(br, nbits); + } else { + for (uint32_t j = 0; j < n; j++) { + uint32_t q = read_unary(br); + uint32_t u = (q << k) | read_bits(br, (int)k); + *dst++ = (int32_t)(u >> 1) ^ -(int32_t)(u & 1); + } + } + if (br.overrun) return false; + } + return true; +} + +// Samples are int32_t, except the 33-bit side channel of 32-bit streams +// (int64_t). Prediction arithmetic is 64-bit either way. +template +static void restore_fixed(T* s, uint32_t bs, uint32_t order) { + switch (order) { + case 1: + for (uint32_t i = 1; i < bs; i++) s[i] = (T)(s[i] + (int64_t)s[i - 1]); + break; + case 2: + for (uint32_t i = 2; i < bs; i++) + s[i] = (T)(s[i] + 2 * (int64_t)s[i - 1] - s[i - 2]); + break; + case 3: + for (uint32_t i = 3; i < bs; i++) + s[i] = (T)(s[i] + 3 * ((int64_t)s[i - 1] - s[i - 2]) + s[i - 3]); + break; + case 4: + for (uint32_t i = 4; i < bs; i++) + s[i] = (T)(s[i] + 4 * ((int64_t)s[i - 1] + s[i - 3]) + - 6 * (int64_t)s[i - 2] - s[i - 4]); + break; + default: + break; + } +} + +template +static void restore_lpc(T* s, uint32_t bs, uint32_t order, + const int32_t* coef, int shift, bool wide) { + if (!wide && sizeof(T) == 4) { + // Small sample + coefficient widths: the sum provably fits 32 bits. + for (uint32_t i = order; i < bs; i++) { + int32_t sum = 0; + for (uint32_t j = 0; j < order; j++) sum += coef[j] * (int32_t)s[i - 1 - j]; + s[i] = (T)(s[i] + (sum >> shift)); + } + } else { + for (uint32_t i = order; i < bs; i++) { + int64_t sum = 0; + for (uint32_t j = 0; j < order; j++) sum += (int64_t)coef[j] * s[i - 1 - j]; + s[i] = (T)(s[i] + (sum >> shift)); + } + } +} + +// Residuals always fit 32 bits; they are decoded into `scratch` (which is +// `out` itself for int32_t samples) and widened into `out` when T is wider. +template +static bool decode_subframe(BitReader& br, T* out, int32_t* scratch, uint32_t bs, int bps) { + if (read_bits(br, 1) != 0) return false; + uint32_t type = read_bits(br, 6); + + int wasted = 0; + if (read_bits(br, 1)) { + wasted = (int)read_unary(br) + 1; + if (wasted >= bps) return false; + bps -= wasted; + } + if (bps < 1 || bps > (sizeof(T) == 4 ? 32 : 33)) return false; + + uint32_t order = 0; + if (type == 0) { + T v = (T)read_signed64(br, bps); + for (uint32_t i = 0; i < bs; i++) out[i] = v; + } else if (type == 1) { + for (uint32_t i = 0; i < bs; i++) out[i] = (T)read_signed64(br, bps); + } else if ((type >= 8 && type <= 12) || type >= 32) { + bool lpc = type >= 32; + order = lpc ? (type & 31) + 1 : type - 8; + if (order > bs) return false; + for (uint32_t i = 0; i < order; i++) out[i] = (T)read_signed64(br, bps); + + int precision = 0, shift = 0; + int32_t coef[32]; + if (lpc) { + precision = (int)read_bits(br, 4) + 1; + if (precision == 16) return false; + shift = read_signed(br, 5); + if (shift < 0) return false; + for (uint32_t i = 0; i < order; i++) coef[i] = read_signed(br, precision); + } + + if (!decode_residual(br, scratch, bs, order)) return false; + if ((void*)scratch != (void*)out) { + for (uint32_t i = order; i < bs; i++) out[i] = scratch[i]; + } + + if (lpc) { + int order_bits = 0; + while ((1u << order_bits) < order) order_bits++; + restore_lpc(out, bs, order, coef, shift, bps + precision + order_bits > 32); + } else { + restore_fixed(out, bs, order); + } + } else { + return false; + } + + if (wasted) { + for (uint32_t i = 0; i < bs; i++) out[i] = (T)((uint64_t)(int64_t)out[i] << wasted); + } + return !br.overrun; +} + +// Undo stereo decorrelation into a (left) and b (right). `side` is the side +// channel: b for left/side and mid/side, a for side/right, or the wide buffer. +template +static void decorrelate(int32_t* a, int32_t* b, const T* side, uint8_t assign, uint32_t bs) { + if (assign == 8) { // left / side + for (uint32_t i = 0; i < bs; i++) b[i] = (int32_t)(a[i] - (int64_t)side[i]); + } else if (assign == 9) { // side / right + for (uint32_t i = 0; i < bs; i++) a[i] = (int32_t)(side[i] + (int64_t)b[i]); + } else if (assign == 10) { // mid / side + for (uint32_t i = 0; i < bs; i++) { + int64_t sd = side[i]; + int64_t mid = ((int64_t)a[i] * 2) | (sd & 1); + a[i] = (int32_t)((mid + sd) >> 1); + b[i] = (int32_t)((mid - sd) >> 1); + } + } +} + +// ============================================================================ +// Output conversion +// ============================================================================ + +static inline int16_t to_s16(int64_t v, int bps) { + if (bps > 16) v >>= (bps - 16); + else if (bps < 16) v *= (int64_t)1 << (16 - bps); + if (v > 32767) v = 32767; + if (v < -32768) v = -32768; + return (int16_t)v; +} + +// Stereo downmix weights (Q14, left/right) for the standard FLAC channel +// orders of 3..8 channel streams. LFE is dropped. +static constexpr int16_t W1 = 16384, WH = 11585, WQ = 8192; +static constexpr int16_t g_downmix[9][MAX_CHANNELS][2] = { + {}, {}, {}, + /* 3: FL FR FC */ {{W1,0},{0,W1},{WH,WH}}, + /* 4: FL FR BL BR */ {{W1,0},{0,W1},{WH,0},{0,WH}}, + /* 5: FL FR FC BL BR */ {{W1,0},{0,W1},{WH,WH},{WH,0},{0,WH}}, + /* 6: + LFE */ {{W1,0},{0,W1},{WH,WH},{0,0},{WH,0},{0,WH}}, + /* 7: FL FR FC LFE BC SL SR */ + {{W1,0},{0,W1},{WH,WH},{0,0},{WQ,WQ},{WH,0},{0,WH}}, + /* 8: FL FR FC LFE BL BR SL SR */ + {{W1,0},{0,W1},{WH,WH},{0,0},{WH,0},{0,WH},{WH,0},{0,WH}}, +}; + +static void emit(const Decoder& dec, const FrameHeader& h, int16_t* out) { + uint32_t bs = h.block_size; + int bps = h.bits_per_sample; + + if (h.channels == 1) { + for (uint32_t i = 0; i < bs; i++) out[i] = to_s16(dec.chan[0][i], bps); + return; + } + if (h.channels == 2) { + for (uint32_t i = 0; i < bs; i++) { + out[2 * i] = to_s16(dec.chan[0][i], bps); + out[2 * i + 1] = to_s16(dec.chan[1][i], bps); + } + return; + } + + const int16_t (*w)[2] = g_downmix[h.channels]; + int64_t wl = 0, wr = 0; + for (int c = 0; c < h.channels; c++) { wl += w[c][0]; wr += w[c][1]; } + for (uint32_t i = 0; i < bs; i++) { + int64_t l = 0, r = 0; + for (int c = 0; c < h.channels; c++) { + l += (int64_t)w[c][0] * dec.chan[c][i]; + r += (int64_t)w[c][1] * dec.chan[c][i]; + } + out[2 * i] = to_s16(l / wl, bps); + out[2 * i + 1] = to_s16(r / wr, bps); + } +} + +// ============================================================================ +// Public API +// ============================================================================ + +int output_channels(const Decoder& dec) { + return dec.info.channels == 1 ? 1 : 2; +} + +uint64_t total_samples(const Decoder& dec) { + return dec.info.total_samples; +} + +uint64_t find_frame(const Decoder& dec, uint64_t offset) { + FrameHeader h; + for (uint64_t i = offset; i + 1 < dec.size; i++) { + if (dec.data[i] != 0xFF || (dec.data[i + 1] & 0xFE) != 0xF8) continue; + if (parse_header(dec, i, h)) return i; + } + return dec.size; +} + +int decode_frame(Decoder& dec, uint64_t offset, int16_t* out, int out_cap_frames, + uint64_t* next, uint64_t* sample_start) { + FrameHeader h; + if (!parse_header(dec, offset, h)) return -1; + if ((int)h.block_size > out_cap_frames) return -1; + + BitReader br = {dec.data + offset, dec.size - offset, (uint64_t)h.header_bytes * 8, false}; + int side = h.channel_assign == 9 ? 0 : (h.channel_assign >= 8 ? 1 : -1); + bool wide_side = false; + for (int c = 0; c < h.channels; c++) { + // The side channel carries one extra bit. + int bps = h.bits_per_sample + (c == side ? 1 : 0); + bool ok; + if (bps > 32) { + if (!dec.wide) return -1; + ok = decode_subframe(br, dec.wide, dec.chan[c], h.block_size, bps); + wide_side = true; + } else { + ok = decode_subframe(br, dec.chan[c], dec.chan[c], h.block_size, bps); + } + if (!ok) return -1; + } + + uint64_t len = (br.pos + 7) / 8; + if (len + 2 > br.size) return -1; + const uint8_t* p = dec.data + offset; + if (crc16(p, len) != (uint16_t)((p[len] << 8) | p[len + 1])) return -1; + + if (side >= 0) { + if (wide_side) + decorrelate(dec.chan[0], dec.chan[1], dec.wide, h.channel_assign, h.block_size); + else + decorrelate(dec.chan[0], dec.chan[1], dec.chan[side], h.channel_assign, h.block_size); + } + + emit(dec, h, out); + *next = offset + len + 2; + *sample_start = h.sample_start; + return (int)h.block_size; +} + +bool seek(const Decoder& dec, uint64_t sample, uint64_t* offset, uint64_t* frame_start) { + if (dec.index_count <= 0) return false; + int lo = 0, hi = dec.index_count - 1, best = 0; + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + if (dec.index[mid].sample_start <= sample) { + best = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + *offset = dec.index[best].offset; + *frame_start = dec.index[best].sample_start; + return true; +} + +static bool push_index(Decoder& dec, int& cap, uint64_t sample_start, uint64_t offset) { + if (dec.index_count >= cap) { + int new_cap = cap ? cap * 2 : 1024; + auto* grown = (SeekPoint*)montauk::realloc(dec.index, (uint64_t)new_cap * sizeof(SeekPoint)); + if (!grown) return false; + dec.index = grown; + cap = new_cap; + } + dec.index[dec.index_count++] = {sample_start, offset}; + return true; +} + +// Walk the frame chain without decoding: each frame's successor is the next +// valid header whose first sample is exactly where this frame ends, which +// rejects sync patterns that happen to appear inside audio data. +static bool build_index(Decoder& dec) { + int cap = 0; + uint64_t off = find_frame(dec, dec.first_frame); + uint64_t last_end = 0; + FrameHeader h; + + while (off < dec.size && parse_header(dec, off, h)) { + if (!push_index(dec, cap, h.sample_start, off)) return false; + uint64_t want = h.sample_start + h.block_size; + last_end = want; + if (dec.info.total_samples && want >= dec.info.total_samples) break; + + uint64_t found = dec.size; + FrameHeader nh; + for (uint64_t i = off + h.header_bytes + 2; i + 1 < dec.size; i++) { + if (dec.data[i] != 0xFF || (dec.data[i + 1] & 0xFE) != 0xF8) continue; + if (parse_header(dec, i, nh) && nh.sample_start == want) { + found = i; + break; + } + } + off = found; + } + + if (dec.index_count <= 0) return false; + if (!dec.info.total_samples) dec.info.total_samples = last_end; + return true; +} + +void close(Decoder& dec) { + for (int c = 0; c < MAX_CHANNELS; c++) { + if (dec.chan[c]) montauk::mfree(dec.chan[c]); + dec.chan[c] = nullptr; + } + if (dec.wide) montauk::mfree(dec.wide); + dec.wide = nullptr; + if (dec.index) montauk::mfree(dec.index); + dec.index = nullptr; + dec.index_count = 0; + dec.chan_cap = 0; + dec.data = nullptr; + dec.size = 0; +} + +bool open(Decoder& dec, const uint8_t* data, uint64_t size) { + init_crc_tables(); + close(dec); + memset(&dec, 0, sizeof(dec)); + + uint64_t pos = 0; + + // Some taggers prepend ID3v2 to FLAC files. + if (size >= 10 && data[0] == 'I' && data[1] == 'D' && data[2] == '3') { + pos = 10 + (((uint64_t)(data[6] & 0x7F) << 21) | + ((uint64_t)(data[7] & 0x7F) << 14) | + ((uint64_t)(data[8] & 0x7F) << 7) | + ((uint64_t)(data[9] & 0x7F))); + if (data[5] & 0x10) pos += 10; // footer + } + + if (pos + 4 > size || memcmp(data + pos, "fLaC", 4) != 0) return false; + pos += 4; + + bool have_info = false; + for (;;) { + if (pos + 4 > size) return false; + uint8_t type = data[pos]; + uint32_t len = ((uint32_t)data[pos + 1] << 16) | ((uint32_t)data[pos + 2] << 8) | data[pos + 3]; + pos += 4; + if (pos + len > size) return false; + + if ((type & 0x7F) == 0 && len >= 34) { + const uint8_t* d = data + pos; + StreamInfo& si = dec.info; + si.min_block = ((uint32_t)d[0] << 8) | d[1]; + si.max_block = ((uint32_t)d[2] << 8) | d[3]; + si.min_frame_bytes = ((uint32_t)d[4] << 16) | ((uint32_t)d[5] << 8) | d[6]; + si.sample_rate = ((uint32_t)d[10] << 12) | ((uint32_t)d[11] << 4) | (d[12] >> 4); + si.channels = (uint8_t)(((d[12] >> 1) & 7) + 1); + si.bits_per_sample = (uint8_t)((((d[12] & 1) << 4) | (d[13] >> 4)) + 1); + si.total_samples = ((uint64_t)(d[13] & 0x0F) << 32) | + ((uint64_t)d[14] << 24) | ((uint64_t)d[15] << 16) | + ((uint64_t)d[16] << 8) | d[17]; + have_info = true; + } + + pos += len; + if (type & 0x80) break; + } + + StreamInfo& si = dec.info; + if (!have_info || si.sample_rate == 0 || si.max_block == 0 || + si.bits_per_sample < 4 || si.bits_per_sample > 32) + return false; + + dec.data = data; + dec.size = size; + dec.first_frame = pos; + + dec.chan_cap = si.max_block; + for (int c = 0; c < si.channels; c++) { + dec.chan[c] = (int32_t*)montauk::malloc((uint64_t)dec.chan_cap * sizeof(int32_t)); + if (!dec.chan[c]) { close(dec); return false; } + } + + if (si.bits_per_sample == 32 && si.channels == 2) { + dec.wide = (int64_t*)montauk::malloc((uint64_t)dec.chan_cap * sizeof(int64_t)); + if (!dec.wide) { close(dec); return false; } + } + + if (!build_index(dec)) { close(dec); return false; } + return true; +} + +} // namespace flac diff --git a/programs/src/music/flac.hpp b/programs/src/music/flac.hpp new file mode 100644 index 0000000..0b00e08 --- /dev/null +++ b/programs/src/music/flac.hpp @@ -0,0 +1,90 @@ +/* + * flac.hpp + * Native FLAC decoder for the Music player + * Copyright (c) 2026 Daniel Hammer + * + * Decodes a FLAC stream held entirely in memory, one frame at a time, into + * interleaved signed 16-bit PCM (the only format the mixer accepts). Streams + * with more than two channels are downmixed to stereo; 4..32-bit sources are + * scaled to 16 bits. + */ + +#pragma once + +#include + +namespace flac { + +static constexpr int MAX_CHANNELS = 8; +static constexpr int MAX_BLOCK = 65535; + +struct StreamInfo { + uint32_t min_block; + uint32_t max_block; + uint32_t min_frame_bytes; // 0 = unknown + uint32_t sample_rate; + uint8_t channels; + uint8_t bits_per_sample; + uint64_t total_samples; // per channel; 0 = unknown +}; + +struct FrameHeader { + uint64_t sample_start; // first sample (per channel) of this frame + uint32_t block_size; + uint32_t header_bytes; // header length including its CRC-8 + uint8_t channel_assign; // 0-7 independent, 8 L/S, 9 S/R, 10 M/S + uint8_t channels; + uint8_t bits_per_sample; +}; + +struct SeekPoint { + uint64_t sample_start; + uint64_t offset; +}; + +struct Decoder { + StreamInfo info; + const uint8_t* data; + uint64_t size; + uint64_t first_frame; // file offset of the first audio frame + + // Per-channel decode buffers, info.max_block samples each. + int32_t* chan[MAX_CHANNELS]; + uint32_t chan_cap; + + // 32-bit stereo streams code the side channel with 33 bits; it is + // restored here (only allocated for those streams). + int64_t* wide; + + // Frame start index built by open(), sorted by sample_start. + SeekPoint* index; + int index_count; +}; + +// Parse the metadata, allocate buffers and index every frame. Returns false +// for anything that is not a playable FLAC stream. `data` must outlive the +// decoder. +bool open(Decoder& dec, const uint8_t* data, uint64_t size); +void close(Decoder& dec); + +// Channels decode_frame() emits (1 or 2). +int output_channels(const Decoder& dec); + +// Total samples per channel, from STREAMINFO or (if absent) the index. +uint64_t total_samples(const Decoder& dec); + +// Decode the frame at `offset` into `out` (interleaved, output_channels() +// wide, room for `out_cap_frames` frames). On success returns the frame count +// and sets *next to the following frame's offset and *sample_start to the +// frame's first sample. Returns -1 if no valid frame starts at `offset`. +int decode_frame(Decoder& dec, uint64_t offset, int16_t* out, int out_cap_frames, + uint64_t* next, uint64_t* sample_start); + +// Offset of the first valid frame header at or after `offset`, or dec.size. +uint64_t find_frame(const Decoder& dec, uint64_t offset); + +// The last indexed frame starting at or before `sample`. Returns false if the +// stream has no index. +bool seek(const Decoder& dec, uint64_t sample, uint64_t* offset, uint64_t* frame_start); + +} // namespace flac diff --git a/programs/src/music/main.cpp b/programs/src/music/main.cpp index 8ad53b1..cf4cea1 100644 --- a/programs/src/music/main.cpp +++ b/programs/src/music/main.cpp @@ -1,6 +1,6 @@ /* * main.cpp - * MontaukOS Music Player — MP3 and WAV playback + * MontaukOS Music Player — MP3, WAV and FLAC playback * Copyright (c) 2026 Daniel Hammer */ @@ -19,6 +19,7 @@ extern "C" { } #include "minimp3.h" +#include "flac.hpp" #include "visualizer.hpp" using namespace gui; @@ -50,8 +51,11 @@ static constexpr int MAX_FILES = 128; static constexpr int MAX_PATH = 256; static constexpr int MAX_FILTER = 64; -// Decode buffer: one MP3 frame = up to 1152 samples * 2 channels -static constexpr int PCM_BUF_SAMPLES = 1152 * 2; +// Decode buffer: one FLAC block (up to 65535 frames, emitted as at most +// 2 channels) is the largest unit any decoder produces. +static constexpr int PCM_BUF_SAMPLES = flac::MAX_BLOCK * 2; +// WAV PCM is copied in chunks of one MP3 frame's worth (1152 * 2 samples). +static constexpr int WAV_CHUNK_BYTES = 1152 * 2 * 2; // Audio write chunk size (bytes) — feed in small chunks for responsiveness static constexpr int AUDIO_CHUNK = 4096; @@ -72,9 +76,11 @@ static constexpr Color STOP_COLOR = Color::from_rgb(0xCC, 0x33, 0x33); enum class PlayState { Stopped, Playing, Paused }; enum class RepeatMode { Off, All, One }; +enum class AudioFormat { Mp3, Wav, Flac }; + struct FileEntry { char name[128]; - bool is_mp3; // true = mp3, false = wav + AudioFormat format; }; struct Mp3Frame { @@ -130,6 +136,12 @@ struct PlayerState { int sample_rate; int channels; + // FLAC decoder + flac::Decoder flac; + uint64_t flac_offset; // next frame to decode in file_data + uint64_t flac_seek_target; // sample to resume from after a seek + bool flac_seeking; // discard decoded audio before flac_seek_target + // WAV state uint64_t wav_data_offset; // offset to PCM data in file uint64_t wav_data_size; // size of PCM data @@ -371,6 +383,14 @@ static bool str_ends_with(const char* s, const char* suffix) { return true; } +static const char* format_tag(AudioFormat format) { + switch (format) { + case AudioFormat::Mp3: return "MP3"; + case AudioFormat::Flac: return "FLAC"; + default: return "WAV"; + } +} + static const char* repeat_mode_label() { switch (g.repeat_mode) { case RepeatMode::All: return "Rpt All"; @@ -561,16 +581,18 @@ static void scan_directory() { int count = montauk::readdir(g.dir_path, names, 256); for (int i = 0; i < count && g.file_count < MAX_FILES; i++) { - bool is_mp3 = str_ends_with(names[i], ".mp3"); - bool is_wav = str_ends_with(names[i], ".wav"); - if (!is_mp3 && !is_wav) continue; + AudioFormat format; + if (str_ends_with(names[i], ".mp3")) format = AudioFormat::Mp3; + else if (str_ends_with(names[i], ".wav")) format = AudioFormat::Wav; + else if (str_ends_with(names[i], ".flac")) format = AudioFormat::Flac; + else continue; FileEntry& f = g.files[g.file_count]; int len = montauk::slen(names[i]); if (len >= (int)sizeof(f.name)) len = sizeof(f.name) - 1; montauk::memcpy(f.name, names[i], len); f.name[len] = 0; - f.is_mp3 = is_mp3; + f.format = format; g.file_count++; } @@ -730,7 +752,16 @@ static void seek_to_samples(uint64_t target_samples) { g.pcm_buf_len = 0; g.pcm_buf_pos = 0; - if (g.current_track >= 0 && g.files[g.current_track].is_mp3) { + AudioFormat format = g.current_track >= 0 ? g.files[g.current_track].format + : AudioFormat::Wav; + if (format == AudioFormat::Flac) { + uint64_t offset = 0, frame_start = 0; + if (!flac::seek(g.flac, target_samples, &offset, &frame_start)) return; + g.flac_offset = offset; + g.played_samples = frame_start; + g.flac_seek_target = target_samples; + g.flac_seeking = true; + } else if (format == AudioFormat::Mp3) { if (g.mp3_frame_count <= 0) return; int lo = 0; int hi = g.mp3_frame_count - 1; @@ -772,6 +803,9 @@ static void stop_playback() { g.file_data = nullptr; } free_mp3_index(); + flac::close(g.flac); + g.flac_offset = 0; + g.flac_seeking = false; music_visualizer::reset(g.visualizer); g.play_state = PlayState::Stopped; g.played_samples = 0; @@ -795,7 +829,7 @@ static bool start_track(int index) { if (fh < 0) return false; g.file_size = montauk::getsize(fh); - if (g.file_size == 0 || g.file_size > 64 * 1024 * 1024) { // 64 MiB limit + if (g.file_size == 0 || g.file_size > 256 * 1024 * 1024) { // 256 MiB limit (hi-res FLAC) montauk::close(fh); return false; } @@ -824,7 +858,19 @@ static bool start_track(int index) { } // Parse format - if (g.files[index].is_mp3) { + AudioFormat format = g.files[index].format; + if (format == AudioFormat::Flac) { + if (!flac::open(g.flac, g.file_data, g.file_size)) { + montauk::mfree(g.file_data); + g.file_data = nullptr; + return false; + } + g.sample_rate = (int)g.flac.info.sample_rate; + g.channels = flac::output_channels(g.flac); + g.total_samples = flac::total_samples(g.flac); + g.flac_offset = g.flac.index[0].offset; + g.flac_seeking = false; + } else if (format == AudioFormat::Mp3) { mp3dec_init(&g.mp3dec); g.mp3_offset = 0; @@ -853,6 +899,7 @@ static bool start_track(int index) { g.audio_handle = montauk::audio_open(g.sample_rate, g.channels, 16); if (g.audio_handle < 0) { free_mp3_index(); + flac::close(g.flac); montauk::mfree(g.file_data); g.file_data = nullptr; return false; @@ -959,7 +1006,47 @@ static void feed_audio() { g.pcm_buf_pos = 0; g.pcm_buf_len = 0; - if (g.current_track >= 0 && g.files[g.current_track].is_mp3) { + AudioFormat format = g.current_track >= 0 ? g.files[g.current_track].format + : AudioFormat::Wav; + if (format == AudioFormat::Flac) { + uint64_t next = 0, frame_start = 0; + int frames = flac::decode_frame(g.flac, g.flac_offset, g.pcm_buf, + PCM_BUF_SAMPLES / g.channels, + &next, &frame_start); + if (frames < 0) { + // Corrupt or truncated frame: resync on the next valid header. + g.flac_offset = flac::find_frame(g.flac, g.flac_offset + 1); + if (g.flac_offset >= g.file_size) { + next_track(true); + return; + } + continue; + } + g.flac_offset = next; + + // Position from the frame itself, so a seek point that landed + // somewhere unexpected still reports and trims correctly. + g.played_samples = frame_start; + if (g.flac_seeking) { + g.flac_seeking = false; + if (g.flac_seek_target > frame_start) { + uint64_t discard = g.flac_seek_target - frame_start; + if (discard > (uint64_t)frames) discard = (uint64_t)frames; + g.pcm_buf_pos = (int)(discard * (uint64_t)g.channels); + g.played_samples += discard; + frames -= (int)discard; + } + } + + g.pcm_buf_len = frames * g.channels; + if (g.pcm_buf_len > 0) { + music_visualizer::feed_pcm(g.visualizer, + g.pcm_buf + g.pcm_buf_pos, + frames, g.channels, g.sample_rate); + } else { + g.pcm_buf_pos = 0; + } + } else if (format == AudioFormat::Mp3) { // MP3 decode if (g.mp3_offset >= g.file_size) { // End of file — advance to next track @@ -1009,7 +1096,7 @@ static void feed_audio() { uint64_t remaining = g.wav_data_size - g.wav_pos; uint64_t to_copy = remaining; - if (to_copy > sizeof(g.pcm_buf)) to_copy = sizeof(g.pcm_buf); + if (to_copy > WAV_CHUNK_BYTES) to_copy = WAV_CHUNK_BYTES; memcpy(g.pcm_buf, g.file_data + g.wav_data_offset + g.wav_pos, to_copy); g.wav_pos += to_copy; @@ -1195,7 +1282,7 @@ static void render(uint32_t* pixels) { px_text(pixels, W, H, 24, iy + (LIST_ITEM_H - fh_sm) / 2, g.files[idx].name, name_color, FONT_SIZE_SM); - const char* tag = g.files[idx].is_mp3 ? "MP3" : "WAV"; + const char* tag = format_tag(g.files[idx].format); int tw = text_w(tag, FONT_SIZE_SM); px_text(pixels, W, H, tag_right - tw - 12, iy + (LIST_ITEM_H - fh_sm) / 2, tag, DIM_TEXT, FONT_SIZE_SM); @@ -1521,7 +1608,8 @@ extern "C" void _start() { char args[256]; int arglen = montauk::getargs(args, sizeof(args)); if (arglen > 0 && args[0]) { - if (str_ends_with(args, ".mp3") || str_ends_with(args, ".wav")) { + if (str_ends_with(args, ".mp3") || str_ends_with(args, ".wav") || + str_ends_with(args, ".flac")) { // File path: extract directory and filename int last_slash = -1; for (int i = 0; args[i]; i++) { @@ -1587,7 +1675,7 @@ extern "C" void _start() { scan_directory(); // Auto-size window width so the longest track name fits without truncation. - // Layout per row: 24px left pad + name + 8px gap + tag ("MP3"/"WAV") + 12px + // Layout per row: 24px left pad + name + 8px gap + tag ("MP3"/"WAV"/"FLAC") + 12px // right pad, plus ~10px for the scrollbar gutter. { int max_name_w = 0; @@ -1595,9 +1683,12 @@ extern "C" void _start() { int w = text_w(g.files[i].name, FONT_SIZE_SM); if (w > max_name_w) max_name_w = w; } - int tag_w = text_w("MP3", FONT_SIZE_SM); - int wav_w = text_w("WAV", FONT_SIZE_SM); - if (wav_w > tag_w) tag_w = wav_w; + int tag_w = 0; + static constexpr AudioFormat formats[] = {AudioFormat::Mp3, AudioFormat::Wav, AudioFormat::Flac}; + for (AudioFormat f : formats) { + int w = text_w(format_tag(f), FONT_SIZE_SM); + if (w > tag_w) tag_w = w; + } int needed_w = 24 + max_name_w + 8 + tag_w + 12 + 10; if (needed_w > g.win_w) g.win_w = needed_w; if (g.win_w > 900) g.win_w = 900; diff --git a/programs/src/music/manifest.toml b/programs/src/music/manifest.toml index dc37971..3fdb72b 100644 --- a/programs/src/music/manifest.toml +++ b/programs/src/music/manifest.toml @@ -12,4 +12,4 @@ pass_home_dir = false [files] # Extensions Files opens with this app (lowercase, no dot) -types = ["mp3", "wav"] +types = ["mp3", "wav", "flac"]