add find-and-enflac pcm compressor

This commit is contained in:
2026-08-08 21:31:52 +02:00
parent 2fe5db60c2
commit cb18297d11
5 changed files with 369 additions and 0 deletions

211
find-and-enflac/encode.go Normal file
View File

@@ -0,0 +1,211 @@
package main
import (
"bytes"
"context"
"fmt"
"io/fs"
"log/slog"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
// Container extensions holding uncompressed PCM. Raw headerless .pcm is
// excluded on purpose: sample rate, width and channel count cannot be
// recovered from the file itself.
var pcmExts = map[string]bool{
".wav": true,
".wave": true,
".bwf": true,
".rf64": true,
".bw64": true,
".w64": true,
".aif": true,
".aiff": true,
".aifc": true,
}
// findPCM returns every regular file under root whose extension names a
// PCM container. Symlinks are not followed.
func findPCM(root string) ([]string, error) {
var found []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.Type().IsRegular() {
return nil
}
if pcmExts[strings.ToLower(filepath.Ext(path))] {
found = append(found, path)
}
return nil
})
if err != nil {
return nil, err
}
sort.Strings(found)
return found, nil
}
type encoder struct {
threads int
dryRun bool
}
// encodeOne compresses src, verifies the result decodes back to
// identical audio, and only then removes src.
func (e *encoder) encodeOne(ctx context.Context, src string) error {
codecName, err := probeCodec(ctx, src)
if err != nil {
return err
}
var dst, codec, level string
switch {
case strings.HasPrefix(codecName, "pcm_f32"), strings.HasPrefix(codecName, "pcm_f64"):
dst, codec, level = replaceExt(src, ".wv"), "wavpack", "3"
case strings.HasPrefix(codecName, "pcm_"):
dst, codec, level = replaceExt(src, ".flac"), "flac", "12"
default:
slog.Warn("skipping, not PCM", "src", src, "codec", codecName)
return nil
}
if _, err := os.Lstat(dst); err == nil {
return fmt.Errorf("destination already exists: %s", dst)
}
if e.dryRun {
slog.Info("would encode", "src", src, "dst", dst, "codec", codec)
return nil
}
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
if err := ffmpeg(ctx,
"-threads", fmt.Sprint(e.threads),
"-i", src,
"-map", "0:a:0",
"-c:a", codec,
"-compression_level", level,
dst,
); err != nil {
os.Remove(dst)
return err
}
srcSum, err := audioMD5(ctx, src)
if err != nil {
os.Remove(dst)
return err
}
dstSum, err := audioMD5(ctx, dst)
if err != nil {
os.Remove(dst)
return err
}
if srcSum != dstSum {
os.Remove(dst)
return fmt.Errorf("verification failed, original kept: %s != %s", srcSum, dstSum)
}
dstInfo, err := os.Stat(dst)
if err != nil {
return err
}
if err := os.Remove(src); err != nil {
return err
}
slog.Info("encoded",
"src", src,
"dst", dst,
"ratio", fmt.Sprintf("%.3f", float64(dstInfo.Size())/float64(srcInfo.Size())),
)
return nil
}
// probeCodec reports the codec of the file's first audio stream, e.g.
// pcm_s24le or pcm_f32le.
func probeCodec(ctx context.Context, path string) (string, error) {
cmd := exec.CommandContext(ctx, "ffprobe",
"-v", "error",
"-select_streams", "a:0",
"-show_entries", "stream=codec_name",
"-of", "default=nw=1:nokey=1",
path,
)
cmd.Stdin = nil
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("ffprobe: %w: %s", err, strings.TrimSpace(stderr.String()))
}
codec := strings.TrimSpace(stdout.String())
if codec == "" {
return "", fmt.Errorf("no audio stream")
}
return codec, nil
}
// audioMD5 hashes the decoded audio, forced to 64-bit float so that
// neither 32-bit integer nor 32-bit float samples lose anything on the
// way to the hash. Without an explicit codec the md5 muxer would
// default to pcm_s16le and hide real differences.
func audioMD5(ctx context.Context, path string) (string, error) {
cmd := exec.CommandContext(ctx, "ffmpeg",
"-nostdin", "-v", "error",
"-i", path,
"-map", "0:a:0",
"-c:a", "pcm_f64le",
"-f", "md5", "-",
)
cmd.Stdin = nil
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("ffmpeg md5 of %s: %w: %s", path, err, strings.TrimSpace(stderr.String()))
}
sum := strings.TrimSpace(stdout.String())
if !strings.HasPrefix(sum, "MD5=") {
return "", fmt.Errorf("unexpected md5 output for %s: %q", path, sum)
}
return strings.TrimPrefix(sum, "MD5="), nil
}
func ffmpeg(ctx context.Context, args ...string) error {
full := append([]string{"-nostdin", "-v", "error"}, args...)
cmd := exec.CommandContext(ctx, "ffmpeg", full...)
cmd.Stdin = nil
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("ffmpeg: %w: %s", err, strings.TrimSpace(stderr.String()))
}
return nil
}
func replaceExt(path, ext string) string {
return strings.TrimSuffix(path, filepath.Ext(path)) + ext
}