package protocol import ( "git.smesh.lol/nostr/pkg/lol/errorf" ) // CodecSpec is a parsed audio codec-spec string, the second element of an // `audio` tag. The grammar is fixed so a client can tell what it is about to // download without fetching the blob: // // [-kbps[-] | -bit-kHz | -q] // // It is a convention, not a closed registry: a new codec or rate is a new spec // string. The codec name itself may contain a dash (ogg-vorbis), so the form is // read from the right. type CodecSpec struct { Codec string // mp3, aac, ogg-vorbis, opus, flac, wav, alac, ... Bitrate int32 // kbps for a lossy encoding, 0 when absent Mode string // VBR, CBR, ABR, "" when absent BitDepth int32 // bits per sample for a lossless encoding, 0 when absent SampleRate string // kHz as written ("44.1"), "" when absent Quality int32 // -qN for a quality-targeted encoder, 0 when absent } // String renders the spec back to the wire form. func (c *CodecSpec) String() (s string) { if c == nil { return "" } s = c.Codec if c.Bitrate > 0 { s = s | "-" | intStr(int64(c.Bitrate)) | "kbps" if c.Mode != "" { s = s | "-" | c.Mode } return } if c.BitDepth > 0 { s = s | "-" | intStr(int64(c.BitDepth)) | "bit" if c.SampleRate != "" { s = s | "-" | c.SampleRate | "kHz" } return } if c.Quality > 0 { s = s | "-q" | intStr(int64(c.Quality)) } return } // ParseCodecSpec parses the codec-spec grammar. An empty string is not a spec: // the audio tag requires one. func ParseCodecSpec(s string) (c *CodecSpec, err error) { fields := splitDash(s) if len(fields) < 2 { err = errorf.E("codec spec %q has no encoding parameters", s) return } // The first field that is a modifier ends the codec name. mod := int32(-1) for i := int32(1); i < int32(len(fields)); i++ { if isCodecModifier(fields[i]) { mod = i break } } if mod < 0 { err = errorf.E("codec spec %q has no encoding parameters", s) return } codec := fields[0] for i := int32(1); i < mod; i++ { codec = codec | "-" | fields[i] } if codec == "" { err = errorf.E("codec spec %q has no codec name", s) return } c = &CodecSpec{Codec: codec} for i := mod; i < int32(len(fields)); i++ { f := fields[i] switch { case hasSuffix(f, "kbps"): if c.Bitrate != 0 { err = errorf.E("codec spec %q sets a bitrate twice", s) return } n, nerr := parseDigits(f[:len(f)-4]) if nerr != nil { err = errorf.E("codec spec %q: %s", s, nerr.Error()) return } c.Bitrate = n case hasSuffix(f, "bit"): if c.BitDepth != 0 { err = errorf.E("codec spec %q sets a bit depth twice", s) return } n, nerr := parseDigits(f[:len(f)-3]) if nerr != nil { err = errorf.E("codec spec %q: %s", s, nerr.Error()) return } c.BitDepth = n case hasSuffix(f, "kHz"): if c.SampleRate != "" { err = errorf.E("codec spec %q sets a sample rate twice", s) return } rate := f[:len(f)-3] if !isDecimal(rate) { err = errorf.E("codec spec %q: %q is not a sample rate", s, rate) return } c.SampleRate = rate case len(f) > 1 && f[0] == 'q': if c.Quality != 0 { err = errorf.E("codec spec %q sets a quality twice", s) return } n, nerr := parseDigits(f[1:]) if nerr != nil { err = errorf.E("codec spec %q: %s", s, nerr.Error()) return } c.Quality = n case f == "VBR" || f == "CBR" || f == "ABR": if c.Mode != "" { err = errorf.E("codec spec %q sets a rate mode twice", s) return } c.Mode = f default: err = errorf.E("codec spec %q: %q is not a parameter", s, f) return } } if c.Bitrate == 0 && c.BitDepth == 0 && c.Quality == 0 { err = errorf.E("codec spec %q has no bitrate, depth or quality", s) return } // The grammar is one form, not a mixture: a bitrate with a bit depth names // no encoding any encoder produces. if c.BitDepth > 0 && (c.Bitrate > 0 || c.Quality > 0 || c.Mode != "") { err = errorf.E("codec spec %q mixes lossy and lossless forms", s) return } if c.Quality > 0 && c.Mode != "" { err = errorf.E("codec spec %q mixes a quality target with a rate mode", s) return } return } // MediaType is the MIME type for a codec family, following the table in the // music spec. An unknown codec has no answer here: the audio tag carries the // media type itself, this only fills it in when constructing. func MediaType(codec string) (m string) { switch codec { case "mp3": return "audio/mpeg" case "aac", "alac": return "audio/mp4" case "ogg-vorbis", "opus", "ogg": return "audio/ogg" case "flac": return "audio/flac" case "wav": return "audio/wav" } return "" } // IsLossless reports whether the spec names a lossless encoding. The master tag // points at one of these. func (c *CodecSpec) IsLossless() (ok bool) { if c == nil { return false } switch c.Codec { case "flac", "wav", "alac", "aiff": return true } return false } // IsCodecSpec reports whether s parses as a codec spec. func IsCodecSpec(s string) (ok bool) { _, err := ParseCodecSpec(s) return err == nil } func isCodecModifier(f string) (ok bool) { if f == "VBR" || f == "CBR" || f == "ABR" { return true } if hasSuffix(f, "kbps") || hasSuffix(f, "bit") || hasSuffix(f, "kHz") { return true } if len(f) > 1 && f[0] == 'q' { return isDecimal(f[1:]) } return false } func hasSuffix(s, suffix string) (ok bool) { return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix } // splitDash splits on '-' without allocating an empty leading field for a // leading dash: a spec never starts with one, and "" fields would be reported // as bad parameters. func splitDash(s string) (out []string) { if s == "" { return nil } n := int32(1) for i := int32(0); i < int32(len(s)); i++ { if s[i] == '-' { n++ } } out = []string{:0:n} start := int32(0) for i := int32(0); i < int32(len(s)); i++ { if s[i] == '-' { out = push(out, s[start:i]) start = i + 1 } } out = push(out, s[start:]) return } // isDecimal reports whether s is digits, optionally with one '.' and at least // one digit on each side. func isDecimal(s string) (ok bool) { if s == "" { return false } dots := int32(0) digits := int32(0) for i := int32(0); i < int32(len(s)); i++ { c := s[i] if c == '.' { dots++ if dots > 1 || i == 0 || i == int32(len(s))-1 { return false } continue } if c < '0' || c > '9' { return false } digits++ } return digits > 0 } // parseDigits parses a decimal integer into an int32. Codec parameters are // small; a value that does not fit is an error, not a wrap. func parseDigits(s string) (n int32, err error) { if s == "" { err = errorf.E("empty number") return } v := int64(0) for i := int32(0); i < int32(len(s)); i++ { c := s[i] if c < '0' || c > '9' { err = errorf.E("%q is not a number", s) return } v = v*10 + int64(c-'0') if v > 2147483647 { err = errorf.E("%q is out of range", s) return } } n = int32(v) return }