From 57600d747c427f953040f1de025e5b6c7a72e6a5 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 28 Apr 2026 23:14:39 +0200 Subject: [PATCH] audio sniffer docs --- scrbl/audio-sniffer.scrbl | 173 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 scrbl/audio-sniffer.scrbl diff --git a/scrbl/audio-sniffer.scrbl b/scrbl/audio-sniffer.scrbl new file mode 100644 index 0000000..83c0dad --- /dev/null +++ b/scrbl/audio-sniffer.scrbl @@ -0,0 +1,173 @@ +#lang scribble/manual + +@(require (for-label racket/base + racket/contract + "../audio-sniffer.rkt")) + +@title{audio-sniffer} +@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] + +@defmodule[(file "../audio-sniffer.rkt")] + +This module provides functionality to detect audio file formats based on +file contents (signature sniffing) and, optionally, file extensions. + +The sniffer prefers binary inspection over extensions and only falls back +to extensions when detection is inconclusive. + +@section{Overview} + +The detection strategy is as follows: + +@itemlist[ + #:style 'compact + @item{Read a prefix of the file (default 4096 bytes)} + @item{Match known binary signatures ("magic numbers")} + @item{Apply format-specific heuristics (e.g. MP3 frame sync, AAC ADTS)} + @item{For ISO-BMFF (MP4/M4A), scan both head and tail for codec markers} + @item{If still unknown, optionally fall back to file extension} +] + +The result is always a symbol describing the detected format or a status. + +@section{Formats} + +Known audio formats: + +@racketblock[ +'(mp3 flac ogg vorbis opus wav aiff + mp4 aac alac encrypted-audio + ac3 ape wavpack wma matroska) +] + +Status values: + +@racketblock[ +'(unknown file-not-found file-not-readable not-a-file) +] + +@section{API} + +@defproc[(audio-format? [v any/c]) boolean?]{ +Returns @racket[#t] if @racket[v] is a known audio format or status symbol. +} + +@defproc[(audio-sniff-format [file path-string?]) audio-format?]{ + +Detects the audio format of @racket[file] using binary inspection only. + +Returns one of: + +@itemlist[ + #:style 'compact + @item{A format symbol such as @racket['mp3], @racket['flac], etc.} + @item{A status symbol such as @racket['file-not-found]} +] + +This function does not use the file extension. +} + +@defproc[(audio-sniff-format/extension [file path-string?]) audio-format?]{ + +Like @racket[audio-sniff-format], but falls back to the file extension +if content-based detection returns @racket['unknown]. + +This is typically the preferred entry point in user-facing code. +} + +@defproc[(audio-sniff-extension [file path-string?]) (or/c string? #f)]{ + +Returns the lowercase file extension (without dot), or @racket[#f] +if no extension is present. +} + +@defproc[(audio-format-known? [fmt symbol?]) boolean?]{ + +Returns @racket[#t] if @racket[fmt] is a known audio format +(excludes status symbols). +} + +@defproc[(audio-format-matches? [file path-string?] + [formats (listof symbol?)]) + boolean?]{ + +Returns @racket[#t] if the detected format of @racket[file] matches +one of @racket[formats]. + +Detection uses @racket[audio-sniff-format/extension]. +} + +@section{Architecture} + +The sniffer is structured as a layered pipeline: + +@itemlist[ + #:style 'compact + @item{@bold{I/O layer} -- reads byte ranges from the file (head and tail)} + @item{@bold{Signature layer} -- matches fixed binary identifiers} + @item{@bold{Heuristic layer} -- validates formats without fixed headers} + @item{@bold{Container layer} -- inspects structured containers (MP4, Ogg)} + @item{@bold{Fallback layer} -- maps file extensions to formats} +] + +Detection proceeds from cheap and deterministic checks to more +expensive or heuristic ones. + +MP4/M4A detection is handled separately because codec identifiers may +appear outside the initial header. For this reason both the beginning +and the end of the file are scanned. + +The sniffer is deliberately stateless; each call operates only on the +given file and does not cache results. + +@section{Detection Details} + +Binary signatures are used where possible: + +@itemlist[ + #:style 'compact + @item{@bold{FLAC}: @"fLaC"} + @item{@bold{Ogg}: @"OggS" + subtype detection (Opus/Vorbis/FLAC)} + @item{@bold{WAV}: RIFF/WAVE} + @item{@bold{AIFF}: FORM/AIFF or AIFC} + @item{@bold{ASF/WMA}: GUID header} + @item{@bold{Matroska}: EBML header} + @item{@bold{AC3}: 0x0B77 sync word} + @item{@bold{APE}: @"MAC "} + @item{@bold{WavPack}: @"wvpk"} +] + +Heuristics are applied for: + +@itemlist[ + #:style 'compact + @item{MP3 (ID3 header or frame sync validation)} + @item{AAC (ADTS sync pattern)} +] + +MP4/M4A detection: + +@itemlist[ + #:style 'compact + @item{Detect ISO-BMFF via @"ftyp"} + @item{Scan for codec markers: @"mp4a", @"alac", @"enca"} + @item{Perform additional scanning near the end of the file} +] + +@section{Why not use FFmpeg?} + +The primary reason for implementing a custom sniffer is performance. + +Format detection in this module is intentionally lightweight: it reads +only small portions of the file and applies simple, deterministic checks. +In most cases, detection completes after inspecting just a few kilobytes. + +Using a library such as FFmpeg would significantly increase the cost of +this operation: + +@itemlist[ + #:style 'compact + @item{@bold{Startup overhead} -- initialization of codec infrastructure} + @item{@bold{I/O overhead} -- more data is typically read than necessary} + @item{@bold{Processing overhead} -- partial parsing of streams or containers} +]