174 lines
4.8 KiB
Racket
174 lines
4.8 KiB
Racket
#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}
|
|
]
|