塞尔达音频文件BFSTM/FSTM转WAV(使用ffmpeg)

7 阅读4分钟
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * 塞尔达BFSTM/FSTM转WAV(使用ffmpeg)
 * 支持:PCM和DSP ADPCM格式的BFSTM/FSTM文件
 * 
 * 要求:系统需要安装ffmpeg,并添加到PATH环境变量中
 * 下载ffmpeg:https://ffmpeg.org/download.html
 */
public class ZeldaBFSTM2WAV {

    /**
     * 检测ffmpeg是否可用
     */
    private static boolean checkFFmpeg() {
        try {
            ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-version");
            pb.redirectErrorStream(true);
            Process process = pb.start();
            
            // 读取输出
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
                String line = reader.readLine();
                if (line != null && line.contains("ffmpeg version")) {
                    System.out.println("✓ 检测到ffmpeg: " + line);
                    return true;
                }
            }
            
            int exitCode = process.waitFor();
            return exitCode == 0;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 使用ffmpeg转换BFSTM到WAV(默认显示详细输出)
     * @param inputFile 输入BFSTM文件
     * @param outputFile 输出WAV文件
     * @throws IOException 转换失败
     */
    public static void convertWithFFmpeg(String inputFile, String outputFile) throws IOException {
        convertWithFFmpeg(inputFile, outputFile, true);
    }

    /**
     * 使用ffmpeg转换BFSTM到WAV
     * @param inputFile 输入BFSTM文件
     * @param outputFile 输出WAV文件
     * @param verbose 是否显示详细输出
     * @throws IOException 转换失败
     */
    public static void convertWithFFmpeg(String inputFile, String outputFile, boolean verbose) throws IOException {
        File input = new File(inputFile);
        if (!input.exists()) {
            throw new IOException("输入文件不存在: " + inputFile);
        }

        // 检查ffmpeg
        if (!checkFFmpeg()) {
            throw new IOException("未检测到ffmpeg!\n" +
                    "请安装ffmpeg并添加到PATH环境变量中。\n" +
                    "下载地址:https://ffmpeg.org/download.html");
        }

        // 构建ffmpeg命令
        List<String> command = new ArrayList<>();
        command.add("ffmpeg");
        command.add("-i");
        command.add(input.getAbsolutePath());
        command.add("-acodec");
        command.add("pcm_s16le"); // 16位PCM,小端序
        command.add("-y"); // 覆盖输出文件
        if (!verbose) {
            command.add("-loglevel"); // 批量转换时减少日志输出
            command.add("error");
        }
        command.add(outputFile);

        if (verbose) {
            System.out.println("执行ffmpeg命令:");
            System.out.println(String.join(" ", command));
            System.out.println();
        }

        ProcessBuilder pb = new ProcessBuilder(command);
        pb.redirectErrorStream(true); // 合并stderr到stdout

        try {
            Process process = pb.start();

            // 读取ffmpeg输出(包含进度信息)
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    // 只在verbose模式下显示详细输出
                    if (verbose) {
                        System.out.println(line);
                    }
                }
            }

            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new IOException("ffmpeg转换失败,退出码: " + exitCode);
            }

            File output = new File(outputFile);
            if (!output.exists()) {
                throw new IOException("输出文件未生成: " + outputFile);
            }

