diff --git a/README.md b/README.md index 8b186ec..eaa375e 100644 --- a/README.md +++ b/README.md @@ -72,13 +72,11 @@ To crosscompile for different architectures the `GOOS` and `GOARCH` environment You need to have the following installed: * Go 1.22 or higher -* libogg -* libvorbis * libasound2 You can install the 3 libraries in Debian (and Ubuntu/Raspbian) using the following command: - sudo apt-get install libogg-dev libvorbis-dev libasound2-dev + sudo apt-get install libasound2-dev You can install a newer Go version from the [Go website](https://go.dev/dl/). diff --git a/audio/metadata.go b/audio/metadata.go index 4d5cddf..d047399 100644 --- a/audio/metadata.go +++ b/audio/metadata.go @@ -10,7 +10,6 @@ import ( librespot "github.com/devgianlu/go-librespot" log "github.com/sirupsen/logrus" - "github.com/xlab/vorbis-go/vorbis" ) const ( @@ -49,51 +48,59 @@ type MetadataPage struct { } func ExtractMetadataPage(r io.ReaderAt, limit int64) (librespot.SizedReadAtSeeker, *MetadataPage, error) { - var syncState vorbis.OggSyncState - vorbis.OggSyncInit(&syncState) - - defer func() { - vorbis.OggSyncClear(&syncState) - syncState.Free() - }() - rr := io.NewSectionReader(r, 0, limit) // read enough bytes for the first ogg packet to fit - buf := vorbis.OggSyncBuffer(&syncState, 512) - n, err := io.ReadFull(rr, buf[:512]) - vorbis.OggSyncWrote(&syncState, n) + buf := make([]byte, 512) + _, err := rr.ReadAt(buf, 0) if err != nil { - return nil, nil, fmt.Errorf("failed reading vorbis stream head") + return nil, nil, fmt.Errorf("failed reading vorbis stream head: %w", err) } - var page vorbis.OggPage - if ret := vorbis.OggSyncPageout(&syncState, &page); ret != 1 { - return nil, nil, errors.New("vorbis: not a valid Ogg bitstream") + // Read the Ogg page header (excluding the segment table). + var page struct { + CapturePattern uint32 + Version uint8 + Flags uint8 + GranulePosition uint64 + BitstreamSerialNumber uint32 + PageSequenceNumber uint32 + Checksum uint32 + PageSegments uint8 + } + bufReader := bytes.NewReader(buf) + err = binary.Read(bufReader, binary.LittleEndian, &page) + if err != nil { + return nil, nil, fmt.Errorf("failed reading ogg page header: %w", err) } - var streamState vorbis.OggStreamState - vorbis.OggStreamInit(&streamState, vorbis.OggPageSerialno(&page)) - - defer func() { - vorbis.OggStreamClear(&streamState) - streamState.Free() - }() - - if ret := vorbis.OggStreamPagein(&streamState, &page); ret < 0 { - return nil, nil, errors.New("vorbis: the supplied page does not belong this Vorbis stream") + // Check that the page looks like a metadata page. + if page.CapturePattern != 0x5367674f || // "OggS" + page.Version != 0 || // always 0 + page.Flags != 6 || // entire "stream" is a single page (BOS and EOS set) + page.PageSegments != 1 { // there's only a single metadata segment + return nil, nil, fmt.Errorf("not a valid Ogg bitstream metadata packet") } - var packet vorbis.OggPacket - if ret := vorbis.OggStreamPacketout(&streamState, &packet); ret != 1 { - return nil, nil, errors.New("vorbis: unable to fetch initial Vorbis packet from the first page") + // Read the segment table field, which has a somewhat odd encoding. + bodySize := int(0) + for { + b, err := bufReader.ReadByte() + if err != nil { + return nil, nil, fmt.Errorf("not a valid Ogg bitstream: %w", err) + } + if b != 255 { + bodySize += int(b) + break + } } - defer packet.Free() + // Get a reader for the page buffer (only). + pageHeaderSize := int(bufReader.Size()) - bufReader.Len() + body := bytes.NewReader(buf[pageHeaderSize : pageHeaderSize+bodySize]) + pageSize := pageHeaderSize + bodySize // we have the ogg packet, check it is the metadata page - packet.Deref() - body := bytes.NewReader(packet.Packet[:packet.Bytes]) if b, _ := body.ReadByte(); b != 0x81 { return nil, nil, fmt.Errorf("invalid metadata page") } @@ -178,8 +185,7 @@ func ExtractMetadataPage(r io.ReaderAt, limit int64) (librespot.SizedReadAtSeeke } // return a new stream without the metadata page - syncState.Deref() - return io.NewSectionReader(r, int64(syncState.Returned), limit-int64(syncState.Returned)), &metadata, nil + return io.NewSectionReader(r, int64(pageSize), limit-int64(pageSize)), &metadata, nil } func (m MetadataPage) GetTrackFactor(normalisationPregain float32) float32 { diff --git a/go.mod b/go.mod index e9372ff..1b078e7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/devgianlu/shannon v0.0.0-20230613115856-82ec90b7fa7e github.com/gofrs/flock v0.12.1 github.com/grandcat/zeroconf v1.0.0 + github.com/jfreymuth/oggvorbis v1.0.5 github.com/knadh/koanf/parsers/yaml v0.1.0 github.com/knadh/koanf/providers/confmap v0.1.0 github.com/knadh/koanf/providers/file v1.1.0 @@ -15,7 +16,6 @@ require ( github.com/rs/cors v1.11.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.5 - github.com/xlab/vorbis-go v0.0.0-20210911202351-b5b85f1ec645 golang.org/x/crypto v0.24.0 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/net v0.26.0 @@ -29,6 +29,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/jfreymuth/vorbis v1.0.2 // indirect github.com/klauspost/compress v1.10.3 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/go.sum b/go.sum index 8b7ec26..7ce2c72 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,10 @@ github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvK github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= +github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ= +github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII= +github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE= +github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= @@ -101,8 +105,6 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/xlab/vorbis-go v0.0.0-20210911202351-b5b85f1ec645 h1:lYg/+vV/Fd5WM1+Ptg54Am3y4mDXaMSrT+mKUHV5uVc= -github.com/xlab/vorbis-go v0.0.0-20210911202351-b5b85f1ec645/go.mod h1:AMqfx3jFwPqem3u8mF2lsRodZs30jG/Mag5HZ3mB3sA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= diff --git a/vorbis/decoder.go b/vorbis/decoder.go index a1b4396..9b325ef 100644 --- a/vorbis/decoder.go +++ b/vorbis/decoder.go @@ -1,30 +1,18 @@ package vorbis import ( - "errors" - "fmt" - "io" - "strings" "sync" librespot "github.com/devgianlu/go-librespot" "github.com/devgianlu/go-librespot/audio" + "github.com/jfreymuth/oggvorbis" log "github.com/sirupsen/logrus" - - "github.com/xlab/vorbis-go/vorbis" -) - -const ( - // DataChunkSize represents the amount of data read from physical bitstream on each iteration. - DataChunkSize = 4096 // could be also 8192 ) // Decoder implements an OggVorbis decoder. type Decoder struct { sync.Mutex - log *log.Entry - SampleRate int32 Channels int32 @@ -34,364 +22,52 @@ type Decoder struct { // gain is the default track gain. gain float32 - // syncState tracks the synchronization of the current page. It is used during - // decoding to track the status of data as it is read in, synchronized, verified, - // and parsed into pages belonging to the various logical bistreams - // in the current physical bitstream link. - syncState vorbis.OggSyncState - - // streamState tracks the current decode state of the current logical bitstream. - streamState vorbis.OggStreamState - - // page encapsulates the data for an Ogg page. Ogg pages are the fundamental unit - // of framing and interleave in an Ogg bitstream. - page vorbis.OggPage - - // packet encapsulates the data for a single raw packet of data and is used to transfer - // data between the Ogg framing layer and the handling codec. - packet vorbis.OggPacket - - // info contains basic information about the audio in a vorbis bitstream. - info vorbis.Info - - // comment stores all the bitstream user comments as Ogg Vorbis comment. - comment vorbis.Comment - - // dspState is the state for one instance of the Vorbis decoder. - // This structure is intended to be private. - dspState vorbis.DspState - - // block holds the data for a single block of audio. One Vorbis block translates to one codec packet. - // The decoding process consists of decoding the packets into blocks and reassembling the audio from the blocks. - // This structure is intended to be private. - block vorbis.Block - - input librespot.SizedReadAtSeeker - pcm [][][]float32 - buf []float32 - stopChan chan struct{} - closed bool - started bool - - lastGranulepos vorbis.OggInt64 + reader *oggvorbis.Reader } -// Info represents basic information about the audio in a Vorbis bitstream. -type Info struct { - Channels int32 - SampleRate int32 - Comments []string - Vendor string -} - -// New creates and initialises a new OggVorbis decoder for the provided bytestream. func New(log *log.Entry, r librespot.SizedReadAtSeeker, meta *audio.MetadataPage, gain float32) (*Decoder, error) { - d := &Decoder{ - log: log, - input: r, - meta: meta, - gain: gain, - stopChan: make(chan struct{}), - } - - vorbis.OggSyncInit(&d.syncState) - - if err := d.readStreamHeaders(); err != nil { - d.decoderStateCleanup() + reader, err := oggvorbis.NewReader(r) + if err != nil { return nil, err } - d.pcm = [][][]float32{ - make([][]float32, d.info.Channels), - } - - if ret := vorbis.SynthesisInit(&d.dspState, &d.info); ret < 0 { - d.decoderStateCleanup() - return nil, errors.New("vorbis: error during playback initialization") + d := &Decoder{ + SampleRate: int32(reader.SampleRate()), + Channels: int32(reader.Channels()), + meta: meta, + gain: gain, + reader: reader, } return d, nil } -// Close stops and finalizes the decoding process, releases the allocated resources. -// Puts the decoder into an unrecoverable state. -func (d *Decoder) Close() { - if !d.stopRequested() { - close(d.stopChan) - } - d.Lock() - defer d.Unlock() - if d.closed { - return - } - d.closed = true - d.decoderStateCleanup() -} - -func (d *Decoder) decoderStateCleanup() { - vorbis.OggSyncClear(&d.syncState) - d.syncState.Free() - - if d.streamState.Ref() != nil { - vorbis.OggStreamClear(&d.streamState) - d.streamState.Free() - } - - if d.comment.Ref() != nil { - vorbis.CommentClear(&d.comment) - d.comment.Free() - } - - if d.info.Ref() != nil { - vorbis.InfoClear(&d.info) - d.info.Free() - } - - if d.dspState.Ref() != nil { - vorbis.DspClear(&d.dspState) - d.dspState.Free() - } - - if d.block.Ref() != nil { - vorbis.BlockClear(&d.block) - d.block.Free() - } - - d.packet.Free() - d.page.Free() -} - -func (d *Decoder) stopRequested() bool { - select { - case <-d.stopChan: - return true - default: - return false - } -} - -func (d *Decoder) readChunk() (n int, err error) { - buf := vorbis.OggSyncBuffer(&d.syncState, DataChunkSize) - n, err = io.ReadFull(d.input, buf[:DataChunkSize]) - vorbis.OggSyncWrote(&d.syncState, n) - if errors.Is(err, io.ErrUnexpectedEOF) { - return n, io.EOF - } - return n, err -} - -func (d *Decoder) readStreamHeaders() error { - if _, err := d.readChunk(); err != nil { - return fmt.Errorf("vorbis: failed reading headers chunk: %w", err) - } - - // Read the first page - if ret := vorbis.OggSyncPageout(&d.syncState, &d.page); ret != 1 { - return errors.New("vorbis: not a valid Ogg bitstream") - } - - // Init the logical bitstream with serial number stored in the page - vorbis.OggStreamInit(&d.streamState, vorbis.OggPageSerialno(&d.page)) - - vorbis.InfoInit(&d.info) - vorbis.CommentInit(&d.comment) - - // Add a complete page to the bitstream - if ret := vorbis.OggStreamPagein(&d.streamState, &d.page); ret < 0 { - return errors.New("vorbis: the supplied page does not belong this Vorbis stream") - } - // Get the first packet - if ret := vorbis.OggStreamPacketout(&d.streamState, &d.packet); ret != 1 { - return errors.New("vorbis: unable to fetch initial Vorbis packet from the first page") - } - // Finally decode the header packet - if ret := vorbis.SynthesisHeaderin(&d.info, &d.comment, &d.packet); ret < 0 { - return fmt.Errorf("vorbis: unable to decode the initial Vorbis header: %d", ret) - } - - var headersRead int -forPage: - for headersRead < 2 { - if res := vorbis.OggSyncPageout(&d.syncState, &d.page); res < 0 { - // bytes have been skipped, try to sync again - continue forPage - } else if res == 0 { - // go get more data - if _, err := d.readChunk(); err != nil { - return errors.New("vorbis: got EOF while reading Vorbis headers") - } - continue forPage - } - // page is synced at this point - vorbis.OggStreamPagein(&d.streamState, &d.page) - for headersRead < 2 { - if ret := vorbis.OggStreamPacketout(&d.streamState, &d.packet); ret < 0 { - return errors.New("vorbis: data is missing near the secondary Vorbis header") - } else if ret == 0 { - // no packets left on the page, go get a new one - continue forPage - } - if ret := vorbis.SynthesisHeaderin(&d.info, &d.comment, &d.packet); ret < 0 { - return errors.New("vorbis: unable to read the secondary Vorbis header") - } - headersRead++ - } - } - - d.info.Deref() - d.comment.Deref() - - d.Channels = d.info.Channels - d.SampleRate = int32(d.info.Rate) - - return nil -} - func (d *Decoder) Read(p []float32) (n int, err error) { d.Lock() defer d.Unlock() - if d.closed { - return 0, errors.New("decoder: decoder has already been closed") - } - - if !d.started { - vorbis.BlockInit(&d.dspState, &d.block) - d.started = true - } - - n = 0 - for n < len(p) { - // read from page buffer - if len(d.buf) > 0 { - copied := copy(p[n:], d.buf) - d.buf = d.buf[copied:] - n += copied - continue - } - - // decode another page - err = d.readNextPage() - if err != nil { - return n, err - } - } - return n, nil -} - -func (d *Decoder) safeSynthesisPcmout() (ret int32) { - defer func() { - err := recover() - if err == nil { - return - } - switch err := err.(type) { - case string: - // the calloc inside allocPPFloatMemory will sometimes fail for no apparent reason, - // avoid panicking the entire program and fail locally instead. - if strings.HasPrefix(err, "memory alloc error") { - ret = -1 - return - } - } + n, err = d.reader.Read(p) - panic(err) - }() - - return vorbis.SynthesisPcmout(&d.dspState, d.pcm) -} - -func (d *Decoder) readNextPage() (err error) { - for { - if ret := vorbis.OggSyncPageout(&d.syncState, &d.page); ret < 0 { - d.log.Debugf("vorbis: corrupt or missing data in bitstream") - continue - } else if ret == 0 { - // need more data - _, err = d.readChunk() - if err != nil { - return err - } - } else { - // we have read the page - break - } - } - - // page is synced at this point - vorbis.OggStreamPagein(&d.streamState, &d.page) - - for { - if ret := vorbis.OggStreamPacketout(&d.streamState, &d.packet); ret < 0 { - // skip this packet - continue - } else if ret == 0 { - // no packets left on the page - break - } - - if vorbis.Synthesis(&d.block, &d.packet) == 0 { - vorbis.SynthesisBlockin(&d.dspState, &d.block) - } - - samples := d.safeSynthesisPcmout() - for ; samples > 0; samples = d.safeSynthesisPcmout() { - for i := 0; i < int(samples); i++ { - for j := 0; j < int(d.info.Channels); j++ { - d.buf = append(d.buf, d.pcm[0][j][:samples][i]*d.gain) - } - } - vorbis.SynthesisRead(&d.dspState, samples) - } - - // save last observed position - d.lastGranulepos = vorbis.OggPageGranulepos(&d.page) + // Apply gain. + for i := 0; i < n; i++ { + p[i] *= d.gain } - if vorbis.OggPageEos(&d.page) == 1 { - return io.EOF - } - - return nil + return } func (d *Decoder) SetPositionMs(pos int64) (err error) { d.Lock() defer d.Unlock() - // get the seek position in bytes from the milliseconds - posSamples := pos * int64(d.SampleRate) / 1000 - posBytes := d.meta.GetSeekPosition(posSamples) - if posBytes > d.input.Size() { - posBytes = d.input.Size() - } - - // seek there - if _, err = d.input.Seek(posBytes, io.SeekStart); err != nil { - return fmt.Errorf("failed seeking input: %w", err) - } - - // empty the read buffer here, if we found the correct offset - // the buffer will contain the correct page data - d.buf = nil - - // we trust that the bytes offset we were given is accurate and process the data at this point - if _, err = d.readChunk(); err != nil { - return fmt.Errorf("failed reading chunk: %w", err) - } - - // get in sync with the next page, this avoids obvious sync errors - vorbis.OggSyncPageout(&d.syncState, &d.page) - - // read the page now that we are aligned - if err = d.readNextPage(); err != nil { - return fmt.Errorf("failed reading page: %w", err) - } - - d.log.Tracef("seek to %dms (diff: %dms, samples: %d, bytes: %d)", pos, pos-d.PositionMs(), posSamples, posBytes) - return nil + // TODO: use seek table + samplePos := pos * int64(d.reader.SampleRate()) / 1000 + return d.reader.SetPosition(samplePos) } func (d *Decoder) PositionMs() int64 { - return int64(vorbis.GranuleTime(&d.dspState, d.lastGranulepos) * 1000) + d.Lock() + defer d.Unlock() + + return d.reader.Position() * 1000 / int64(d.reader.SampleRate()) }