feat: add lossless flac to wav converter
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mathismqn/godeez/internal/fsutil"
|
||||
"github.com/mewkiz/flac"
|
||||
)
|
||||
|
||||
const (
|
||||
headerSize = 44
|
||||
formatPCM = 1
|
||||
ctxCheckInterval = 64
|
||||
maxDataSize = math.MaxUint32 - (headerSize - 8)
|
||||
)
|
||||
|
||||
func FLACToWAV(ctx context.Context, srcPath, dstPath string) error {
|
||||
stream, err := flac.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
info := stream.Info
|
||||
bytesPerSample, err := bytesPerSample(info.BitsPerSample)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.NChannels < 1 || info.NChannels > 2 {
|
||||
return fmt.Errorf("unsupported channel count: %d", info.NChannels)
|
||||
}
|
||||
if size := int64(info.NSamples) * int64(info.NChannels) * int64(bytesPerSample); size > maxDataSize {
|
||||
return fmt.Errorf("audio data of %d bytes exceeds the wav format limit", size)
|
||||
}
|
||||
|
||||
file, err := os.CreateTemp(filepath.Dir(dstPath), fsutil.PartPattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpPath := file.Name()
|
||||
done := false
|
||||
defer func() {
|
||||
if !done {
|
||||
file.Close()
|
||||
os.Remove(tmpPath)
|
||||
}
|
||||
}()
|
||||
|
||||
w := bufio.NewWriter(file)
|
||||
if err := writeHeader(w, info.SampleRate, info.NChannels, info.BitsPerSample, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dataSize, err := writeSamples(ctx, w, stream, int(info.NChannels), bytesPerSample)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dataSize > maxDataSize {
|
||||
return fmt.Errorf("audio data of %d bytes exceeds the wav format limit", dataSize)
|
||||
}
|
||||
if dataSize%2 != 0 {
|
||||
if err := w.WriteByte(0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := patchSizes(file, dataSize); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := file.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmpPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
done = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bytesPerSample(bitsPerSample uint8) (int, error) {
|
||||
switch bitsPerSample {
|
||||
case 8, 16, 24:
|
||||
return int(bitsPerSample) / 8, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported bit depth: %d", bitsPerSample)
|
||||
}
|
||||
}
|
||||
|
||||
func writeHeader(w io.Writer, sampleRate uint32, nChannels, bitsPerSample uint8, dataSize uint32) error {
|
||||
blockAlign := uint32(nChannels) * uint32(bitsPerSample) / 8
|
||||
|
||||
header := make([]byte, 0, headerSize)
|
||||
header = append(header, "RIFF"...)
|
||||
header = binary.LittleEndian.AppendUint32(header, uint32(headerSize-8)+dataSize)
|
||||
header = append(header, "WAVE"...)
|
||||
header = append(header, "fmt "...)
|
||||
header = binary.LittleEndian.AppendUint32(header, 16)
|
||||
header = binary.LittleEndian.AppendUint16(header, formatPCM)
|
||||
header = binary.LittleEndian.AppendUint16(header, uint16(nChannels))
|
||||
header = binary.LittleEndian.AppendUint32(header, sampleRate)
|
||||
header = binary.LittleEndian.AppendUint32(header, sampleRate*blockAlign)
|
||||
header = binary.LittleEndian.AppendUint16(header, uint16(blockAlign))
|
||||
header = binary.LittleEndian.AppendUint16(header, uint16(bitsPerSample))
|
||||
header = append(header, "data"...)
|
||||
header = binary.LittleEndian.AppendUint32(header, dataSize)
|
||||
|
||||
_, err := w.Write(header)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func writeSamples(ctx context.Context, w io.Writer, stream *flac.Stream, nChannels, bytesPerSample int) (int64, error) {
|
||||
var dataSize int64
|
||||
buf := make([]byte, 4)
|
||||
|
||||
for i := 0; ; i++ {
|
||||
if i%ctxCheckInterval == 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return dataSize, ctx.Err()
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
frame, err := stream.ParseNext()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return dataSize, err
|
||||
}
|
||||
if len(frame.Subframes) != nChannels {
|
||||
return dataSize, fmt.Errorf("frame %d has %d channels, want %d", frame.Num, len(frame.Subframes), nChannels)
|
||||
}
|
||||
|
||||
for i := range frame.Subframes[0].Samples {
|
||||
for _, subframe := range frame.Subframes {
|
||||
putSample(buf, subframe.Samples[i], bytesPerSample)
|
||||
if _, err := w.Write(buf[:bytesPerSample]); err != nil {
|
||||
return dataSize, err
|
||||
}
|
||||
dataSize += int64(bytesPerSample)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dataSize, nil
|
||||
}
|
||||
|
||||
func putSample(buf []byte, sample int32, bytesPerSample int) {
|
||||
if bytesPerSample == 1 {
|
||||
buf[0] = byte(sample + 128)
|
||||
return
|
||||
}
|
||||
|
||||
value := uint32(sample)
|
||||
for i := range bytesPerSample {
|
||||
buf[i] = byte(value >> (8 * i))
|
||||
}
|
||||
}
|
||||
|
||||
func patchSizes(file *os.File, dataSize int64) error {
|
||||
buf := make([]byte, 4)
|
||||
|
||||
binary.LittleEndian.PutUint32(buf, uint32(headerSize-8+dataSize+dataSize%2))
|
||||
if _, err := file.WriteAt(buf, 4); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint32(buf, uint32(dataSize))
|
||||
_, err := file.WriteAt(buf, headerSize-4)
|
||||
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user