cli/internal/sniff/audio.go

107 lines
2.7 KiB
Go

package sniff
import (
"bytes"
"encoding/binary"
"golang.org/x/exp/slices"
)
type Sniffer interface {
Sniff(header []byte) bool
}
var audioExtensions = map[string]Sniffer{
// ref: https://mimesniff.spec.whatwg.org
".mp3": prefixSniffer("ID3"), // todo: check mp3 without ID3v2 tag
".ogg": prefixSniffer("OggS"),
".wav": prefixSniffer("RIFF"),
// ref: https://www.loc.gov/preservation/digital/formats/fdd/fdd000027.shtml
".wma": prefixSniffer{
0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11,
0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c,
},
// ref: https://www.garykessler.net/library/file_sigs.html
".m4a": m4aSniffer{}, // MPEG-4 container, Apple Lossless Audio Codec
".mp4": &mpeg4Sniffer{}, // MPEG-4 container, other fallback
".flac": prefixSniffer("fLaC"), // ref: https://xiph.org/flac/format.html
".dff": prefixSniffer("FRM8"), // DSDIFF, ref: https://www.sonicstudio.com/pdf/dsd/DSDIFF_1.5_Spec.pdf
}
// AudioExtension sniffs the known audio types, and returns the file extension.
// header is recommended to at least 16 bytes.
func AudioExtension(header []byte) (string, bool) {
for ext, sniffer := range audioExtensions {
if sniffer.Sniff(header) {
return ext, true
}
}
return "", false
}
// AudioExtensionWithFallback is equivalent to AudioExtension, but returns fallback
// most likely to use .mp3 as fallback, because mp3 files may not have ID3v2 tag.
func AudioExtensionWithFallback(header []byte, fallback string) string {
ext, ok := AudioExtension(header)
if !ok {
return fallback
}
return ext
}
type prefixSniffer []byte
func (s prefixSniffer) Sniff(header []byte) bool {
return bytes.HasPrefix(header, s)
}
type m4aSniffer struct{}
func (m4aSniffer) Sniff(header []byte) bool {
box := readMpeg4FtypBox(header)
if box == nil {
return false
}
return box.majorBrand == "M4A " || slices.Contains(box.compatibleBrands, "M4A ")
}
type mpeg4Sniffer struct{}
func (s *mpeg4Sniffer) Sniff(header []byte) bool {
return readMpeg4FtypBox(header) != nil
}
type mpeg4FtpyBox struct {
majorBrand string
minorVersion uint32
compatibleBrands []string
}
func readMpeg4FtypBox(header []byte) *mpeg4FtpyBox {
if (len(header) < 8) || !bytes.Equal([]byte("ftyp"), header[4:8]) {
return nil // not a valid ftyp box
}
size := binary.BigEndian.Uint32(header[0:4]) // size
if size < 16 || size%4 != 0 {
return nil // invalid ftyp box
}
box := mpeg4FtpyBox{
majorBrand: string(header[8:12]),
minorVersion: binary.BigEndian.Uint32(header[12:16]),
}
// compatible brands
for i := 16; i < int(size) && i+4 < len(header); i += 4 {
box.compatibleBrands = append(box.compatibleBrands, string(header[i:i+4]))
}
return &box
}