package metadata import ( "encoding/binary" "errors" "fmt" "os" ) // errInvalidFLACSignature is returned when the file does not contain // a valid FLAC stream signature ("fLaC") at the expected position. var errInvalidFLACSignature = errors.New( "invalid FLAC signature", ) // errInvalidStreamInfo is returned when the first metadata block is // not a StreamInfo block or has an unexpected length. var errInvalidStreamInfo = errors.New( "invalid StreamInfo metadata block", ) // errZeroSampleRate is returned when the StreamInfo block reports a // sample rate of zero, which would cause a division by zero. var errZeroSampleRate = errors.New( "FLAC StreamInfo sample rate is zero", ) // flacSignatureBytes is the four-byte marker that begins every FLAC // stream. var flacSignatureBytes = [4]byte{'f', 'L', 'a', 'C'} // streamInfoLength is the fixed size of a FLAC StreamInfo body in // bytes. const streamInfoLength = 34 // streamInfoBlockType is the metadata block type for StreamInfo. const streamInfoBlockType = 0 // getFlacDuration computes the duration of a FLAC file in // milliseconds by reading only the StreamInfo metadata block header. // It also extracts sample rate, bit depth, and channel count from // the same header. It handles an optional prepended ID3v2 tag by // seeking past it. // // This replaces the previous beep/mewkiz-flac decode path which has // a bug in its ID3v2 skip logic (bufio over bufseekio causes a // position overshoot). // // The file position is undefined after this call. // //nolint:mnd // byte offsets and bit shifts from the FLAC spec. func getFlacDuration( f *os.File, ) (int64, *AudioProperties, error) { audioStart, err := skipID3v2(f) if err != nil { return 0, nil, fmt.Errorf("skipping ID3v2: %w", err) } // Read the 4-byte FLAC signature. var sig [4]byte if _, err := f.ReadAt(sig[:], audioStart); err != nil { return 0, nil, fmt.Errorf( "reading FLAC signature: %w", err, ) } if sig != flacSignatureBytes { return 0, nil, fmt.Errorf( "%w: expected %q, got %q", errInvalidFLACSignature, flacSignatureBytes, sig, ) } // Read the metadata block header (4 bytes) immediately after // the signature. var mbh [4]byte if _, err := f.ReadAt( mbh[:], audioStart+4, ); err != nil { return 0, nil, fmt.Errorf( "reading metadata block header: %w", err, ) } blockType := mbh[0] & 0x7F blockLen := int64(mbh[1])<<16 | int64(mbh[2])<<8 | int64(mbh[3]) if blockType != streamInfoBlockType || blockLen != streamInfoLength { return 0, nil, fmt.Errorf( "%w: type=%d, length=%d", errInvalidStreamInfo, blockType, blockLen, ) } // Read the 34-byte StreamInfo body. var si [streamInfoLength]byte if _, err := f.ReadAt( si[:], audioStart+8, ); err != nil { return 0, nil, fmt.Errorf( "reading StreamInfo block: %w", err, ) } sampleRate, totalSamples, channels, bitDepth := parseFlacStreamInfo(si) if sampleRate == 0 { return 0, nil, errZeroSampleRate } durationMS := int64(totalSamples) * 1000 / int64(sampleRate) props := &AudioProperties{ SampleRate: int(sampleRate), BitDepth: int(bitDepth), Channels: int(channels), } return durationMS, props, nil } // parseFlacStreamInfo extracts key fields from a 34-byte FLAC // StreamInfo body. // // StreamInfo layout (bytes 10-17 contain the fields we need): // // bits 0-19: sample rate in Hz (20 bits) // bits 20-22: number of channels -1 (3 bits) // bits 23-27: bits per sample -1 (5 bits) // bits 28-63: total samples (36 bits) // //nolint:mnd // bit offsets from the FLAC spec. func parseFlacStreamInfo( si [streamInfoLength]byte, ) (sampleRate uint32, totalSamples uint64, channels uint32, bitDepth uint32) { // Bytes 10-13 packed as big-endian uint32 contain sample rate // in the upper 20 bits, channels in bits 9-11, and bits per // sample in bits 4-8. packed := binary.BigEndian.Uint32(si[10:14]) sampleRate = packed >> 12 channels = (packed>>9)&0x07 + 1 bitDepth = (packed>>4)&0x1F + 1 // Total samples: 4 low bits of byte 13, then bytes 14-17. totalSamples = uint64(si[13]&0x0F)<<32 | uint64(si[14])<<24 | uint64(si[15])<<16 | uint64(si[16])<<8 | uint64(si[17]) return sampleRate, totalSamples, channels, bitDepth }