Refactor GetDuration for improved MP3 duration estimation (#7)

* Refactor GetDuration to improve MP3 duration estimation using bitrate sampling and file size

* Improve error message for bitrate sampling failure in GetDuration
This commit is contained in:
2025-04-13 23:23:27 +03:00
committed by GitHub
parent b5f23a50b7
commit f9d0347ed4

View File

@@ -11,8 +11,31 @@ import (
"github.com/tcolgate/mp3"
)
// GetDuration returns the actual duration of an MP3 file by analyzing its frames
// Maximum number of frames to sample for bitrate calculation
const maxFramesToSample = 100
// GetDuration returns the estimated duration of an MP3 file by sampling frames
// and using file size and bitrate for calculation to avoid processing very large files
func GetDuration(filePath string) (time.Duration, error) {
fileInfo, err := os.Stat(filePath)
if err != nil {
return 0, fmt.Errorf("failed to stat MP3 file: %w", err)
}
fileSize := fileInfo.Size()
// First try to calculate duration by sampling bitrate
duration, err := getDurationByBitrateSampling(filePath, fileSize)
if err == nil {
return duration, nil
}
// If bitrate sampling fails, fall back to size-based estimation
return getDurationByFileSize(fileSize), nil
}
// getDurationByBitrateSampling calculates MP3 duration by sampling a limited number of frames
// to determine the average bitrate, then uses that with the file size to estimate duration
func getDurationByBitrateSampling(filePath string, fileSize int64) (time.Duration, error) {
file, err := os.Open(filePath)
if err != nil {
return 0, fmt.Errorf("failed to open MP3 file: %w", err)
@@ -27,11 +50,11 @@ func GetDuration(filePath string) (time.Duration, error) {
decoder := mp3.NewDecoder(file)
var frame mp3.Frame
var skipped int
var totalSamples int
sampleRate := 0
var totalBitrate int
var frameCount int
// Process the frames to calculate the total duration
for {
// Sample a limited number of frames to calculate average bitrate
for frameCount < maxFramesToSample {
if err := decoder.Decode(&frame, &skipped); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
break
@@ -39,17 +62,46 @@ func GetDuration(filePath string) (time.Duration, error) {
return 0, fmt.Errorf("failed to decode MP3 frame: %w", err)
}
if sampleRate == 0 {
sampleRate = frame.Samples()
// Get bitrate from the frame header in bits per second
bitrate := int(frame.Header().BitRate())
if bitrate <= 0 {
continue
}
totalSamples += frame.Samples()
totalBitrate += bitrate
frameCount++
}
if totalSamples == 0 || sampleRate == 0 {
return 0, errors.New("could not determine MP3 duration")
if frameCount == 0 {
return 0, errors.New("could not read any MP3 frames with a valid bitrate")
}
durationSeconds := float64(totalSamples) / float64(sampleRate)
// Calculate average bitrate in bits per second
averageBitrate := totalBitrate / frameCount
if averageBitrate <= 0 {
return 0, errors.New("invalid bitrate detected")
}
// Calculate duration: file size (bits) / bitrate (bits per second)
// Subtract ~10KB for headers and metadata (conservative estimate)
metadataEstimate := int64(10 * 1024)
adjustedSize := fileSize
if fileSize > metadataEstimate {
adjustedSize -= metadataEstimate
}
durationSeconds := float64(adjustedSize*8) / float64(averageBitrate)
return time.Duration(durationSeconds * float64(time.Second)), nil
}
// getDurationByFileSize provides a rough estimate of MP3 duration based only on file size
// using the assumption of ~128kbps average bitrate for most MP3 files
func getDurationByFileSize(fileSize int64) time.Duration {
// Assume average bitrate of 128kbps (common for MP3)
assumedBitrate := 128 * 1024 // bits per second
// Calculate duration: file size (bits) / bitrate (bits per second)
durationSeconds := float64(fileSize*8) / float64(assumedBitrate)
return time.Duration(durationSeconds * float64(time.Second))
}