From e9e63ce1753630737ad0379e5dd131d5c1ea38ad Mon Sep 17 00:00:00 2001 From: Unlock Music Dev Date: Tue, 6 Dec 2022 21:47:33 +0800 Subject: [PATCH] feat(qmc): improve m4a & mp4 sniff --- algo/qmc/qmc.go | 2 +- cmd/um/main.go | 2 +- go.sum | 2 -- internal/sniff/audio.go | 55 +++++++++++++++++++++++++++++++++++++---- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/algo/qmc/qmc.go b/algo/qmc/qmc.go index b709c8c..3347882 100644 --- a/algo/qmc/qmc.go +++ b/algo/qmc/qmc.go @@ -90,7 +90,7 @@ func (d *Decoder) validateDecode() error { return fmt.Errorf("qmc seek to start: %w", err) } - buf := make([]byte, 16) + buf := make([]byte, 64) if _, err := io.ReadFull(d.raw, buf); err != nil { return fmt.Errorf("qmc read header: %w", err) } diff --git a/cmd/um/main.go b/cmd/um/main.go index 4eae5e8..62eb144 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -186,7 +186,7 @@ func tryDecFile(inputFile string, outputDir string, allDec []common.NewDecoderFu } header := bytes.NewBuffer(nil) - _, err = io.CopyN(header, dec, 16) + _, err = io.CopyN(header, dec, 64) if err != nil { return fmt.Errorf("read header failed: %w", err) } diff --git a/go.sum b/go.sum index f3acf8d..c86c908 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,6 @@ golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb h1:QIsP/NmClBICkqnJ4rSIhnrGiGR7Yv9ZORGGnmmLTPk= golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/sniff/audio.go b/internal/sniff/audio.go index 307f626..2f36276 100644 --- a/internal/sniff/audio.go +++ b/internal/sniff/audio.go @@ -1,6 +1,11 @@ package sniff -import "bytes" +import ( + "bytes" + "encoding/binary" + + "golang.org/x/exp/slices" +) type Sniffer interface { Sniff(header []byte) bool @@ -19,8 +24,8 @@ var audioExtensions = map[string]Sniffer{ }, // ref: https://www.garykessler.net/library/file_sigs.html - ".m4a": mpeg4Sniffer{}, // MPEG-4 container, m4a treat as audio - ".aac": prefixSniffer{0xFF, 0xF1}, // MPEG-4 AAC-LC + ".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 @@ -54,8 +59,48 @@ 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 (mpeg4Sniffer) Sniff(header []byte) bool { - return len(header) >= 8 && bytes.Equal([]byte("ftyp"), header[4:8]) +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 }