1 package protocol
2 3 import (
4 "git.smesh.lol/nostr/pkg/lol/errorf"
5 )
6 7 // CodecSpec is a parsed audio codec-spec string, the second element of an
8 // `audio` tag. The grammar is fixed so a client can tell what it is about to
9 // download without fetching the blob:
10 //
11 // <codec>[-<bitrate>kbps[-<VBR|CBR|ABR>] | -<bit-depth>bit-<sample-rate>kHz | -q<quality>]
12 //
13 // It is a convention, not a closed registry: a new codec or rate is a new spec
14 // string. The codec name itself may contain a dash (ogg-vorbis), so the form is
15 // read from the right.
16 type CodecSpec struct {
17 Codec string // mp3, aac, ogg-vorbis, opus, flac, wav, alac, ...
18 Bitrate int32 // kbps for a lossy encoding, 0 when absent
19 Mode string // VBR, CBR, ABR, "" when absent
20 BitDepth int32 // bits per sample for a lossless encoding, 0 when absent
21 SampleRate string // kHz as written ("44.1"), "" when absent
22 Quality int32 // -qN for a quality-targeted encoder, 0 when absent
23 }
24 25 // String renders the spec back to the wire form.
26 func (c *CodecSpec) String() (s string) {
27 if c == nil {
28 return ""
29 }
30 s = c.Codec
31 if c.Bitrate > 0 {
32 s = s | "-" | intStr(int64(c.Bitrate)) | "kbps"
33 if c.Mode != "" {
34 s = s | "-" | c.Mode
35 }
36 return
37 }
38 if c.BitDepth > 0 {
39 s = s | "-" | intStr(int64(c.BitDepth)) | "bit"
40 if c.SampleRate != "" {
41 s = s | "-" | c.SampleRate | "kHz"
42 }
43 return
44 }
45 if c.Quality > 0 {
46 s = s | "-q" | intStr(int64(c.Quality))
47 }
48 return
49 }
50 51 // ParseCodecSpec parses the codec-spec grammar. An empty string is not a spec:
52 // the audio tag requires one.
53 func ParseCodecSpec(s string) (c *CodecSpec, err error) {
54 fields := splitDash(s)
55 if len(fields) < 2 {
56 err = errorf.E("codec spec %q has no encoding parameters", s)
57 return
58 }
59 // The first field that is a modifier ends the codec name.
60 mod := int32(-1)
61 for i := int32(1); i < int32(len(fields)); i++ {
62 if isCodecModifier(fields[i]) {
63 mod = i
64 break
65 }
66 }
67 if mod < 0 {
68 err = errorf.E("codec spec %q has no encoding parameters", s)
69 return
70 }
71 codec := fields[0]
72 for i := int32(1); i < mod; i++ {
73 codec = codec | "-" | fields[i]
74 }
75 if codec == "" {
76 err = errorf.E("codec spec %q has no codec name", s)
77 return
78 }
79 c = &CodecSpec{Codec: codec}
80 for i := mod; i < int32(len(fields)); i++ {
81 f := fields[i]
82 switch {
83 case hasSuffix(f, "kbps"):
84 if c.Bitrate != 0 {
85 err = errorf.E("codec spec %q sets a bitrate twice", s)
86 return
87 }
88 n, nerr := parseDigits(f[:len(f)-4])
89 if nerr != nil {
90 err = errorf.E("codec spec %q: %s", s, nerr.Error())
91 return
92 }
93 c.Bitrate = n
94 case hasSuffix(f, "bit"):
95 if c.BitDepth != 0 {
96 err = errorf.E("codec spec %q sets a bit depth twice", s)
97 return
98 }
99 n, nerr := parseDigits(f[:len(f)-3])
100 if nerr != nil {
101 err = errorf.E("codec spec %q: %s", s, nerr.Error())
102 return
103 }
104 c.BitDepth = n
105 case hasSuffix(f, "kHz"):
106 if c.SampleRate != "" {
107 err = errorf.E("codec spec %q sets a sample rate twice", s)
108 return
109 }
110 rate := f[:len(f)-3]
111 if !isDecimal(rate) {
112 err = errorf.E("codec spec %q: %q is not a sample rate", s, rate)
113 return
114 }
115 c.SampleRate = rate
116 case len(f) > 1 && f[0] == 'q':
117 if c.Quality != 0 {
118 err = errorf.E("codec spec %q sets a quality twice", s)
119 return
120 }
121 n, nerr := parseDigits(f[1:])
122 if nerr != nil {
123 err = errorf.E("codec spec %q: %s", s, nerr.Error())
124 return
125 }
126 c.Quality = n
127 case f == "VBR" || f == "CBR" || f == "ABR":
128 if c.Mode != "" {
129 err = errorf.E("codec spec %q sets a rate mode twice", s)
130 return
131 }
132 c.Mode = f
133 default:
134 err = errorf.E("codec spec %q: %q is not a parameter", s, f)
135 return
136 }
137 }
138 if c.Bitrate == 0 && c.BitDepth == 0 && c.Quality == 0 {
139 err = errorf.E("codec spec %q has no bitrate, depth or quality", s)
140 return
141 }
142 // The grammar is one form, not a mixture: a bitrate with a bit depth names
143 // no encoding any encoder produces.
144 if c.BitDepth > 0 && (c.Bitrate > 0 || c.Quality > 0 || c.Mode != "") {
145 err = errorf.E("codec spec %q mixes lossy and lossless forms", s)
146 return
147 }
148 if c.Quality > 0 && c.Mode != "" {
149 err = errorf.E("codec spec %q mixes a quality target with a rate mode", s)
150 return
151 }
152 return
153 }
154 155 // MediaType is the MIME type for a codec family, following the table in the
156 // music spec. An unknown codec has no answer here: the audio tag carries the
157 // media type itself, this only fills it in when constructing.
158 func MediaType(codec string) (m string) {
159 switch codec {
160 case "mp3":
161 return "audio/mpeg"
162 case "aac", "alac":
163 return "audio/mp4"
164 case "ogg-vorbis", "opus", "ogg":
165 return "audio/ogg"
166 case "flac":
167 return "audio/flac"
168 case "wav":
169 return "audio/wav"
170 }
171 return ""
172 }
173 174 // IsLossless reports whether the spec names a lossless encoding. The master tag
175 // points at one of these.
176 func (c *CodecSpec) IsLossless() (ok bool) {
177 if c == nil {
178 return false
179 }
180 switch c.Codec {
181 case "flac", "wav", "alac", "aiff":
182 return true
183 }
184 return false
185 }
186 187 // IsCodecSpec reports whether s parses as a codec spec.
188 func IsCodecSpec(s string) (ok bool) {
189 _, err := ParseCodecSpec(s)
190 return err == nil
191 }
192 193 func isCodecModifier(f string) (ok bool) {
194 if f == "VBR" || f == "CBR" || f == "ABR" {
195 return true
196 }
197 if hasSuffix(f, "kbps") || hasSuffix(f, "bit") || hasSuffix(f, "kHz") {
198 return true
199 }
200 if len(f) > 1 && f[0] == 'q' {
201 return isDecimal(f[1:])
202 }
203 return false
204 }
205 206 func hasSuffix(s, suffix string) (ok bool) {
207 return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
208 }
209 210 // splitDash splits on '-' without allocating an empty leading field for a
211 // leading dash: a spec never starts with one, and "" fields would be reported
212 // as bad parameters.
213 func splitDash(s string) (out []string) {
214 if s == "" {
215 return nil
216 }
217 n := int32(1)
218 for i := int32(0); i < int32(len(s)); i++ {
219 if s[i] == '-' {
220 n++
221 }
222 }
223 out = []string{:0:n}
224 start := int32(0)
225 for i := int32(0); i < int32(len(s)); i++ {
226 if s[i] == '-' {
227 out = push(out, s[start:i])
228 start = i + 1
229 }
230 }
231 out = push(out, s[start:])
232 return
233 }
234 235 // isDecimal reports whether s is digits, optionally with one '.' and at least
236 // one digit on each side.
237 func isDecimal(s string) (ok bool) {
238 if s == "" {
239 return false
240 }
241 dots := int32(0)
242 digits := int32(0)
243 for i := int32(0); i < int32(len(s)); i++ {
244 c := s[i]
245 if c == '.' {
246 dots++
247 if dots > 1 || i == 0 || i == int32(len(s))-1 {
248 return false
249 }
250 continue
251 }
252 if c < '0' || c > '9' {
253 return false
254 }
255 digits++
256 }
257 return digits > 0
258 }
259 260 // parseDigits parses a decimal integer into an int32. Codec parameters are
261 // small; a value that does not fit is an error, not a wrap.
262 func parseDigits(s string) (n int32, err error) {
263 if s == "" {
264 err = errorf.E("empty number")
265 return
266 }
267 v := int64(0)
268 for i := int32(0); i < int32(len(s)); i++ {
269 c := s[i]
270 if c < '0' || c > '9' {
271 err = errorf.E("%q is not a number", s)
272 return
273 }
274 v = v*10 + int64(c-'0')
275 if v > 2147483647 {
276 err = errorf.E("%q is out of range", s)
277 return
278 }
279 }
280 n = int32(v)
281 return
282 }
283