            if (verbose) {
                System.out.println("\n✓ 转换成功!");
                System.out.println("输出文件: " + output.getAbsolutePath());
                System.out.println("文件大小: " + output.length() + " 字节");
            } else {
                System.out.println("✓ 转换成功: " + new File(outputFile).getName() + 
                        " (" + (output.length() / 1024) + " KB)");
            }

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("转换过程被中断", e);
        }
    }

    /**
     * 扫描目录下的所有BFSTM文件(递归查找子目录)
     * @param directory 目录路径
     * @return BFSTM文件列表
     */
    private static List<File> scanBFSTMFiles(String directory) {
        List<File> bfstmFiles = new ArrayList<>();
        File dir = new File(directory);
        
        if (!dir.exists() || !dir.isDirectory()) {
            System.err.println("目录不存在或不是有效目录: " + directory);
            return bfstmFiles;
        }
        
        scanBFSTMFilesRecursive(dir, bfstmFiles);
        
        return bfstmFiles;
    }

    /**
     * 递归扫描目录下的所有BFSTM文件
     * @param directory 当前目录
     * @param bfstmFiles 结果列表
     */
    private static void scanBFSTMFilesRecursive(File directory, List<File> bfstmFiles) {
        File[] files = directory.listFiles();
        
        if (files == null) {
            return;
        }
        
        for (File file : files) {
            if (file.isFile()) {
                String fileName = file.getName().toLowerCase();
                if (fileName.endsWith(".bfstm") || fileName.endsWith(".fstm")) {
                    bfstmFiles.add(file);
                }
            } else if (file.isDirectory()) {
                // 递归查找子目录
                scanBFSTMFilesRecursive(file, bfstmFiles);
            }
        }
    }

    /**
     * 批量转换目录下的所有BFSTM文件(不限制文件大小)
     * @param inputDir 输入目录
     * @param outputDir 输出目录(如果为null,则输出到输入目录)
     */
    public static void convertDirectory(String inputDir, String outputDir) {
        convertDirectory(inputDir, outputDir, 0);
    }

    /**
     * 批量转换目录下的所有BFSTM文件
     * @param inputDir 输入目录
     * @param outputDir 输出目录(如果为null,则输出到输入目录)
     * @param maxFileSizeKB 最大文件大小(KB),只转换小于此大小的文件,如果为0或负数则不限制
     */
    public static void convertDirectory(String inputDir, String outputDir, int maxFileSizeKB) {
        try {
            // 检查ffmpeg
            if (!checkFFmpeg()) {
                throw new IOException("未检测到ffmpeg!\n" +
                        "请安装ffmpeg并添加到PATH环境变量中。\n" +
                        "下载地址:https://ffmpeg.org/download.html");
            }

            File inputDirectory = new File(inputDir);
            if (!inputDirectory.exists() || !inputDirectory.isDirectory()) {
                throw new IOException("输入目录不存在或不是有效目录: " + inputDir);
            }

            // 确定输出目录
            File outputDirectory;
            if (outputDir != null && !outputDir.isEmpty()) {
                outputDirectory = new File(outputDir);
            } else {
                outputDirectory = inputDirectory; // 输出到输入目录
            }

            // 创建输出目录(如果不存在)
            if (!outputDirectory.exists()) {
                outputDirectory.mkdirs();
            }

            // 扫描BFSTM文件
            List<File> bfstmFiles = scanBFSTMFiles(inputDir);
            
            if (bfstmFiles.isEmpty()) {
                System.out.println("未找到BFSTM文件在目录: " + inputDir);
                return;
            }

            // 根据文件大小过滤
            if (maxFileSizeKB > 0) {
                long maxSizeBytes = maxFileSizeKB * 1024L;
                int originalCount = bfstmFiles.size();
                List<File> filteredFiles = new ArrayList<>();
                for (File file : bfstmFiles) {
                    if (file.length() >= maxSizeBytes) {
                        filteredFiles.add(file);
                    }
                }
                bfstmFiles.removeAll(filteredFiles);
                int filteredCount = filteredFiles.size();
                if (filteredCount > 0) {
                    System.out.println("过滤掉 " + filteredCount + " 个大于 " + maxFileSizeKB + " KB 的文件:");
                    for (File file : filteredFiles) {
                        System.out.println("  - " + file.getName() + " (" + (file.length() / 1024) + " KB)");
                    }
                    System.out.println();
                }
            }

            if (bfstmFiles.isEmpty()) {
                System.out.println("过滤后没有符合条件的BFSTM文件");
                return;
            }

            System.out.println("找到 " + bfstmFiles.size() + " 个BFSTM文件" + 
                    (maxFileSizeKB > 0 ? "(小于 " + maxFileSizeKB + " KB)" : ""));
            System.out.println("输入目录: " + inputDirectory.getAbsolutePath());
            System.out.println("输出目录: " + outputDirectory.getAbsolutePath());
            System.out.println();

            // 批量转换
            int successCount = 0;
            int failCount = 0;

            for (int i = 0; i < bfstmFiles.size(); i++) {
                File bfstmFile = bfstmFiles.get(i);
                String fileName = bfstmFile.getName();
                String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
                // 添加序号前缀(001_, 002_, ...)
                String sequenceNumber = String.format("%03d_", i + 1);
                String outputFileName = sequenceNumber + baseName + ".wav";
                String outputFile = new File(outputDirectory, outputFileName).getAbsolutePath();

                System.out.println("========================================");
                System.out.println("[" + (i + 1) + "/" + bfstmFiles.size() + "] 转换: " + fileName);
                System.out.println("输出: " + outputFile);
                System.out.println();

                try {
                    convertWithFFmpeg(bfstmFile.getAbsolutePath(), outputFile, false);
                    successCount++;
                } catch (Exception e) {
                    System.err.println("转换失败: " + fileName);
                    System.err.println("错误: " + e.getMessage());
                    failCount++;
                }

                System.out.println();
            }

            // 输出统计信息
            System.out.println("========================================");
            System.out.println("批量转换完成!");
            System.out.println("成功: " + successCount + " 个");
            System.out.println("失败: " + failCount + " 个");
            System.out.println("总计: " + bfstmFiles.size() + " 个");

        } catch (Exception e) {
            System.err.println("批量转换失败: " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 测试主方法
     */
    public static void main(String[] args) {
        // 方式1: 转换单个文件
        // String inputFile = "D:/Amiibo_Hit.bfstm";
        // String outputFile = "D:/Amiibo_Hit.wav";
        // try {
        //     System.out.println("开始转换BFSTM文件...");
        //     System.out.println("输入: " + inputFile);
        //     System.out.println("输出: " + outputFile);
        //     System.out.println();
        //     convertWithFFmpeg(inputFile, outputFile, true);
        // } catch (Exception e) {
        //     System.err.println("转换失败: " + e.getMessage());
        //     e.printStackTrace();
        // }

        // 方式2: 批量转换目录下的所有BFSTM文件
        String inputDir = "D:\game\wiiu塞尔达旷野之息1.6.0本体+MLC+1.271新手包\1.271\cemu1.27.1\Games\The Legend of Zelda-Breath of the Wild\content\Voice";
        String outputDir = "D:/222/"; // 输出到D盘根目录,如果为null则输出到输入目录
        int maxFileSizeKB = 500; // 只转换小于1000KB的文件,设置为0则不限制
        
        System.out.println("开始批量转换目录下的所有BFSTM文件...");
        convertDirectory(inputDir, outputDir, maxFileSizeKB);
    }
}