#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[racket-audio/audio-sniffer] 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} ]