Files
muse/lib/audio/wav_writer.go

117 lines
2.6 KiB
Go

package audio
import (
"encoding/binary"
"math"
"os"
)
// Represents the header of a WAV file
type WAVHeader struct {
RiffMark [4]byte
FileSize uint32
WaveMark [4]byte
FmtMark [4]byte
FmtSize uint32
FormatType uint16
NumChannels uint16
SampleRate uint32
ByteRate uint32
BlockAlign uint16
BitsPerSample uint16
DataMark [4]byte
DataSize uint32
}
// Writes the audio data to a WAV file
func (g *Generator) WriteWAV(filename string, samples []float64) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
// Ensure the samples are normalized first
Normalize(samples)
header := WAVHeader{
RiffMark: [4]byte{'R', 'I', 'F', 'F'},
WaveMark: [4]byte{'W', 'A', 'V', 'E'},
FmtMark: [4]byte{'f', 'm', 't', ' '},
FmtSize: 16,
FormatType: 1, // PCM
NumChannels: Channels,
SampleRate: uint32(g.sampleRate),
BitsPerSample: Bits,
ByteRate: uint32(g.sampleRate * float64(Channels) * float64(Bits) / 8),
BlockAlign: uint16(Channels * Bits / 8),
DataMark: [4]byte{'d', 'a', 't', 'a'},
}
// Calculate sizes - samples are interleaved LRLR...
header.DataSize = uint32(len(samples) * int(Bits) / 8)
header.FileSize = 36 + header.DataSize // 36 is the size of the header up to data
// Write header
if err := binary.Write(file, binary.LittleEndian, &header); err != nil {
return err
}
// Write audio data - batch convert and write for better performance
const batchSize = 8192 // Write in 8KB chunks
int16Buffer := make([]int16, batchSize)
for i := 0; i < len(samples); i += batchSize {
end := i + batchSize
if end > len(samples) {
end = len(samples)
}
// Convert batch of samples to int16
for j := i; j < end; j++ {
sample := samples[j]
// Clamp to prevent overflow
if sample > 1.0 {
sample = 1.0
} else if sample < -1.0 {
sample = -1.0
}
int16Buffer[j-i] = int16(sample * 32767.0)
}
// Write entire batch at once
if err := binary.Write(file, binary.LittleEndian, int16Buffer[:end-i]); err != nil {
return err
}
}
return nil
}
// Normalizes the audio buffer to prevent clipping
func Normalize(buffer []float64) {
if len(buffer) == 0 {
return
}
// Find the maximum absolute value in the buffer
maxVal := 0.0
for _, sample := range buffer {
absVal := math.Abs(sample)
if absVal > maxVal {
maxVal = absVal
}
}
// If the maximum is very small, don't normalize to avoid amplifying noise
if maxVal < 0.0001 {
return
}
// Normalize to 90% of maximum to avoid clipping
scale := 0.9 / maxVal
for i := range buffer {
buffer[i] *= scale
}
}