feat: duration and seeking for in-progress FLAC recordings
FLAC duration cannot be derived from byte size (variable compression), so unlike WAV the header cannot be patched from st_size alone. Instead, every FLAC frame header carries its own frame/sample number: read the last 64 KB of the growing file, scan backwards for a frame sync, CRC-8-verify the header to reject false matches in compressed data, and compute the exact samples recorded so far. STREAMINFO total_samples (36 bits at a fixed offset) is rewritten in the served bytes only - the on-disk file is never touched. Overhead: one tail read per /stream request, active files only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -93,6 +93,124 @@ def _live_wav_header(path: Path, size: int):
|
||||
return None
|
||||
|
||||
|
||||
# CRC-8 (poly 0x07) used by FLAC frame headers
|
||||
_CRC8_TABLE = []
|
||||
for _i in range(256):
|
||||
_c = _i
|
||||
for _ in range(8):
|
||||
_c = ((_c << 1) ^ 0x07) & 0xFF if _c & 0x80 else (_c << 1) & 0xFF
|
||||
_CRC8_TABLE.append(_c)
|
||||
|
||||
_FLAC_BLOCKSIZES = {1: 192, 2: 576, 3: 1152, 4: 2304, 5: 4608, 8: 256, 9: 512,
|
||||
10: 1024, 11: 2048, 12: 4096, 13: 8192, 14: 16384, 15: 32768}
|
||||
|
||||
|
||||
def _crc8(data: bytes) -> int:
|
||||
crc = 0
|
||||
for b in data:
|
||||
crc = _CRC8_TABLE[crc ^ b]
|
||||
return crc
|
||||
|
||||
|
||||
def _flac_coded_number(buf: bytes, pos: int):
|
||||
"""Decode the UTF-8-style frame/sample number; returns (value, next_pos)."""
|
||||
b0 = buf[pos]
|
||||
if b0 < 0x80:
|
||||
return b0, pos + 1
|
||||
n, mask = 0, 0x40
|
||||
while b0 & mask:
|
||||
n += 1
|
||||
mask >>= 1
|
||||
if n < 1 or n > 6: # 10xxxxxx is not a valid leading byte
|
||||
return None
|
||||
val = b0 & (mask - 1)
|
||||
for i in range(1, n + 1):
|
||||
c = buf[pos + i]
|
||||
if c & 0xC0 != 0x80:
|
||||
return None
|
||||
val = (val << 6) | (c & 0x3F)
|
||||
return val, pos + 1 + n
|
||||
|
||||
|
||||
def _flac_frame_samples(buf: bytes, pos: int, fixed_bs: int):
|
||||
"""If a valid FLAC frame header starts at pos, return the stream sample
|
||||
count through the end of that frame, else None. Validity is confirmed by
|
||||
the header's CRC-8, so false sync matches in compressed data are rejected."""
|
||||
try:
|
||||
variable = buf[pos + 1] & 0x01
|
||||
bs_code = buf[pos + 2] >> 4
|
||||
sr_code = buf[pos + 2] & 0x0F
|
||||
if bs_code == 0 or sr_code == 15 or buf[pos + 3] & 0x01:
|
||||
return None
|
||||
if (buf[pos + 3] >> 4) > 10: # reserved channel assignment
|
||||
return None
|
||||
coded = _flac_coded_number(buf, pos + 4)
|
||||
if coded is None:
|
||||
return None
|
||||
val, p = coded
|
||||
bs = _FLAC_BLOCKSIZES.get(bs_code)
|
||||
if bs_code == 6:
|
||||
bs = buf[p] + 1
|
||||
p += 1
|
||||
elif bs_code == 7:
|
||||
bs = int.from_bytes(buf[p:p + 2], 'big') + 1
|
||||
p += 2
|
||||
if sr_code == 12:
|
||||
p += 1
|
||||
elif sr_code in (13, 14):
|
||||
p += 2
|
||||
if _crc8(buf[pos:p]) != buf[p]:
|
||||
return None
|
||||
if variable: # val is the frame's starting sample number
|
||||
return val + (bs or 0)
|
||||
return val * (fixed_bs or bs or 4096) + (bs or 0)
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
def _live_flac_header(path: Path, size: int):
|
||||
"""Return the first 26 bytes of a FLAC file with STREAMINFO total_samples
|
||||
patched to the samples recorded so far, or None.
|
||||
|
||||
Like _live_wav_header, but FLAC duration cannot be derived from the byte
|
||||
count (variable compression). Instead the sample count is parsed out of
|
||||
the last frame header in the file tail — each FLAC frame carries its own
|
||||
frame/sample number.
|
||||
"""
|
||||
try:
|
||||
with open(path, 'rb') as fh:
|
||||
head = fh.read(42)
|
||||
if len(head) < 42 or head[:4] != b'fLaC':
|
||||
return None
|
||||
# STREAMINFO must be the first metadata block
|
||||
if head[4] & 0x7F != 0 or int.from_bytes(head[5:8], 'big') != 34:
|
||||
return None
|
||||
fixed_bs = int.from_bytes(head[8:10], 'big')
|
||||
|
||||
tail_len = min(size, 65536)
|
||||
fh.seek(size - tail_len)
|
||||
buf = fh.read(tail_len)
|
||||
|
||||
samples = None
|
||||
for i in range(len(buf) - 20, -1, -1):
|
||||
if buf[i] == 0xFF and (buf[i + 1] & 0xFC) == 0xF8:
|
||||
samples = _flac_frame_samples(buf, i, fixed_bs)
|
||||
if samples:
|
||||
break
|
||||
if not samples:
|
||||
return None
|
||||
|
||||
# Bytes 18-25 hold: sample rate (20 bits) | channels-1 (3) |
|
||||
# bps-1 (5) | total_samples (36). Replace only the low 36 bits.
|
||||
field = int.from_bytes(head[18:26], 'big')
|
||||
field = (field & ~((1 << 36) - 1)) | min(samples, (1 << 36) - 1)
|
||||
patched = bytearray(head[:26])
|
||||
patched[18:26] = field.to_bytes(8, 'big')
|
||||
return bytes(patched)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_audio_duration(path: Path):
|
||||
"""Return duration in seconds for any supported audio file, or None."""
|
||||
ext = path.suffix.lower()
|
||||
@@ -491,8 +609,8 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
"""Serve audio for inline playback with HTTP Range support.
|
||||
|
||||
In-progress recordings are served with Cache-Control: no-store (the
|
||||
content is still growing) and, for WAV, with a header patched to the
|
||||
current size so the browser can show a duration and seek.
|
||||
content is still growing) and, for WAV/FLAC, with a header patched to
|
||||
the duration recorded so far so the browser can show it and seek.
|
||||
"""
|
||||
path = self._safe_path(filename)
|
||||
if path is None:
|
||||
@@ -503,8 +621,12 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
is_active = self._is_active(filename)
|
||||
|
||||
prefix = b''
|
||||
if is_active and path.suffix.lower() == '.wav':
|
||||
prefix = _live_wav_header(path, size) or b''
|
||||
if is_active:
|
||||
ext = path.suffix.lower()
|
||||
if ext == '.wav':
|
||||
prefix = _live_wav_header(path, size) or b''
|
||||
elif ext == '.flac':
|
||||
prefix = _live_flac_header(path, size) or b''
|
||||
|
||||
range_header = self.headers.get('Range', '')
|
||||
m = re.match(r'bytes=(\d+)-(\d*)', range_header) if range_header else None
|
||||
|
||||
Reference in New Issue
Block a user