codec.mx raw

   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