initial import from racket-sound -> racket-audio
This commit is contained in:
@@ -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}
|
||||
]
|
||||
Reference in New Issue
Block a user