flac: initial functionality complete
parent
4d89c2fe03
commit
d04d04801f
105
lib/flac/flac.go
105
lib/flac/flac.go
|
@ -3,8 +3,11 @@ package flac
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
|
@ -12,6 +15,7 @@ type Metadata struct {
|
||||||
BPS int64
|
BPS int64
|
||||||
Channels int64
|
Channels int64
|
||||||
NumSamples int64 // Total length of file in samples (per channel).
|
NumSamples int64 // Total length of file in samples (per channel).
|
||||||
|
MD5Sum string
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMetadata(path string) (md Metadata, err error) {
|
func GetMetadata(path string) (md Metadata, err error) {
|
||||||
|
@ -21,6 +25,7 @@ func GetMetadata(path string) (md Metadata, err error) {
|
||||||
"--show-bps",
|
"--show-bps",
|
||||||
"--show-channels",
|
"--show-channels",
|
||||||
"--show-total-samples",
|
"--show-total-samples",
|
||||||
|
"--show-md5sum",
|
||||||
path)
|
path)
|
||||||
|
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
|
@ -29,10 +34,10 @@ func GetMetadata(path string) (md Metadata, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
l := bytes.Split(out, []byte{'\n'})
|
l := bytes.Split(out, []byte{'\n'})
|
||||||
if len(l) != 5 {
|
if len(l) != 6 {
|
||||||
return md, fmt.Errorf("Output not understood:\n%s", out)
|
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)
|
md.SampleRate, err = strconv.ParseInt(string(l[0]), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -53,11 +58,103 @@ func GetMetadata(path string) (md Metadata, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return md, fmt.Errorf("Invalid length: %w", err)
|
return md, fmt.Errorf("Invalid length: %w", err)
|
||||||
}
|
}
|
||||||
|
md.MD5Sum = string(l[4])
|
||||||
|
|
||||||
return md, nil
|
return md, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(path string, nSamples int64) (L []float32, R []float32, err error) {
|
// Load nSamples of data from the given file.
|
||||||
// TODO!
|
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
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package flac
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetMetadata(t *testing.T) {
|
func TestGetMetadata(t *testing.T) {
|
||||||
|
@ -14,8 +15,37 @@ func TestGetMetadata(t *testing.T) {
|
||||||
BPS: 16,
|
BPS: 16,
|
||||||
Channels: 2,
|
Channels: 2,
|
||||||
NumSamples: 174564,
|
NumSamples: 174564,
|
||||||
|
MD5Sum: "c4f2c7de2f0f952354bfec3a0809c00e",
|
||||||
}
|
}
|
||||||
if md != expected {
|
if md != expected {
|
||||||
t.Fatalf("%v != %v", 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Reference in New Issue