From bb04365dc387504c2b7a077c26d6015c4bb9cbec Mon Sep 17 00:00:00 2001 From: "Arnaldo E. Pereira" Date: Mon, 10 Nov 2025 14:10:23 -0600 Subject: [PATCH 1/2] Change brr_encoder to be more flexible with metadata headers --- src/brr_encoder.c | 314 +++++++++++++++++++++++++++------------------- 1 file changed, 184 insertions(+), 130 deletions(-) diff --git a/src/brr_encoder.c b/src/brr_encoder.c index 322d3a0..d208c48 100644 --- a/src/brr_encoder.c +++ b/src/brr_encoder.c @@ -400,144 +400,198 @@ int main(const int argc, char *const argv[]) char *inwav_path = argv[optind]; // Path of input and output files char *outbrr_path = argv[optind+1]; - FILE *inwav = fopen(inwav_path, "rb"); - if(!inwav) - { - fprintf(stderr, "Error : Can't open file %s for reading.\n", inwav_path); - exit(1); - } - - struct - { - char chunk_ID[4]; // Should be 'RIFF' - u32 chunk_size; - char wave_str[4]; // Should be 'WAVE' - char sc1_id[4]; // Should be 'fmt ' - u32 sc1size; // Should be at least 16 - u16 audio_format; // Should be 1 for PCM - u16 chans; // 1 for mono, 2 for stereo, etc... - u32 sample_rate; - u32 byte_rate; - u16 block_align; - u16 bits_per_sample; - } - hdr; - - // Read header - int err = fread(&hdr, 1, sizeof(hdr), inwav); - // If they couldn't read the file (for example if it's too small) - if(err != sizeof(hdr)) - { - fprintf(stderr, "Error : Input file in incompatible format %d\n", err); - exit(1); - } - - // Read "RIFF" word - if(strncmp(hdr.chunk_ID, "RIFF", 4)) - { - fprintf(stderr, "Error : Input file in unsupported format : \"RIFF\" block missing.\n"); - exit(1); - } - // "WAVEfmt" letters - if(strncmp(hdr.wave_str, "WAVEfmt ", 8)) - { - fprintf(stderr, "Input file in unsupported format : \"WAVEfmt\" block missing !\n"); - exit(1); - } - - //Size of sub-chunk1 (header) must be at least 16 and in PCM format - if(hdr.sc1size < 0x10 || hdr.audio_format != 1) - { - fprintf(stderr, "Input file in unsupported format : file must be uncompressed PCM !\n"); - exit(1); - } - - //Check how many channels - if(hdr.chans != 1) - printf("Input is multi-channel : Will automatically be converted to mono.\n"); - - // Check for correctness of byte rate - if(hdr.byte_rate != hdr.sample_rate*hdr.chans*hdr.bits_per_sample/8) - { - fprintf(stderr, "Byte rate in input file is set incorrectly.\n"); - exit(1); - } + FILE *inwav = fopen(inwav_path, "rb"); + if (!inwav) + { + fprintf(stderr, "Error : Can't open file %s for reading.\n", inwav_path); + exit(1); + } - //Read block align and bits per sample numbers - if(hdr.block_align != hdr.bits_per_sample*hdr.chans/8) - { - fprintf(stderr, "Block align in input file is set incorrectly\n"); - exit(1); - } - fseek(inwav, hdr.sc1size-0x10, SEEK_CUR); // nSkip possible longer header + // Read RIFF header (first 12 bytes only) + struct + { + char chunk_ID[4]; // Should be 'RIFF' + u32 chunk_size; + char wave_str[4]; // Should be 'WAVE' + } riff_hdr; + + int err = fread(&riff_hdr, 1, sizeof(riff_hdr), inwav); + if (err != sizeof(riff_hdr)) + { + fprintf(stderr, "Error : Input file too small or unreadable\n"); + exit(1); + } - struct - { - char name[4]; - u32 size; - } - sub_hdr; - while(true) - { - err = fread(&sub_hdr, 1, sizeof(sub_hdr), inwav); - if(err != sizeof(sub_hdr)) - { - fprintf(stderr, "End of file reached without finding a \"data\" chunk.\n"); - exit(1); - } - if(strncmp(sub_hdr.name, "data", 4)) // If there is anyother non-"data" block, skip it - fseek(inwav, sub_hdr.size, SEEK_CUR); - else - break; - } + // Validate RIFF header + if (strncmp(riff_hdr.chunk_ID, "RIFF", 4)) + { + fprintf(stderr, "Error : Input file in unsupported format : \"RIFF\" block missing.\n"); + exit(1); + } - // Output buffer - unsigned int samples_length = sub_hdr.size/hdr.block_align; - // Optional truncation of input sample - if(truncate_len && (truncate_len < samples_length)) - samples_length = truncate_len; + if (strncmp(riff_hdr.wave_str, "WAVE", 4)) + { + fprintf(stderr, "Error : Input file in unsupported format : \"WAVE\" marker missing.\n"); + exit(1); + } - Sample *samples = safe_malloc(WIDTH * samples_length); + // Variables to store fmt chunk data + struct + { + u32 sc1size; // fmt chunk size + u16 audio_format; // Should be 1 for PCM + u16 chans; // 1 for mono, 2 for stereo, etc... + u32 sample_rate; + u32 byte_rate; + u16 block_align; + u16 bits_per_sample; + } fmt_data; + + bool fmt_found = false; + + // Parse chunks until we find 'fmt ' + while (!fmt_found && !feof(inwav)) + { + char chunk_id[4]; + u32 chunk_size; + + if (fread(chunk_id, 1, 4, inwav) != 4) + break; + if (fread(&chunk_size, 1, 4, inwav) != 4) + break; + + if (strncmp(chunk_id, "fmt ", 4) == 0) + { + // Found fmt chunk - read it + fmt_found = true; + fmt_data.sc1size = chunk_size; + + // Read the standard 16 bytes of fmt data + if (fread(&fmt_data.audio_format, 1, 16, inwav) != 16) + { + fprintf(stderr, "Error : fmt chunk is too small\n"); + exit(1); + } + + // Validate format + if (fmt_data.sc1size < 0x10 || fmt_data.audio_format != 1) + { + fprintf(stderr, "Input file in unsupported format : file must be uncompressed PCM !\n"); + exit(1); + } + + // Check for correctness of byte rate + if (fmt_data.byte_rate != fmt_data.sample_rate * fmt_data.chans * fmt_data.bits_per_sample / 8) + { + fprintf(stderr, "Byte rate in input file is set incorrectly.\n"); + exit(1); + } + + // Check block align + if (fmt_data.block_align != fmt_data.bits_per_sample * fmt_data.chans / 8) + { + fprintf(stderr, "Block align in input file is set incorrectly\n"); + exit(1); + } + + // Check channels + if (fmt_data.chans != 1) + printf("Input is multi-channel : Will automatically be converted to mono.\n"); + + // Skip any extra bytes in fmt chunk (e.g., if chunk_size > 16) + if (fmt_data.sc1size > 16) + fseek(inwav, fmt_data.sc1size - 16, SEEK_CUR); + } + else + { + // Unknown chunk - skip it + fseek(inwav, chunk_size, SEEK_CUR); + // Handle odd-sized chunks (RIFF spec requires 16-bit alignment) + if (chunk_size % 2 == 1) + fseek(inwav, 1, SEEK_CUR); + } + } - // Adjust amplitude in function of amount of channels - ampl_adjust /= hdr.chans; - switch (hdr.bits_per_sample) - { - signed int sample; - case 8 : - for(int i=0; i < samples_length; ++i) - { - unsigned char in8_chns[hdr.chans]; - fread(in8_chns, 1, hdr.chans, inwav); // Read single sample on CHANS channels at a time - sample = 0; - for(int ch=0; ch < hdr.chans; ++ch) // Average samples of all channels - sample += in8_chns[ch]-0x80; - samples[i] = (Sample)((sample<<8) * ampl_adjust); - } - break; + if (!fmt_found) + { + fprintf(stderr, "Error : No \"fmt \" chunk found in WAV file\n"); + exit(1); + } - case 16 : - for(int i=0; i < samples_length; ++i) - { - signed short in16_chns[hdr.chans]; - fread(in16_chns, 2, hdr.chans, inwav); - sample = 0; - for(int ch=0; ch < hdr.chans; ++ch) - sample += in16_chns[ch]; - samples[i] = (Sample)(sample * ampl_adjust); - } - break; + // Now find the data chunk (rest of code stays mostly the same) + struct + { + char name[4]; + u32 size; + } sub_hdr; + + while (true) + { + err = fread(&sub_hdr, 1, sizeof(sub_hdr), inwav); + if (err != sizeof(sub_hdr)) + { + fprintf(stderr, "End of file reached without finding a \"data\" chunk.\n"); + exit(1); + } + if (strncmp(sub_hdr.name, "data", 4)) + { + // Skip non-data chunks + fseek(inwav, sub_hdr.size, SEEK_CUR); + // Handle odd-sized chunks + if (sub_hdr.size % 2 == 1) + fseek(inwav, 1, SEEK_CUR); + } + else + break; + } - // If you encounter the error below, add your implementation for different # of bits - default : - fprintf(stderr, "Error : unsupported amount of bits per sample (8 or 16 are supported)\n"); - exit(1); - } - fclose(inwav); // We're done with the input wave file + // Output buffer + unsigned int samples_length = sub_hdr.size / fmt_data.block_align; + // Optional truncation of input sample + if (truncate_len && (truncate_len < samples_length)) + samples_length = truncate_len; + + Sample *samples = safe_malloc(WIDTH * samples_length); + + // Adjust amplitude in function of amount of channels + ampl_adjust /= fmt_data.chans; + switch (fmt_data.bits_per_sample) + { + signed int sample; + case 8: + for (int i = 0; i < samples_length; ++i) + { + unsigned char in8_chns[fmt_data.chans]; + fread(in8_chns, 1, fmt_data.chans, inwav); + sample = 0; + for (int ch = 0; ch < fmt_data.chans; ++ch) + sample += in8_chns[ch] - 0x80; + samples[i] = (Sample)((sample << 8) * ampl_adjust); + } + break; + + case 16: + for (int i = 0; i < samples_length; ++i) + { + signed short in16_chns[fmt_data.chans]; + fread(in16_chns, 2, fmt_data.chans, inwav); + sample = 0; + for (int ch = 0; ch < fmt_data.chans; ++ch) + sample += in16_chns[ch]; + samples[i] = (Sample)(sample * ampl_adjust); + } + break; + + default: + fprintf(stderr, "Error : unsupported amount of bits per sample (8 or 16 are supported)\n"); + exit(1); + } + fclose(inwav); // We're done with the input wave file - if(target_samplerate) { - ratio = 1.0 * hdr.sample_rate / target_samplerate; - } + if (target_samplerate) + { + ratio = 1.0 * fmt_data.sample_rate / target_samplerate; + } unsigned int target_length; unsigned int new_loopsize; From 49fc8e73c7bb34302ccac036c2432b2a1a2c83fb Mon Sep 17 00:00:00 2001 From: "Arnaldo E. Pereira" Date: Tue, 11 Nov 2025 12:44:44 -0600 Subject: [PATCH 2/2] Add Python batch processing script --- scripts/batch_convert.py | 261 +++++++++++++++++++++++++++++++++++++++ scripts/requirements.txt | 1 + 2 files changed, 262 insertions(+) create mode 100644 scripts/batch_convert.py create mode 100644 scripts/requirements.txt diff --git a/scripts/batch_convert.py b/scripts/batch_convert.py new file mode 100644 index 0000000..4e9699c --- /dev/null +++ b/scripts/batch_convert.py @@ -0,0 +1,261 @@ +import argparse +import subprocess +import os +import sys +from pathlib import Path +from time import sleep + +from tqdm import tqdm + + +class BatchProcessor: + + def __init__(self, brrtools_dir, indir, outdir, rate): + self.infiles = BatchProcessor.get_infiles(indir) + self.outpath = BatchProcessor.get_outpath(outdir) + self.rate = rate + self.brrtools_bin = BatchProcessor.get_brrtools_bin(brrtools_dir) + + # Find Cygwin bash on Windows + if sys.platform == "win32": + self.cygwin_bash = self._find_cygwin_bash() + print(f"Using Cygwin bash: {self.cygwin_bash}") + + def process_batch(self): + success_count = 0 + error_count = 0 + + for infile in tqdm(self.infiles, desc="Processing"): + try: + self.process_file(infile) + success_count += 1 + except subprocess.CalledProcessError as e: + error_count += 1 + print(f"\n{'='*70}", file=sys.stderr) + print(f"ERROR processing {infile.name}", file=sys.stderr) + print(f"Exit code: {e.returncode}", file=sys.stderr) + if e.stdout: + print(f"Stdout: {e.stdout}", file=sys.stderr) + if e.stderr: + print(f"Stderr: {e.stderr}", file=sys.stderr) + print(f"{'='*70}", file=sys.stderr) + continue + except Exception as e: + error_count += 1 + print(f"\nUnexpected error processing {infile.name}: {e}", file=sys.stderr) + import traceback + traceback.print_exc() + continue + + print(f"\n{'='*70}") + print(f"Batch processing complete!") + print(f" Success: {success_count}/{len(self.infiles)}") + print(f" Errors: {error_count}/{len(self.infiles)}") + print(f"{'='*70}") + + @staticmethod + def get_infiles(indir): + dirpath = Path(indir) + if not dirpath.is_dir(): + raise ValueError(f"Input directory does not exist: {dirpath}") + + infiles = [item for item in dirpath.iterdir() + if item.is_file() and item.suffix.lower() == ".wav"] + + if not infiles: + raise ValueError(f"No .wav files found in {dirpath}") + + print(f"Found {len(infiles)} WAV files to process") + return infiles + + @staticmethod + def get_outpath(outdir): + outpath = Path(outdir) + if not outpath.exists(): + os.makedirs(outpath, exist_ok=True) + print(f"Created output directory: {outpath}") + + if not outpath.is_dir(): + raise ValueError(f"Output path exists but is not a directory: {outpath}") + + return outpath + + @staticmethod + def get_brrtools_bin(brrtools_dir): + brrtools_path = Path(brrtools_dir) + if not brrtools_path.is_dir(): + raise ValueError(f"BRRtools directory does not exist: {brrtools_path}") + + brrtools_bin = brrtools_path / "bin" + if not brrtools_bin.is_dir(): + raise ValueError(f"BRRtools bin directory does not exist: {brrtools_bin}") + + return brrtools_bin + + @staticmethod + def _find_cygwin_bash(): + """Find Cygwin bash.exe, avoiding WSL bash""" + # Common Cygwin installation paths + possible_paths = [ + Path("C:/cygwin64/bin/bash.exe"), + Path("C:/cygwin/bin/bash.exe"), + ] + + # Check environment variable + cygwin_root = os.environ.get("CYGWIN_ROOT") + if cygwin_root: + possible_paths.insert(0, Path(cygwin_root) / "bin" / "bash.exe") + + # Check standard locations first + for bash_path in possible_paths: + if bash_path.exists(): + return str(bash_path) + + # Try to find all bash.exe locations and filter for Cygwin + try: + result = subprocess.run( + ["where", "bash.exe"], + capture_output=True, + text=True, + check=False + ) + if result.returncode == 0: + bash_locations = result.stdout.strip().split('\n') + for location in bash_locations: + location = location.strip() + # Skip WSL bash (in System32 or system32) + if "system32" in location.lower(): + continue + # Accept Cygwin bash + if "cygwin" in location.lower() and Path(location).exists(): + return location + except Exception: + pass + + raise FileNotFoundError( + "Could not find Cygwin bash.exe.\n" + "Make sure Cygwin is installed.\n" + "If Cygwin is installed in a non-standard location, set CYGWIN_ROOT environment variable.\n" + "Example: set CYGWIN_ROOT=C:\\cygwin64" + ) + + def process_file(self, infile): + """Process a single WAV file through BRR encoding and decoding. + + Handles both Unix/Linux and Windows (Cygwin) environments. + """ + # Prepare file paths + in_wav = str(infile) + tmp_brr = self.outpath / f"{infile.stem}.brr" + out_wav = self.outpath / infile.name + + # Determine executable names based on platform + if sys.platform == "win32": + # Windows with Cygwin executables + brr_encoder_exe = "brr_encoder" + brr_decoder_exe = "brr_decoder" + + # Convert Windows paths to Cygwin format + in_wav_path = self._windows_to_cygwin_path(in_wav) + tmp_brr_path = self._windows_to_cygwin_path(str(tmp_brr)) + out_wav_path = self._windows_to_cygwin_path(str(out_wav)) + + # Full path to Cygwin executables + brr_encoder = self._windows_to_cygwin_path(str(self.brrtools_bin / brr_encoder_exe)) + brr_decoder = self._windows_to_cygwin_path(str(self.brrtools_bin / brr_decoder_exe)) + + # Run through Cygwin bash (NOT WSL bash!) + encode_cmd = f'"{brr_encoder}" -sb{self.rate} "{in_wav_path}" "{tmp_brr_path}"' + decode_cmd = f'"{brr_decoder}" -s{self.rate} -g "{tmp_brr_path}" "{out_wav_path}"' + + subprocess.run( + [self.cygwin_bash, "-c", encode_cmd], + check=True, + capture_output=True, + text=True + ) + subprocess.run( + [self.cygwin_bash, "-c", decode_cmd], + check=True, + capture_output=True, + text=True + ) + else: + # Mac/Linux - native executables + brr_encoder = str(self.brrtools_bin / "brr_encoder") + brr_decoder = str(self.brrtools_bin / "brr_decoder") + + subprocess.run( + [brr_encoder, f"-sb{self.rate}", in_wav, str(tmp_brr)], + check=True, + capture_output=True, + text=True + ) + subprocess.run( + [brr_decoder, f"-s{self.rate}", "-g", str(tmp_brr), str(out_wav)], + check=True, + capture_output=True, + text=True + ) + + # Delete temporary BRR file + if tmp_brr.exists(): + retries = 0 + # Add a retry loop with a short wait in case the file is temporarily locked by the OS. + while retries < 6: + try: + retries += 1 + os.remove(tmp_brr) + break + except Exception as e_del: + if retries < 6: + sleep(0.05) + continue + print(f"Warning: Could not remove temporary BRR file: {e_del}", file=sys.stderr) + break + + @staticmethod + def _windows_to_cygwin_path(win_path): + """Convert Windows path to Cygwin /cygdrive format. + + Examples: + C:\\Users\\file.wav -> /cygdrive/c/Users/file.wav + D:\\Music\\song.wav -> /cygdrive/d/Music/song.wav + """ + path = Path(win_path).resolve() + parts = path.parts + + # Check if path has a drive letter (e.g., C:\\) + if len(parts) > 0 and len(parts[0]) == 3 and parts[0][1] == ':': + drive = parts[0][0].lower() + rest = '/'.join(parts[1:]) + return f"/cygdrive/{drive}/{rest}" + else: + # Relative or UNC path - just convert backslashes + return str(path).replace('\\', '/') + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Batch process WAV files to make them sound like SNES sounds", + epilog="Requires Cygwin on Windows. Set CYGWIN_ROOT if installed in non-standard location." + ) + parser.add_argument("brrtools", type=str, help="Path to BRRTools directory") + parser.add_argument("indir", type=str, help="Path to input directory with WAV files") + parser.add_argument("outdir", type=str, help="Path to output directory") + parser.add_argument( + "--rate", "-r", type=int, default=16000, + help="Sample rate in Hz for BRR compression (default: 16000)" + ) + + args = parser.parse_args() + + try: + processor = BatchProcessor(args.brrtools, args.indir, args.outdir, args.rate) + processor.process_batch() + print("\nDONE") + except Exception as e: + print(f"\nFATAL ERROR: {e}", file=sys.stderr) + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..33899aa --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +tqdm >= 4 \ No newline at end of file