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 }