This repository has been archived on 2019-12-11. You can view files and clone it, but cannot push or open issues/pull-requests.
jlaudio/lib/flac/flac.go

161 lines
3.4 KiB
Go

package flac
import (
"bytes"
"fmt"
"io"
"math"
"os/exec"
"strconv"
"unsafe"
)
type Metadata struct {
SampleRate int64
BPS int64
Channels int64
NumSamples int64 // Total length of file in samples (per channel).
MD5Sum string
}
func GetMetadata(path string) (md Metadata, err error) {
cmd := exec.Command(
"metaflac",
"--show-sample-rate",
"--show-bps",
"--show-channels",
"--show-total-samples",
"--show-md5sum",
path)
out, err := cmd.Output()
if err != nil {
return md, fmt.Errorf("Failed to open file: %w", err)
}
l := bytes.Split(out, []byte{'\n'})
if len(l) != 6 {
return md, fmt.Errorf("Output not understood:\n%s", out)
}
l = l[:5]
md.SampleRate, err = strconv.ParseInt(string(l[0]), 10, 64)
if err != nil {
return md, fmt.Errorf("Invalid sample rate: %w", err)
}
md.BPS, err = strconv.ParseInt(string(l[1]), 10, 64)
if err != nil {
return md, fmt.Errorf("Invalid bps: %w", err)
}
md.Channels, err = strconv.ParseInt(string(l[2]), 10, 64)
if err != nil {
return md, fmt.Errorf("Invalid channels: %w", err)
}
md.NumSamples, err = strconv.ParseInt(string(l[3]), 10, 64)
if err != nil {
return md, fmt.Errorf("Invalid length: %w", err)
}
md.MD5Sum = string(l[4])
return md, nil
}
// 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, 16384)
sampleVals := (*[1 << 44]int16)(unsafe.Pointer(&buf[0]))[:len(buf)/2]
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
}