package utils
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
"time"
)
type WAVMetadata struct {
Duration float64 SampleRate int Comment string Artist string Channels int BitsPerSample int FileModTime time.Time }
func ParseWAVHeader(filepath string) (*WAVMetadata, error) {
file, err := os.Open(filepath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}
modTime := fileInfo.ModTime()
headerBuf := make([]byte, 200*1024)
n, err := file.Read(headerBuf)
if err != nil && err != io.EOF {
return nil, fmt.Errorf("failed to read header: %w", err)
}
headerBuf = headerBuf[:n]
metadata, err := parseWAVFromBytes(headerBuf, filepath)
if err != nil {
return nil, err
}
metadata.FileModTime = modTime
return metadata, nil
}
func parseWAVFromBytes(data []byte, filepath string) (*WAVMetadata, error) {
if len(data) < 44 {
return nil, fmt.Errorf("file too small to be valid WAV")
}
if string(data[0:4]) != "RIFF" {
return nil, fmt.Errorf("not a valid WAV file (missing RIFF header)")
}
if string(data[8:12]) != "WAVE" {
return nil, fmt.Errorf("not a valid WAV file (missing WAVE format)")
}
metadata := &WAVMetadata{}
offset := 12
for offset < len(data)-8 {
chunkID := string(data[offset : offset+4])
chunkSize := int(binary.LittleEndian.Uint32(data[offset+4 : offset+8]))
offset += 8
if offset+chunkSize > len(data) {
break
}
switch chunkID {
case "fmt ":
if chunkSize >= 16 {
metadata.Channels = int(binary.LittleEndian.Uint16(data[offset+2 : offset+4]))
metadata.SampleRate = int(binary.LittleEndian.Uint32(data[offset+4 : offset+8]))
metadata.BitsPerSample = int(binary.LittleEndian.Uint16(data[offset+14 : offset+16]))
}
case "data":
if metadata.SampleRate > 0 && metadata.Channels > 0 && metadata.BitsPerSample > 0 {
bytesPerSample := metadata.BitsPerSample / 8
bytesPerSecond := metadata.SampleRate * metadata.Channels * bytesPerSample
if bytesPerSecond > 0 {
metadata.Duration = float64(chunkSize) / float64(bytesPerSecond)
}
}
case "LIST":
if chunkSize >= 4 {
listType := string(data[offset : offset+4])
if listType == "INFO" {
parseINFOChunk(data[offset+4:offset+chunkSize], metadata)
}
}
}
offset += chunkSize
if chunkSize%2 != 0 {
offset++ }
}
if metadata.Duration == 0 {
if fileInfo, err := os.Stat(filepath); err == nil {
dataSize := fileInfo.Size() - 100
if metadata.SampleRate > 0 && metadata.Channels > 0 && metadata.BitsPerSample > 0 {
bytesPerSample := metadata.BitsPerSample / 8
bytesPerSecond := metadata.SampleRate * metadata.Channels * bytesPerSample
if bytesPerSecond > 0 {
metadata.Duration = float64(dataSize) / float64(bytesPerSecond)
}
}
}
}
return metadata, nil
}
func parseINFOChunk(data []byte, metadata *WAVMetadata) {
offset := 0
for offset < len(data)-8 {
if offset+8 > len(data) {
break
}
subchunkID := string(data[offset : offset+4])
subchunkSize := int(binary.LittleEndian.Uint32(data[offset+4 : offset+8]))
offset += 8
if offset+subchunkSize > len(data) {
break
}
value := extractNullTerminatedString(data[offset : offset+subchunkSize])
switch subchunkID {
case "ICMT": metadata.Comment = value
case "IART": metadata.Artist = value
}
offset += subchunkSize
if subchunkSize%2 != 0 {
offset++ }
}
}
func extractNullTerminatedString(data []byte) string {
nullIdx := bytes.IndexByte(data, 0)
if nullIdx >= 0 {
return string(data[:nullIdx])
}
return string(data)
}