| package common |
|
|
| import ( |
| "context" |
| "encoding/binary" |
| "fmt" |
| "io" |
|
|
| "github.com/abema/go-mp4" |
| "github.com/go-audio/aiff" |
| "github.com/go-audio/wav" |
| "github.com/jfreymuth/oggvorbis" |
| "github.com/mewkiz/flac" |
| "github.com/pkg/errors" |
| "github.com/tcolgate/mp3" |
| "github.com/yapingcat/gomedia/go-codec" |
| ) |
|
|
| |
| |
| func GetAudioDuration(ctx context.Context, f io.ReadSeeker, ext string) (duration float64, err error) { |
| SysLog(fmt.Sprintf("GetAudioDuration: ext=%s", ext)) |
| |
| switch ext { |
| case ".mp3": |
| duration, err = getMP3Duration(f) |
| case ".wav": |
| duration, err = getWAVDuration(f) |
| case ".flac": |
| duration, err = getFLACDuration(f) |
| case ".m4a", ".mp4": |
| duration, err = getM4ADuration(f) |
| case ".ogg", ".oga", ".opus": |
| duration, err = getOGGDuration(f) |
| if err != nil { |
| duration, err = getOpusDuration(f) |
| } |
| case ".aiff", ".aif", ".aifc": |
| duration, err = getAIFFDuration(f) |
| case ".webm": |
| duration, err = getWebMDuration(f) |
| case ".aac": |
| duration, err = getAACDuration(f) |
| default: |
| return 0, fmt.Errorf("unsupported audio format: %s", ext) |
| } |
| SysLog(fmt.Sprintf("GetAudioDuration: duration=%f", duration)) |
| return duration, err |
| } |
|
|
| |
| |
| |
| func getMP3Duration(r io.Reader) (float64, error) { |
| d := mp3.NewDecoder(r) |
| var f mp3.Frame |
| skipped := 0 |
| duration := 0.0 |
|
|
| for { |
| if err := d.Decode(&f, &skipped); err != nil { |
| if err == io.EOF { |
| break |
| } |
| return 0, errors.Wrap(err, "failed to decode mp3 frame") |
| } |
| duration += f.Duration().Seconds() |
| } |
| return duration, nil |
| } |
|
|
| |
| func getWAVDuration(r io.ReadSeeker) (float64, error) { |
| dec := wav.NewDecoder(r) |
| if !dec.IsValidFile() { |
| return 0, errors.New("invalid wav file") |
| } |
| d, err := dec.Duration() |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to get wav duration") |
| } |
| return d.Seconds(), nil |
| } |
|
|
| |
| func getFLACDuration(r io.Reader) (float64, error) { |
| stream, err := flac.Parse(r) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to parse flac stream") |
| } |
| defer stream.Close() |
|
|
| |
| duration := float64(stream.Info.NSamples) / float64(stream.Info.SampleRate) |
| return duration, nil |
| } |
|
|
| |
| func getM4ADuration(r io.ReadSeeker) (float64, error) { |
| |
| info, err := mp4.Probe(r) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to probe m4a/mp4 file") |
| } |
| |
| return float64(info.Duration) / float64(info.Timescale), nil |
| } |
|
|
| |
| func getOGGDuration(r io.ReadSeeker) (float64, error) { |
| |
| if _, err := r.Seek(0, io.SeekStart); err != nil { |
| return 0, errors.Wrap(err, "failed to seek ogg file") |
| } |
|
|
| reader, err := oggvorbis.NewReader(r) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to create ogg vorbis reader") |
| } |
|
|
| |
| |
| channels := reader.Channels() |
| sampleRate := reader.SampleRate() |
|
|
| |
| var totalSamples int64 |
| buf := make([]float32, 4096*channels) |
| for { |
| n, err := reader.Read(buf) |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to read ogg samples") |
| } |
| totalSamples += int64(n / channels) |
| } |
|
|
| duration := float64(totalSamples) / float64(sampleRate) |
| return duration, nil |
| } |
|
|
| |
| func getOpusDuration(r io.ReadSeeker) (float64, error) { |
| |
| |
| if _, err := r.Seek(0, io.SeekStart); err != nil { |
| return 0, errors.Wrap(err, "failed to seek opus file") |
| } |
|
|
| |
| var totalGranulePos int64 |
| buf := make([]byte, 27) |
|
|
| for { |
| n, err := r.Read(buf) |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to read opus/ogg page") |
| } |
| if n < 27 { |
| break |
| } |
|
|
| |
| if string(buf[0:4]) != "OggS" { |
| |
| if _, err := r.Seek(-26, io.SeekCurrent); err != nil { |
| break |
| } |
| continue |
| } |
|
|
| |
| granulePos := int64(binary.LittleEndian.Uint64(buf[6:14])) |
| if granulePos > totalGranulePos { |
| totalGranulePos = granulePos |
| } |
|
|
| |
| numSegments := int(buf[26]) |
| segmentTable := make([]byte, numSegments) |
| if _, err := io.ReadFull(r, segmentTable); err != nil { |
| break |
| } |
|
|
| |
| var pageSize int |
| for _, segSize := range segmentTable { |
| pageSize += int(segSize) |
| } |
| if _, err := r.Seek(int64(pageSize), io.SeekCurrent); err != nil { |
| break |
| } |
| } |
|
|
| |
| duration := float64(totalGranulePos) / 48000.0 |
| return duration, nil |
| } |
|
|
| |
| func getAIFFDuration(r io.ReadSeeker) (float64, error) { |
| if _, err := r.Seek(0, io.SeekStart); err != nil { |
| return 0, errors.Wrap(err, "failed to seek aiff file") |
| } |
|
|
| dec := aiff.NewDecoder(r) |
| if !dec.IsValidFile() { |
| return 0, errors.New("invalid aiff file") |
| } |
|
|
| d, err := dec.Duration() |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to get aiff duration") |
| } |
|
|
| return d.Seconds(), nil |
| } |
|
|
| |
| |
| func getWebMDuration(r io.ReadSeeker) (float64, error) { |
| if _, err := r.Seek(0, io.SeekStart); err != nil { |
| return 0, errors.Wrap(err, "failed to seek webm file") |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| buf := make([]byte, 8192) |
| n, err := r.Read(buf) |
| if err != nil && err != io.EOF { |
| return 0, errors.Wrap(err, "failed to read webm file") |
| } |
|
|
| |
| |
| |
| if n > 0 { |
| |
| if len(buf) >= 4 && binary.BigEndian.Uint32(buf[0:4]) == 0x1A45DFA3 { |
| |
| |
| return 0, errors.New("webm duration parsing requires full EBML parser (consider using ffprobe for webm files)") |
| } |
| } |
|
|
| return 0, errors.New("failed to parse webm file") |
| } |
|
|
| |
| |
| func getAACDuration(r io.ReadSeeker) (float64, error) { |
| if _, err := r.Seek(0, io.SeekStart); err != nil { |
| return 0, errors.Wrap(err, "failed to seek aac file") |
| } |
|
|
| |
| data, err := io.ReadAll(r) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to read aac file") |
| } |
|
|
| var totalFrames int64 |
| var sampleRate int |
|
|
| |
| codec.SplitAACFrame(data, func(aac []byte) { |
| |
| if len(aac) >= 7 { |
| |
| asc, err := codec.ConvertADTSToASC(aac) |
| if err == nil && sampleRate == 0 { |
| sampleRate = codec.AACSampleIdxToSample(int(asc.Sample_freq_index)) |
| } |
| totalFrames++ |
| } |
| }) |
|
|
| if sampleRate == 0 || totalFrames == 0 { |
| return 0, errors.New("no valid aac frames found") |
| } |
|
|
| |
| totalSamples := totalFrames * 1024 |
| duration := float64(totalSamples) / float64(sampleRate) |
| return duration, nil |
| } |
|
|