From d04d04801ffc56c01e2ea78e1d3c06260dd390ae Mon Sep 17 00:00:00 2001 From: "J. David Lee" Date: Sun, 1 Dec 2019 22:12:58 +0100 Subject: [PATCH] flac: initial functionality complete --- lib/flac/flac.go | 105 ++++++++++++++++++++++++++++++++++++++++-- lib/flac/flac_test.go | 30 ++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/lib/flac/flac.go b/lib/flac/flac.go index fa6c6dc..f368389 100644 --- a/lib/flac/flac.go +++ b/lib/flac/flac.go @@ -3,8 +3,11 @@ package flac import ( "bytes" "fmt" + "io" + "math" "os/exec" "strconv" + "unsafe" ) type Metadata struct { @@ -12,6 +15,7 @@ type Metadata struct { BPS int64 Channels int64 NumSamples int64 // Total length of file in samples (per channel). + MD5Sum string } func GetMetadata(path string) (md Metadata, err error) { @@ -21,6 +25,7 @@ func GetMetadata(path string) (md Metadata, err error) { "--show-bps", "--show-channels", "--show-total-samples", + "--show-md5sum", path) out, err := cmd.Output() @@ -29,10 +34,10 @@ func GetMetadata(path string) (md Metadata, err error) { } l := bytes.Split(out, []byte{'\n'}) - if len(l) != 5 { + if len(l) != 6 { return md, fmt.Errorf("Output not understood:\n%s", out) } - l = l[:4] + l = l[:5] md.SampleRate, err = strconv.ParseInt(string(l[0]), 10, 64) if err != nil { @@ -53,11 +58,103 @@ func GetMetadata(path string) (md Metadata, err error) { if err != nil { return md, fmt.Errorf("Invalid length: %w", err) } + md.MD5Sum = string(l[4]) return md, nil } -func Load(path string, nSamples int64) (L []float32, R []float32, err error) { - // TODO! +// Load nSamples of data from the given file. +func Load(path string) (L []float32, R []float32, err error) { + md, err := GetMetadata(path) + if err != nil { + return nil, nil, err + } + if md.BPS != 16 || md.Channels != 2 { + return nil, nil, fmt.Errorf("Only 16 bit stereo flac files are supported.") + } + + cmd := exec.Command( + "flac", + "--decode", + "--stdout", + "--force-raw-format", + "--endian=little", + "--sign=signed", + path) + r, err := cmd.StdoutPipe() + if err != nil { + return nil, nil, fmt.Errorf("Failed to connect to flac process: %w", err) + } + defer r.Close() + + if err := cmd.Start(); err != nil { + return nil, nil, fmt.Errorf("Failed to start flac process: %w", err) + } + + // Allocate L and R, and fill. + L = make([]float32, md.NumSamples) + R = make([]float32, md.NumSamples) + + written := 0 + buf := make([]byte, 4096) + sampleVals := (*[1 << 44]int16)(unsafe.Pointer(&buf[0]))[:2048] + + for { + n, err := r.Read(buf) + if n != 0 { + for i := range sampleVals[:n/4] { + L[written] = float32(sampleVals[2*i]) / math.MaxInt16 + R[written] = float32(sampleVals[2*i+1]) / math.MaxInt16 + written++ + } + } + if err == io.EOF { + break + } + if err != nil { + return L, R, fmt.Errorf("Failed to read from flac process: %w", err) + } + } + return } + +// Save to path, assuming L and R are normalized to [-1,1] +func Save(path string, L, R []float32) error { + cmd := exec.Command( + "flac", + "--best", + "--force", + "--no-padding", + "--force-raw-format", + "--endian=little", + "--sign=signed", + "--channels=2", + "--bps=16", + "--sample-rate=48000", + "--input-size="+fmt.Sprintf("%d", 4*len(L)), // In bytes. + "-", + "-o", path) + w, err := cmd.StdinPipe() + if err != nil { + return fmt.Errorf("Failed to connect to flac process: %w", err) + } + defer w.Close() + + if err := cmd.Start(); err != nil { + return fmt.Errorf("Failed to start flac process: %w", err) + } + + iBuf := make([]int16, 2*len(L)) + for i := range L { + iBuf[2*i] = int16(math.MaxInt16 * L[i]) + iBuf[2*i+1] = int16(math.MaxInt16 * R[i]) + } + + buf := (*[1 << 44]byte)(unsafe.Pointer(&iBuf[0]))[:4*len(L)] + if _, err := w.Write(buf); err != nil { + return fmt.Errorf("Failed to write flac data: %w", err) + } + + return nil +} diff --git a/lib/flac/flac_test.go b/lib/flac/flac_test.go index 4c49dec..61e0e96 100644 --- a/lib/flac/flac_test.go +++ b/lib/flac/flac_test.go @@ -2,6 +2,7 @@ package flac import ( "testing" + "time" ) func TestGetMetadata(t *testing.T) { @@ -14,8 +15,37 @@ func TestGetMetadata(t *testing.T) { BPS: 16, Channels: 2, NumSamples: 174564, + MD5Sum: "c4f2c7de2f0f952354bfec3a0809c00e", } if md != expected { t.Fatalf("%v != %v", md, expected) } } + +func TestLoad(t *testing.T) { + l, r, err := Load("test_files/audio.flac") + if err != nil { + t.Fatal(err) + } + + if err := Save("test_files/out.flac", l, r); err != nil { + t.Fatal(err) + } + + // Apparently necessary. + time.Sleep(time.Second) + + md1, err := GetMetadata("test_files/audio.flac") + if err != nil { + t.Fatal(err) + } + + md2, err := GetMetadata("test_files/out.flac") + if err != nil { + t.Fatal(err) + } + + if md1 != md2 { + t.Fatalf("%v != %v", md2, md1) + } +}