diff --git a/bytebeat_compiler.py b/bytebeat_compiler.py index 1761328..dc9dcb4 100644 --- a/bytebeat_compiler.py +++ b/bytebeat_compiler.py @@ -1,11 +1,15 @@ #!/usr/bin/python3 +if __name__ == "__main__": + print(":: C bytebeat generator: compiler unit") + from argparse import ArgumentParser from os import environ, makedirs from os.path import exists, join as path_join from shlex import split as command_line_split from sys import stdin, stdout, exit from typing import Dict, Union +import re import subprocess # Paths @@ -65,7 +69,7 @@ def read_from_file_or_stdin(path: str) -> str: raise SystemExit def substitute_vars(replacements: Dict[str, Union[bool, str]], text: str, - verbose: bool) -> str: + verbose: bool) -> str: if verbose: print("Substituting values:") for placeholder, replacement in replacements.items(): @@ -89,6 +93,26 @@ OUTPUT_FILE = fetch("OUTPUT_FILE") if extra := fetch("CFLAGS_EXTRA"): CFLAGS += " " + extra +parameter_line_regex = re.compile(r"^\/\/ *RENDER PARAMETERS *: ", re.MULTILINE) + +ALLOWED_ARGUMENTS_FROM_FILE = ( + "sample_rate", + "final_sample_rate", + "bit_depth", + "signed", + "channels", + "no_return", +) + +DEFAULT_ARGS = { + "sample_rate": 8000, + "final_sample_rate": 0, + "bit_depth": 8, + "signed": False, + "channels": 1, + "no_return": False +} + if __name__ == "__main__": parser = ArgumentParser(description=\ "Substitutes supplied C (non-JavaScript!) bytebeat into the template, " @@ -98,37 +122,45 @@ if __name__ == "__main__": 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="specify output file path : default is `output.wav`") + parser.add_argument("-r", "--sample-rate", default=None, 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, + parser.add_argument("-b", "--bit-depth", default=None, type=int, help="bit depth") - parser.add_argument("-s", "--signed", default=False, action="store_true", + parser.add_argument("-s", "--signed", default=None, action="store_true", help="is signed?") + parser.add_argument("-u", "--unsigned", default=None, action="store_true", + help="is unsigned? (overrides the 'is signed' parameter)") parser.add_argument("-R", "--precalculate-ratio", default=False, action="store_true", help="precalculate sample ratio to speed up rendering (may produce " - "inaccurate results") + "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, + parser.add_argument("-c", "--channels", default=None, 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", + "default = seconds + 0 samples") + parser.add_argument("-S", "--skip-first", default=None, type=str, + help="skip first `A` seconds and `B` samples: in format `As`, `B` or " + "`AsB` : default = 0") + parser.add_argument("-k", "--repeat", default=0, type=int, + help="how many times to repeat the bytebeat : " + "default = 0") + parser.add_argument("-a", "--no-return", default=None, action="store_true", help="do not insert return statement before the code") - parser.add_argument("-u", "--mode", default="sequential", type=str, + 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, @@ -148,7 +180,42 @@ if __name__ == "__main__": if not bytebeat_contents: print("No valid contents") - raise SystemExit + exit(1) + + # - Parse arguments from file + used_parameter_line = False + for line in bytebeat_contents.splitlines(): + if (match := re.search(parameter_line_regex, line)) and \ + not used_parameter_line: + used_parameter_line = True + + parsed_parameters = line[match.start(0):].split(",") + for parameter in parsed_parameters: + kv = [x.strip() for x in parameter.split("=")] + + key = None + value = None + + if len(kv) == 1: + key, value = kv[0], True + elif len(kv) == 2: + key, value = kv[0], int(kv[1]) + else: + break + + # Apply the argument only if it was not used by user yet and is + # allowed to be set + if (key not in args or getattr(args, key) is None) and \ + key in ALLOWED_ARGUMENTS_FROM_FILE: + setattr(args, key, value) + + # - Set default values + for key, value in DEFAULT_ARGS.items(): + if getattr(args, key) is None: + setattr(args, key, value) + + if args.unsigned is True: + args.signed = False # - Compilation makedirs(PATHS["build_dir"], exist_ok=True) @@ -162,7 +229,7 @@ if __name__ == "__main__": 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.precalculate_ratio and args.final_sample_rate != 0: if args.faster_sample_ratio_math: sample_rate_ratio = args.sample_rate / args.final_sample_rate final_sample_rate_code = f"time *= {sample_rate_ratio}L;" @@ -172,7 +239,8 @@ if __name__ == "__main__": args.sample_rate = args.final_sample_rate final_sample_rate = \ - value if (value := args.final_sample_rate) else original_sample_rate + value if (value := args.final_sample_rate) != 0 \ + else original_sample_rate samples = 0 while True: no_seconds = args.seconds is None or args.seconds == 0 @@ -182,7 +250,7 @@ if __name__ == "__main__": if seconds_specified and args.seconds < 0: print("CLI: Count of seconds can't be less than zero.") - raise SystemExit + exit(1) if no_seconds and samples_specified: samples = args.samples @@ -195,27 +263,53 @@ if __name__ == "__main__": continue else: print("CLI: Incorrect seconds/samples length format.") - raise SystemExit + exit(1) break if samples <= 0: print("CLI: Count of samples should be greater than zero.") - raise SystemExit + exit(1) if args.mode != "sequential" and args.mode != "instant": print("Invalid mode '%s'" % args.mode) - raise SystemExit - - gen_length = args.channels * samples + exit(1) ansi_escape_codes_supported = args.color == "auto" and stdout_atty or \ args.color == "always" + actual_sample_rate = \ + value if (value := args.final_sample_rate) else args.sample_rate + + # - Parse the '--skip-first' argument + if not args.skip_first is None: + encountered_s = False + for character in args.skip_first: + if character.isdigit() or character == "s" and not encountered_s: + if character == "s": + encountered_s = True + else: + print(f"Invalid --skip-first format: `{args.skip_first}`") + exit(1) + + skip_first = \ + [int(x) if x.isdigit() else 0 for x in args.skip_first.split("s")] + + skip_first_samples = 0 + if len(skip_first) == 1: + skip_first += [0] + + skip_first_samples = skip_first[0] * actual_sample_rate + skip_first[1] + else: + skip_first_samples = 0 + + length_formula = lambda channels, samples, n: channels * (samples + n) + gen_length = length_formula(args.channels, samples, 0) + loop_end = length_formula(args.channels, samples, skip_first_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, + "sample_rate": actual_sample_rate, "original_sample_rate": original_sample_rate, "final_sample_rate_code": final_sample_rate_code, "bit_depth": args.bit_depth, @@ -224,7 +318,11 @@ if __name__ == "__main__": "faster_sample_ratio_math": args.precalculate_ratio, "fp_return_type": args.floating_point, "channels": args.channels, - "length": samples, + "running_length": samples, + "loop_end": loop_end, + "loop_end_minus_1": loop_end - 1, + "initial_time": skip_first_samples, + "repeat_times": args.repeat, "wav_product": gen_length * (args.bit_depth // 8), "gen_length": gen_length, "sequential_mode": args.mode == "sequential", diff --git a/samples/beeper.c b/samples/beeper.c new file mode 100644 index 0000000..54034a7 --- /dev/null +++ b/samples/beeper.c @@ -0,0 +1,5 @@ +// RENDER PARAMETERS: sample_rate = 44100, no_return + +bb_counter_t u = t << 1; +SAMPLE_TYPE x = u & u >> 8; +return (x | 127) + 63; diff --git a/samples/cykstep.c b/samples/cykstep.c new file mode 100644 index 0000000..ed3535c --- /dev/null +++ b/samples/cykstep.c @@ -0,0 +1,3 @@ +// RENDER PARAMETERS: sample_rate = 44100, final_sample_rate = 48000, signed + +t/=3,(t*(t+(444+(t/444)))&((t>>9)>4095 ? 4095 : (t>>(9+(3&t>>13)))))>>(3&t>>(5+(3&t>>13))) diff --git a/samples/melody.c b/samples/melody.c new file mode 100644 index 0000000..9c4ba5c --- /dev/null +++ b/samples/melody.c @@ -0,0 +1,5 @@ +// RENDER PARAMETERS: sample_rate = 44100, no_return + +const long double array[] = {1, 1.25, 1.5, 2}; +long double v = array[3 & (t >> 13)]; +return (long double) t * v / 3.1415926535; diff --git a/src/template.c b/src/template.c index 6af8835..c7e0e8e 100644 --- a/src/template.c +++ b/src/template.c @@ -9,6 +9,10 @@ #include "`fwrite_le`" +// typedefs +typedef uintmax_t bb_counter_t; +typedef long double bb_fp_return_t; + // constants #define ANSI_ESCAPE_CODES_SUPPORTED `ansi_escape_codes_supported` @@ -29,20 +33,41 @@ #define BIT_DEPTH `bit_depth` #define IS_SIGNED `is_signed` #define CHANNELS `channels` -#define LENGTH `length` +#define RUNNING_LENGTH `running_length` #if BIT_DEPTH <= 8 +# define ACTUAL_BIT_DEPTH 8 + # define SAMPLE_TYPE uint8_t -#elif BIT_DEPTH >= 16 +#elif BIT_DEPTH <= 16 +# define ACTUAL_BIT_DEPTH 16 + # if IS_SIGNED # define SAMPLE_TYPE int16_t # else # define SAMPLE_TYPE uint16_t # endif +#elif BIT_DEPTH <= 32 +# define ACTUAL_BIT_DEPTH 32 + +# if IS_SIGNED +# define SAMPLE_TYPE int32_t +# else +# define SAMPLE_TYPE uint32_t +# endif +#else +# error "Unsupported bit depth" +# define _ERROR #endif -#define PRODUCT `wav_product` -#define GEN_LENGTH `gen_length` +#ifndef _ERROR + +#define PRODUCT `wav_product`ULL +#define GEN_LENGTH `gen_length`ULL +#define INITIAL_TIME `initial_time` +#define LOOP_END `loop_end`ULL +#define LOOP_END_MINUS_1 `loop_end_minus_1`ULL +#define REPEAT_TIMES `repeat_times`ULL #define FREQUENCY_OF_STATUS_REPORTING 5000 #define SEQUENTIAL_MODE `sequential_mode` @@ -53,8 +78,8 @@ #define FP_RETURN_TYPE `fp_return_type` #define PRECALCULATED_RATIO `precalculated_ratio` -#define BIT_DEPTH_LIMITER ((1 << BIT_DEPTH) - 1) -#define PCM_COEFFICIENT ((1 << (BIT_DEPTH - 1)) - 1) +#define BIT_DEPTH_LIMITER ((1ULL << ACTUAL_BIT_DEPTH) - 1ULL) +#define PCM_COEFFICIENT ((1ULL << (ACTUAL_BIT_DEPTH - 1ULL)) - 1ULL) #define unsigned_to_signed(x) (x - PCM_COEFFICIENT) #define signed_to_unsigned(x) (x + PCM_COEFFICIENT) @@ -78,19 +103,19 @@ // function prototypes #if FP_RETURN_TYPE -long double +bb_fp_return_t #else SAMPLE_TYPE #endif -bytebeat(long double time); +bytebeat(bb_counter_t time); // function implementations #if FP_RETURN_TYPE -long double +bb_fp_return_t #else SAMPLE_TYPE #endif -bytebeat(long double time) +bytebeat(bb_counter_t time) { #if PRECALCULATED_RATIO `final_sample_rate_code` @@ -105,39 +130,10 @@ bytebeat(long double time) `bytebeat_contents`; } -int -main(void) -{ - // * log -> welcome -#if !SILENT_MODE - puts(":: C bytebeat generator runtime unit"); - fflush(stdout); -#endif - -#if !SILENT_MODE - const uintmax_t seconds = LENGTH / SAMPLE_RATE, - samples = LENGTH % SAMPLE_RATE; - - printf( - "\n" - "Sample rate: " INT2STR(SAMPLE_RATE) " Hz\n" - "Channels: " INT2STR(CHANNELS) -#if CHANNELS <= 2 - " (" -# if CHANNELS == 2 - "stereo" -# else - "mono" -# endif - ")" -#endif - "\n" - "Bit depth: " -#if !IS_SIGNED - "un" -#endif - "signed " INT2STR(BIT_DEPTH) "-bit\n" - "Duration: "); +void +print_time(uintmax_t length) { + const uintmax_t seconds = length / SAMPLE_RATE, + samples = length % SAMPLE_RATE; if (seconds > 0) if (seconds >= 3600) @@ -156,6 +152,54 @@ main(void) if (samples > 0) printf("%" PRIuMAX " samples", samples); +} + +int +main(void) +{ + // * log -> welcome +#if !SILENT_MODE + puts(":: C bytebeat generator: runtime unit"); + fflush(stdout); +#endif + +#if !SILENT_MODE + printf( + "\n" + "Sample rate: " INT2STR(SAMPLE_RATE) " Hz\n" + "Channels: " INT2STR(CHANNELS) +#if CHANNELS <= 2 + " (" +# if CHANNELS == 2 + "stereo" +# else + "mono" +# endif + ")" +#endif + "\n" + "Bit depth: " +#if !IS_SIGNED + "un" +#endif + "signed " INT2STR(BIT_DEPTH) "-bit" +#if BIT_DEPTH != ACTUAL_BIT_DEPTH + " -> " INT2STR(ACTUAL_BIT_DEPTH) "-bit" +#endif + "\n" + "Duration: "); + + print_time(RUNNING_LENGTH); + +#if REPEAT_TIMES > 0 + printf(", %llu times -> ", REPEAT_TIMES + 1); + print_time(RUNNING_LENGTH * (REPEAT_TIMES + 1)); +#endif + +#if INITIAL_TIME != 0 + printf("\nStart time: "); + print_time(INITIAL_TIME); +#endif puts( #if VERBOSE_MODE || SEQUENTIAL_MODE @@ -169,39 +213,47 @@ main(void) // * write WAVE headers // 0. log -#if SEQUENTIAL_MODE +#if VERBOSE_MODE puts("Writing WAVE headers..."); #endif // 1. open file - FILE* output_file = fopen(OUTPUT_FILE, "wb"); + FILE* output_file = fopen(OUTPUT_FILE, + "wb" +#if REPEAT_TIMES > 0 + "+" +#endif + ); if (output_file == NULL) { fflush(stdout); perror("fopen"); - return 1; + return EXIT_FAILURE; } // 2. prepare variables - uint32_t buffer_size = PRODUCT, + const uint32_t header_size = + 4 * 4 /* 4 strings of 4 characters */ + + 5 * 4 /* 5 uint32_t values */ + + 4 * 2 /* 4 uint16_t values */; + + uint32_t buffer_size = PRODUCT * (REPEAT_TIMES + 1), file_length = - 4 * 4 /* 4 strings of 4 characters */ + - 5 * 4 /* 4 uint32_t values */ + - 4 * 2 /* 5 uint16_t values */ + - PRODUCT /* sample data */ + header_size + + buffer_size /* sample data */ /* subtract Subchunk2 headers: */ - - 4 /* a 4-character string */ + - 4 /* a string of 4 characters */ - 4 /* a uint32_t value */, fmt_data_length = 16 /* <-- * length of format data before this value * in the file format structure */, sample_rate = SAMPLE_RATE, - byte_rate = (SAMPLE_RATE * BIT_DEPTH * CHANNELS) / 8; + byte_rate = (SAMPLE_RATE * ACTUAL_BIT_DEPTH * CHANNELS) / 8; uint16_t fmt_type = 1, // format type is PCM channels = CHANNELS, - block_align = (BIT_DEPTH * CHANNELS) / 8, - bit_depth = BIT_DEPTH > 8 ? BIT_DEPTH : 8; + block_align = (ACTUAL_BIT_DEPTH * CHANNELS) / 8, + bit_depth = ACTUAL_BIT_DEPTH; // 3. write headers // : : @@ -237,22 +289,23 @@ main(void) size_t calc_block_size = BLOCK_SIZE; ALLOCATE_MEMORY(calc_block_size) #else - ALLOCATE_MEMORY(PRODUCT) + ALLOCATE_MEMORY(GEN_LENGTH) #endif -#if SEQUENTIAL_MODE - const uintmax_t gen_length_minus_1 = GEN_LENGTH - 1, - bit_depth_limiter = BIT_DEPTH_LIMITER + const uintmax_t bit_depth_limiter = BIT_DEPTH_LIMITER #if FP_RETURN_TYPE - + 1 -#endif -#if BIT_DEPTH < 8 - , - bit_depth_stretch = ((long double) BIT_DEPTH) / 8.L + + 1 #endif ; - size_t time = 0; +#if BIT_DEPTH != ACTUAL_BIT_DEPTH + const long double bit_depth_stretch = + ((long double) BIT_DEPTH) / ACTUAL_BIT_DEPTH; +#endif + +#if SEQUENTIAL_MODE + size_t time = INITIAL_TIME; + for (size_t seq = 0; seq < MAX; seq++) { if ((time + BLOCK_SIZE) >= GEN_LENGTH) calc_block_size = GEN_LENGTH - time; @@ -260,34 +313,34 @@ main(void) // * bytebeat generating loop #if SEQUENTIAL_MODE - for (size_t idx = 0; idx < BLOCK_SIZE && time < GEN_LENGTH; idx++, + for (size_t idx = 0; idx < BLOCK_SIZE && time < LOOP_END; idx++, time++) { #else - for (size_t time = 0; time < GEN_LENGTH; time++) { + for (size_t time = INITIAL_TIME; time < LOOP_END; time++) { #endif // 1. generate audio data #if FP_RETURN_TYPE - long double bytebeat_res = - floor(bytebeat(floor((long double) time))); + bb_fp_return_t bytebeat_res = + floor(bytebeat(floor((bb_counter_t) time))); #elif IS_SIGNED intmax_t bytebeat_res = - (intmax_t) bytebeat(floor((long double) time)); + (intmax_t) bytebeat(floor((bb_counter_t) time)); #else uintmax_t bytebeat_res = - (uintmax_t) bytebeat(floor((long double) time)); + (uintmax_t) bytebeat(floor((bb_counter_t) time)); #endif - // 2. if signed, then wrap up to unsigned -#if IS_SIGNED + // 2. if signed and bit depth <= 8, then wrap up to unsigned +#if IS_SIGNED && (BIT_DEPTH <= 8) bytebeat_res = signed_to_unsigned(bytebeat_res); #endif // 3. convert audio data to sample SAMPLE_TYPE sample_res = (SAMPLE_TYPE) #if FP_RETURN_TYPE - fmodl(bytebeat_res, bit_depth_limiter); + fmod(bytebeat_res, BIT_DEPTH_LIMITER); #else - ((uintmax_t) bytebeat_res & bit_depth_limiter); + ((uintmax_t) bytebeat_res & BIT_DEPTH_LIMITER); #endif // 4. if bit depth is less than 8, stretch it @@ -297,25 +350,27 @@ main(void) #endif // 5. save sample into buffer + buffer[ #if SEQUENTIAL_MODE - buffer[idx] = sample_res; + idx #else - buffer[time] = sample_res; + time #endif + ] = sample_res; // 6. log #if VERBOSE_MODE if (time % FREQUENCY_OF_STATUS_REPORTING == 0 || - time >= gen_length_minus_1 /* or if writing last sample */) { + time >= LOOP_END_MINUS_1 /* or if writing last sample */) { printf( - "%sremaining samples = %18" PRIuMAX " (%3.2Lf%% done)" + "%sremaining samples = %18" PRIuMAX " (%6.2Lf%% done)" #if SEQUENTIAL_MODE " (part %" PRIuMAX "/%" PRIuMAX ")" #endif , _ANSI_CLEAR_STRING, - gen_length_minus_1 - time, - ((long double) time * 100) / (long double) GEN_LENGTH + LOOP_END_MINUS_1 - time, + ((long double) time * 100) / (long double) LOOP_END_MINUS_1 #if SEQUENTIAL_MODE , (uintmax_t) seq + 1, (uintmax_t) MAX #endif @@ -330,13 +385,13 @@ main(void) // 5. log #if !(SEQUENTIAL_MODE && VERBOSE_MODE) -#if SEQUENTIAL_MODE +# if SEQUENTIAL_MODE if (seq == 0) -#endif +# endif puts( -#if !SEQUENTIAL_MODE +# if !SEQUENTIAL_MODE "\n" -#endif +# endif "Writing out file " OUTPUT_FILE "..."); #endif fflush(stdout); @@ -347,7 +402,7 @@ main(void) #if SEQUENTIAL_MODE calc_block_size, #else - PRODUCT, + GEN_LENGTH, #endif output_file); #if SEQUENTIAL_MODE @@ -358,6 +413,43 @@ main(void) } #endif +#if REPEAT_TIMES > 0 + // * repeat as much as needed + + puts( +# if SEQUENTIAL_MODE + "\n" +# endif + "Repeating..."); + + for (size_t counter = 0; counter < REPEAT_TIMES; counter++) { +# if SEQUENTIAL_MODE + off_t position_read = header_size; + + calc_block_size = BLOCK_SIZE; + for (size_t seq = 0, time = 0; seq < MAX; seq++, time += BLOCK_SIZE) { + bool end = false; + if ((time + BLOCK_SIZE) >= GEN_LENGTH) { + calc_block_size = GEN_LENGTH - time; + end = true; + } + + fseeko(output_file, position_read, SEEK_SET); + fread(buffer, sizeof(SAMPLE_TYPE), calc_block_size, output_file); + fseeko(output_file, 0, SEEK_END); + fwrite(buffer, sizeof(SAMPLE_TYPE), calc_block_size, output_file); + + if (end) + break; + + position_read += calc_block_size; + } +# else + fwrite_le(buffer, sizeof(SAMPLE_TYPE), GEN_LENGTH, output_file); +# endif + } +#endif + // * free allocated heap free(buffer); @@ -367,11 +459,13 @@ main(void) // * end of program #if !SILENT_MODE puts( -#if SEQUENTIAL_MODE && VERBOSE_MODE +# if SEQUENTIAL_MODE && VERBOSE_MODE && REPEAT_TIMES == 0 "\n" -#endif +# endif "Done!"); #endif return EXIT_SUCCESS; } + +#endif /* _ERROR */