#!/usr/bin/python3 from argparse import ArgumentParser from os import system, environ, makedirs from os.path import exists, join as path_join from sys import stdin, exit from typing import Dict, Union import subprocess # Paths PATHS = { "src_dir": "src", "build_dir": "build", "template": "template.c", "substitute": "substituted.c", "output": "render_bytebeat", "fwrite_le_header": "fwrite_le.h", "fwrite_le": "fwrite_le.c", "include_directory": "include" } # Resolve paths PATHS["template"] = path_join(PATHS["src_dir"], PATHS["template"]) PATHS["substitute"] = path_join(PATHS["build_dir"], PATHS["substitute"]) PATHS["output"] = path_join(PATHS["build_dir"], PATHS["output"]) PATHS["fwrite_le"] = path_join(PATHS["src_dir"], PATHS["fwrite_le"]) # Add `.` directory before all paths for compilation PATHS["template"] = path_join(".", PATHS["template"]) PATHS["substitute"] = path_join(".", PATHS["substitute"]) PATHS["output"] = path_join(".", PATHS["output"]) PATHS["fwrite_le"] = path_join(".", PATHS["fwrite_le"]) PATHS["include_directory"] = path_join(".", PATHS["include_directory"]) # Default parameters DEFAULT_PARAMETERS = { "CC": "cc", "CFLAGS": "-Ofast -march=native -mtune=native -Wall -Wextra -Wpedantic " "-pedantic -Wno-unused-variable -Wno-unused-but-set-variable " "-Wno-dangling-else -Wno-parentheses -std=c99", "INPUT_FILE": PATHS["substitute"], "OUTPUT_FILE": PATHS["output"] } def fetch(name: str): if from_env := environ.get(name): return from_env elif name != "CFLAGS_EXTRA": return DEFAULT_PARAMETERS[name] def read_file(path: str) -> str: return open(path, "r", encoding="utf-8-sig").read() def rewrite_file(path: str, content: str): return open(path, "w", encoding="utf-8").write(content) def read_from_file_or_stdin(path: str) -> str: if path == "-": return "\n".join(stdin) elif exists(path): return read_file(path) else: print("The specified file doesn't exist") raise SystemExit def substitute_vars(replacements: Dict[str, Union[bool, str]], text: str, verbose: bool) -> str: if verbose: print("Substituting values:") for placeholder, replacement in replacements.items(): if isinstance(replacement, bool): replacement = preprocessor_bool(replacement) if verbose and placeholder != "bytebeat_contents": print(placeholder, ": ", replacement, sep="") text = text.replace(f"`{placeholder}`", str(replacement)) if verbose: print() return text preprocessor_bool = lambda value: "1" if value else "0" C_str_repr = lambda s: '"' + s.replace("\\", "\\\\").replace(r'"', r'\"') + '"' CC = fetch("CC") CFLAGS = fetch("CFLAGS") INPUT_FILE = fetch("INPUT_FILE") OUTPUT_FILE = fetch("OUTPUT_FILE") if extra := fetch("CFLAGS_EXTRA"): CFLAGS += " " + extra if __name__ == "__main__": parser = ArgumentParser(description=\ "Substitutes supplied C (non-JavaScript!) bytebeat into the template, " "then attempts to compile the instance of the template. Accepts " "environmental variables `CC`, `CFLAGS`, `INPUT_FILE`, `OUTPUT_FILE`. " "`CFLAGS_EXTRA` can be used to add to default `CFLAGS`.") parser.add_argument("file", type=str, help="bytebeat formula file (use `-` to read from stdin)") parser.add_argument("-o", "--output", default="output.wav", type=str, help="specify output file path (default is `output.wav`") parser.add_argument("-r", "--sample-rate", default=8000, type=int, help="sample rate (Hz)") parser.add_argument("-p", "--final-sample-rate", default=None, type=int, help="convert the output to a different sample rate (usually one that " "is set in the system, to improve sound quality) during generation " "(not just reinterpretation)") parser.add_argument("-b", "--bit-depth", default=8, type=int, help="bit depth") parser.add_argument("-s", "--signed", default=False, action="store_true", help="is signed?") parser.add_argument("-R", "--precalculate-ratio", default=False, action="store_true", help="precalculate sample ratio to speed up rendering (may produce " "inaccurate results") parser.add_argument("-m", "--faster-sample-ratio-math", default=False, action="store_true", help="faster sample ratio math (implies argument -R)") parser.add_argument("-f", "--floating-point", default=False, action="store_true", help="use floating point as the return type") parser.add_argument("-c", "--channels", default=1, type=int, help="amount of channels") parser.add_argument("-t", "--seconds", default=None, type=int, help="length in seconds (samples = sample rate * seconds) : " "default = 30 seconds") parser.add_argument("-l", "--samples", default=None, type=int, help="length in samples (adds to `-t`; supports negative numbers) : " "default = +0 samples") parser.add_argument("-a", "--no-return", default=False, action="store_true", help="do not insert return statement before the code") parser.add_argument("-u", "--mode", default="sequential", type=str, help="mode of writing: `sequential` or `instant` (the latter is not " "recommended, since the whole result would be stored in RAM)") parser.add_argument("-n", "--block-size", default=65536, type=int, help="sequential mode only: block size of each sequence, bytes") parser.add_argument("-q", "--silent", default=False, action="store_true", help="do not output anything during generation") parser.add_argument("-v", "--verbose", default=False, action="store_true", help="show progress during generation") parser.add_argument("-E", "--show-substituted-values", default=False, action="store_true", help="show substituted values") args = parser.parse_args() bytebeat_contents = read_from_file_or_stdin(args.file).strip() if not bytebeat_contents: print("No valid contents") raise SystemExit # - Compilation makedirs(PATHS["build_dir"], exist_ok=True) if not args.no_return: # Insert `return` statement # XXX: The bytebeat code is enclosed in parentheses to allow for the # use of commas as a comma operator, enabling more formulas to function. bytebeat_contents = f"return ({bytebeat_contents})" original_sample_rate = args.sample_rate final_sample_rate_code = "" if args.faster_sample_ratio_math: args.precalculate_ratio = True if args.precalculate_ratio and not args.final_sample_rate is None: if args.faster_sample_ratio_math: sample_rate_ratio = args.sample_rate / args.final_sample_rate final_sample_rate_code = f"w *= {sample_rate_ratio}L;" else: sample_rate_ratio = args.final_sample_rate / args.sample_rate final_sample_rate_code = f"w /= {sample_rate_ratio}L;" args.sample_rate = args.final_sample_rate final_sample_rate = \ value if (value := args.final_sample_rate) else original_sample_rate samples = 0 while True: no_seconds = args.seconds is None or args.seconds == 0 no_samples = args.samples is None or args.samples == 0 seconds_specified = not no_seconds samples_specified = not no_samples if seconds_specified and args.seconds < 0: print("CLI: Count of seconds can't be less than zero.") raise SystemExit if no_seconds and samples_specified: samples = args.samples elif seconds_specified and samples_specified: samples = args.seconds * final_sample_rate + args.samples elif seconds_specified and no_samples: samples = args.seconds * final_sample_rate elif no_seconds and no_samples: args.seconds = 30 # default continue else: print("CLI: Incorrect seconds/samples length format.") raise SystemExit break if samples <= 0: print("CLI: Count of samples should be greater than zero.") raise SystemExit if args.mode != "sequential" and args.mode != "instant": print("Invalid mode '%s'" % args.mode) raise SystemExit gen_length = args.channels * samples rewrite_file(PATHS["substitute"], substitute_vars({ "bytebeat_contents": bytebeat_contents, "output_file": C_str_repr(args.output), "sample_rate": \ value if (value := args.final_sample_rate) else args.sample_rate, "original_sample_rate": original_sample_rate, "final_sample_rate_code": final_sample_rate_code, "bit_depth": args.bit_depth, "is_signed": args.signed, "precalculated_ratio": args.precalculate_ratio, "faster_sample_ratio_math": args.precalculate_ratio, "fp_return_type": args.floating_point, "channels": args.channels, "length": samples, "wav_product": gen_length * (args.bit_depth // 8), "gen_length": gen_length, "sequential_mode": args.mode == "sequential", "block_size": args.block_size, "silent_mode": args.silent, "verbose_mode": args.verbose and not args.silent, "fwrite_le": PATHS["fwrite_le_header"] }, read_file(PATHS["template"]), args.show_substituted_values)) # Compile by invoking the shell script print("Compiling") # Let the system execute aliases by calling os.system command = " ".join([CC, CFLAGS, INPUT_FILE, PATHS["fwrite_le"], "-o", OUTPUT_FILE, "-I" + PATHS["include_directory"]]) print(command, flush=True) exit(system(command))