From d064ff76e0b4536ee05dd8b2fa1b9428c4d1ec4a Mon Sep 17 00:00:00 2001 From: Paul Melnikov Date: Wed, 6 May 2026 17:00:53 +0200 Subject: [PATCH 1/6] Add wav2mozzi.py --- extras/python/wav2mozzi.py | 188 +++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 extras/python/wav2mozzi.py diff --git a/extras/python/wav2mozzi.py b/extras/python/wav2mozzi.py new file mode 100644 index 000000000..984a81bab --- /dev/null +++ b/extras/python/wav2mozzi.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +##@file wav2mozzi.py +# @ingroup util +# A script for converting .WAV sound files to wavetables for Mozzi. +# +# Usage: +# >>>wav2mozzi.py infile [-t tablename] [-o outfile] +# +# @param infile The .WAV file to convert. +# @param -t tablename (Optional) The name to give the table. Default: uppercase input filename. +# @param -o outfile (Optional) The output .h file. Default: derived from input filename. +# +# Reads bitness, sample format, and sample rate from the WAV header automatically. +# Supports 8-bit unsigned, 16-bit signed, 24-bit signed, and 32-bit signed PCM WAV files, +# as well as 32-bit IEEE float WAV files (samples in -1.0..1.0 range). +# +# All sample data is converted to signed 8-bit (-128..127). +# If audio is stereo, only the first channel is used. +# + +import sys, os, textwrap, struct, random, argparse, re + +def read_wav(infile): + """Read a WAV file, supporting both PCM and IEEE float formats. + Returns (nchannels, sampwidth, samplerate, nframes, raw_bytes, is_float).""" + with open(infile, 'rb') as f: + # Parse RIFF header + riff = f.read(4) + if riff != b'RIFF': + print("Error: not a valid WAV file (missing RIFF header)") + sys.exit(1) + f.read(4) # file size + wave_id = f.read(4) + if wave_id != b'WAVE': + print("Error: not a valid WAV file (missing WAVE identifier)") + sys.exit(1) + + fmt_found = False + data_raw = None + audio_format = None + nchannels = None + samplerate = None + sampwidth = None + nframes = None + + while True: + chunk_header = f.read(8) + if len(chunk_header) < 8: + break + chunk_id = chunk_header[:4] + chunk_size = struct.unpack(' convert to signed -128..127 + val = struct.unpack('B', sample_bytes)[0] - 128 + elif sampwidth == 2: + # 16-bit little-endian signed + val = struct.unpack('= 0x800000: + val -= 0x1000000 + elif sampwidth == 4: + val = struct.unpack(' int8: divide by 256 + store_values = [max(-128, min(127, int(round(v / 256.0)))) for v in values] + elif sampwidth == 3: + # 24-bit -> int8: divide by 65536 + store_values = [max(-128, min(127, int(round(v / 65536.0)))) for v in values] + elif sampwidth == 4: + # 32-bit -> int8: divide by 16777216 + store_values = [max(-128, min(127, int(round(v / 16777216.0)))) for v in values] + else: + print("Unsupported sample width: %d bytes" % sampwidth) + sys.exit(1) + + # Dither triple-33 sequences (taken from char2mozzi.py) + for i in range(len(store_values) - 2): + if store_values[i] == 33 and store_values[i+1] == 33 and store_values[i+2] == 33: + store_values[i+2] = random.choice([32, 34]) + + fout = open(os.path.expanduser(outfile), "w") + fout.write('#ifndef ' + tablename + '_H_\n') + fout.write('#define ' + tablename + '_H_\n\n') + fout.write('#include \n') + fout.write('#include "mozzi_pgmspace.h"\n\n') + fout.write('#define ' + tablename + '_NUM_CELLS ' + str(len(store_values)) + '\n') + fout.write('#define ' + tablename + '_SAMPLERATE ' + str(samplerate) + '\n\n') + fout.write('CONSTTABLE_STORAGE(' + c_type + ') ' + tablename + '_DATA [] = {\n') + outstring = '' + for v in store_values: + outstring += str(v) + ", " + outstring = textwrap.fill(outstring, 80) + fout.write(outstring) + fout.write('\n};\n') + fout.write('\n#endif /* ' + tablename + '_H_ */\n') + fout.close() + print("wrote " + outfile) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Convert a .WAV file to a Mozzi wavetable header.') + parser.add_argument('infile', help='Input .WAV file') + parser.add_argument('-t', '--tablename', help='Table name for the generated header (default: uppercase input filename)') + parser.add_argument('-o', '--outfile', help='Output .h file (default: derived from input filename)') + args = parser.parse_args() + + infile = os.path.expanduser(args.infile) + if args.tablename: + tablename = args.tablename + else: + # derive from filename: strip extension, keep only alnum/underscore, uppercase + tablename = re.sub(r'[^A-Za-z0-9_]', '_', os.path.splitext(os.path.basename(infile))[0]).upper() + outfile = args.outfile if args.outfile else os.path.splitext(infile)[0] + '.h' + + wav2mozzi(infile, outfile, tablename) From 6cda4aba8eafc43ffcd547e6a2f44cbd95faee75 Mon Sep 17 00:00:00 2001 From: Paul Melnikov Date: Wed, 6 May 2026 17:00:53 +0200 Subject: [PATCH 2/6] Minor renaming and output formatting --- extras/python/wav2mozzi.py | 72 ++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/extras/python/wav2mozzi.py b/extras/python/wav2mozzi.py index 984a81bab..1ec8142fe 100644 --- a/extras/python/wav2mozzi.py +++ b/extras/python/wav2mozzi.py @@ -23,7 +23,7 @@ def read_wav(infile): """Read a WAV file, supporting both PCM and IEEE float formats. - Returns (nchannels, sampwidth, samplerate, nframes, raw_bytes, is_float).""" + Returns (nchannels, sample_size, samplerate, nframes, raw_bytes, is_float).""" with open(infile, 'rb') as f: # Parse RIFF header riff = f.read(4) @@ -39,10 +39,10 @@ def read_wav(infile): fmt_found = False data_raw = None audio_format = None - nchannels = None - samplerate = None - sampwidth = None - nframes = None + n_channels = None + sample_rate = None + sample_size = None + n_frames = None while True: chunk_header = f.read(8) @@ -54,14 +54,14 @@ def read_wav(infile): if chunk_id == b'fmt ': fmt_data = f.read(chunk_size) audio_format = struct.unpack(' convert to signed -128..127 val = struct.unpack('B', sample_bytes)[0] - 128 - elif sampwidth == 2: + elif sample_size == 2: # 16-bit little-endian signed val = struct.unpack('= 0x800000: val -= 0x1000000 - elif sampwidth == 4: + elif sample_size == 4: val = struct.unpack(' int8: divide by 256 store_values = [max(-128, min(127, int(round(v / 256.0)))) for v in values] - elif sampwidth == 3: + elif sample_size == 3: # 24-bit -> int8: divide by 65536 store_values = [max(-128, min(127, int(round(v / 65536.0)))) for v in values] - elif sampwidth == 4: + elif sample_size == 4: # 32-bit -> int8: divide by 16777216 store_values = [max(-128, min(127, int(round(v / 16777216.0)))) for v in values] else: - print("Unsupported sample width: %d bytes" % sampwidth) + print("Unsupported sample size: %d bytes" % sample_size) sys.exit(1) # Dither triple-33 sequences (taken from char2mozzi.py) @@ -157,12 +159,12 @@ def wav2mozzi(infile, outfile, tablename): fout.write('#include \n') fout.write('#include "mozzi_pgmspace.h"\n\n') fout.write('#define ' + tablename + '_NUM_CELLS ' + str(len(store_values)) + '\n') - fout.write('#define ' + tablename + '_SAMPLERATE ' + str(samplerate) + '\n\n') + fout.write('#define ' + tablename + '_SAMPLERATE ' + str(sample_rate) + '\n\n') fout.write('CONSTTABLE_STORAGE(' + c_type + ') ' + tablename + '_DATA [] = {\n') outstring = '' for v in store_values: outstring += str(v) + ", " - outstring = textwrap.fill(outstring, 80) + outstring = textwrap.fill(outstring, width=80, initial_indent=' ', subsequent_indent=' ') fout.write(outstring) fout.write('\n};\n') fout.write('\n#endif /* ' + tablename + '_H_ */\n') From 182ca651004bb5d3b1d58854f821fce2eeebca2f Mon Sep 17 00:00:00 2001 From: Paul Melnikov Date: Wed, 6 May 2026 17:00:53 +0200 Subject: [PATCH 3/6] Add symmetric output and 8/16b --- extras/python/wav2mozzi.py | 122 ++++++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 43 deletions(-) diff --git a/extras/python/wav2mozzi.py b/extras/python/wav2mozzi.py index 1ec8142fe..f0f74730f 100644 --- a/extras/python/wav2mozzi.py +++ b/extras/python/wav2mozzi.py @@ -5,15 +5,20 @@ # A script for converting .WAV sound files to wavetables for Mozzi. # # Usage: -# >>>wav2mozzi.py infile [-t tablename] [-o outfile] +# >>>wav2mozzi.py infile [-t tablename] [-o outfile] [--output-bits {8,16}] [--no-symmetric-output] # -# @param infile The .WAV file to convert. -# @param -t tablename (Optional) The name to give the table. Default: uppercase input filename. -# @param -o outfile (Optional) The output .h file. Default: derived from input filename. +# Arguments: +# * infile The .WAV file to convert. +# * -t tablename (Optional) The name to give the table. Default: uppercase input filename. +# * -o outfile (Optional) The output .h file. Default: derived from input filename. +# * -b, --output-bits +# (Optional) Output sample size in bits. Allowed: 8 or 16. Default: 8. +# * --symmetric-output, --no-symmetric-output +# (Optional) Generate symmetric signed output range. Default: enabled. # # Reads bitness, sample format, and sample rate from the WAV header automatically. # Supports 8-bit unsigned, 16-bit signed, 24-bit signed, and 32-bit signed PCM WAV files, -# as well as 32-bit IEEE float WAV files (samples in -1.0..1.0 range). +# as well as 32-bit IEEE float WAV files (samples in -1.0..1.0 range). # # All sample data is converted to signed 8-bit (-128..127). # If audio is stereo, only the first channel is used. @@ -80,21 +85,55 @@ def read_wav(infile): return n_channels, sample_size, sample_rate, n_frames, data_raw, is_float -def wav2mozzi(infile, outfile, tablename): + +def wav2mozzi(infile, outfile, tablename, output_bytes=1, symmetric_output=True): """ Convert a WAV file to a Mozzi wavetable header. - infile: input WAV file path - outfile: output .h file path - tablename: name to use for the generated variables (e.g. "MYTABLE") + - output_bytes: number of bytes for output samples (1 or 2 supported) + - symmetric_output: if True, negative range is the same as positive (e.g. -128 becomes -127 for 1 byte) """ n_channels, sample_size, sample_rate, n_frames, data_bytes, is_float = read_wav(infile) print("opened " + infile) print(" channels: %d, rate: %d Hz, sample size: %dbit, samples: %d, format: %s" % (n_channels, sample_rate, sample_size * 8, n_frames, "float" if is_float else "PCM")) + # for clarity, convert input to -1.0...1.0 first + + input_midpoint = 0.0 + input_max = 1.0 + + if is_float: + if sample_size != 4: + print("Unsupported float sample size: %d B" % sample_size) + sys.exit(1) + else: + if sample_size == 1: + # 8-bit WAV is unsigned 0..255 + # by definition, mid point is 128 + input_midpoint = 128 + input_max = 255 + elif sample_size == 2: + # 16-bit little-endian signed + input_midpoint = 0 + input_max = 2**15 - 1 + elif sample_size == 3: + # 24-bit little-endian signed + input_midpoint = 0 + input_max = 2**23 - 1 + elif sample_size == 4: + # 32-bit little-endian signed + input_midpoint = 0 + input_max = 2**31 - 1 + else: + print("Unsupported sample size: %d B" % sample_size) + sys.exit(1) + # Decode raw bytes into samples (mono only - take first channel) values = [] - is_float_data = is_float + frame_size = n_channels * sample_size for i in range(n_frames): offset = i * frame_size @@ -102,13 +141,10 @@ def wav2mozzi(infile, outfile, tablename): if is_float: if sample_size == 4: val = struct.unpack(' convert to signed -128..127 - val = struct.unpack('B', sample_bytes)[0] - 128 + # 8-bit WAV is unsigned 0..255 + val = struct.unpack('B', sample_bytes)[0] elif sample_size == 2: # 16-bit little-endian signed val = struct.unpack('= 0x800000: + if val >= 0x800000: # convert to signed val -= 0x1000000 elif sample_size == 4: val = struct.unpack(' int8: divide by 256 - store_values = [max(-128, min(127, int(round(v / 256.0)))) for v in values] - elif sample_size == 3: - # 24-bit -> int8: divide by 65536 - store_values = [max(-128, min(127, int(round(v / 65536.0)))) for v in values] - elif sample_size == 4: - # 32-bit -> int8: divide by 16777216 - store_values = [max(-128, min(127, int(round(v / 16777216.0)))) for v in values] + # Scale to -1.0...1.0 range + if is_float: + scaled_values = values # already in -1.0...1.0 range else: - print("Unsupported sample size: %d bytes" % sample_size) - sys.exit(1) + input_max = input_max - input_midpoint # for uint8, max should be is 127 + scaled_values = [(v - input_midpoint) / input_max for v in values] + + # Convert to signed 8-bit or 16-bit range for output + c_type = 'int8_t' if output_bytes == 1 else 'int16_t' + out_range = [-128, 127] if output_bytes == 1 else [-2**15, 2**15-1] + if symmetric_output: + out_range[0] += 1 # e.g. -128 becomes -127 for symmetric range + + out_values = [ + max(out_range[0], min(out_range[1], int(v * out_range[1]))) + for v in scaled_values + ] # Dither triple-33 sequences (taken from char2mozzi.py) - for i in range(len(store_values) - 2): - if store_values[i] == 33 and store_values[i+1] == 33 and store_values[i+2] == 33: - store_values[i+2] = random.choice([32, 34]) + for i in range(len(out_values) - 2): + if out_values[i] == 33 and out_values[i+1] == 33 and out_values[i+2] == 33: + out_values[i+2] = random.choice([32, 34]) fout = open(os.path.expanduser(outfile), "w") fout.write('#ifndef ' + tablename + '_H_\n') fout.write('#define ' + tablename + '_H_\n\n') fout.write('#include \n') fout.write('#include "mozzi_pgmspace.h"\n\n') - fout.write('#define ' + tablename + '_NUM_CELLS ' + str(len(store_values)) + '\n') + fout.write('// Arguments: "' + ' '.join(sys.argv[1:]) + '"\n\n') + fout.write('#define ' + tablename + '_NUM_CELLS ' + str(len(out_values)) + '\n') fout.write('#define ' + tablename + '_SAMPLERATE ' + str(sample_rate) + '\n\n') fout.write('CONSTTABLE_STORAGE(' + c_type + ') ' + tablename + '_DATA [] = {\n') outstring = '' - for v in store_values: + for v in out_values: outstring += str(v) + ", " outstring = textwrap.fill(outstring, width=80, initial_indent=' ', subsequent_indent=' ') fout.write(outstring) @@ -170,6 +199,7 @@ def wav2mozzi(infile, outfile, tablename): fout.write('\n#endif /* ' + tablename + '_H_ */\n') fout.close() print("wrote " + outfile) + print(" table name: " + tablename + ", type: " + c_type + (", symmetric" if symmetric_output else "")) if __name__ == "__main__": @@ -177,6 +207,10 @@ def wav2mozzi(infile, outfile, tablename): parser.add_argument('infile', help='Input .WAV file') parser.add_argument('-t', '--tablename', help='Table name for the generated header (default: uppercase input filename)') parser.add_argument('-o', '--outfile', help='Output .h file (default: derived from input filename)') + parser.add_argument('-b', '--output-bits', type=int, choices=(8, 16), default=8, + help='Output sample size in bits (default: 8)') + parser.add_argument('-s', '--symmetric-output', action=argparse.BooleanOptionalAction, default=True, + help='Generate a symmetric signed output range (default: enabled)') args = parser.parse_args() infile = os.path.expanduser(args.infile) @@ -187,4 +221,6 @@ def wav2mozzi(infile, outfile, tablename): tablename = re.sub(r'[^A-Za-z0-9_]', '_', os.path.splitext(os.path.basename(infile))[0]).upper() outfile = args.outfile if args.outfile else os.path.splitext(infile)[0] + '.h' - wav2mozzi(infile, outfile, tablename) + wav2mozzi(infile, outfile, tablename, + output_bytes=args.output_bits // 8, + symmetric_output=args.symmetric_output) From 566427ccad8cdbc9ec7455bb0b30894a44d2ce89 Mon Sep 17 00:00:00 2001 From: Paul Melnikov Date: Wed, 6 May 2026 17:00:53 +0200 Subject: [PATCH 4/6] Use Exceptions instead of sys.exit --- extras/python/wav2mozzi.py | 79 +++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/extras/python/wav2mozzi.py b/extras/python/wav2mozzi.py index f0f74730f..9c287deef 100644 --- a/extras/python/wav2mozzi.py +++ b/extras/python/wav2mozzi.py @@ -23,6 +23,8 @@ # All sample data is converted to signed 8-bit (-128..127). # If audio is stereo, only the first channel is used. # +# Requires Python 3.9+, no dependencies. +# import sys, os, textwrap, struct, random, argparse, re @@ -33,13 +35,11 @@ def read_wav(infile): # Parse RIFF header riff = f.read(4) if riff != b'RIFF': - print("Error: not a valid WAV file (missing RIFF header)") - sys.exit(1) + raise IOError("Not a valid WAV file (missing RIFF header)") f.read(4) # file size wave_id = f.read(4) if wave_id != b'WAVE': - print("Error: not a valid WAV file (missing WAVE identifier)") - sys.exit(1) + raise IOError("Not a valid WAV file (missing WAVE identifier)") fmt_found = False data_raw = None @@ -74,14 +74,12 @@ def read_wav(infile): f.read(1) # padding byte if not fmt_found or data_raw is None: - print("Error: could not find fmt/data chunks in WAV file") - sys.exit(1) + raise IOError("Could not find fmt/data chunks in WAV file") # audio_format: 1 = PCM, 3 = IEEE float is_float = (audio_format == 3) if audio_format not in (1, 3): - print("Error: unsupported WAV format code %d (only PCM=1 and IEEE float=3 are supported)" % audio_format) - sys.exit(1) + raise ValueError(f"Unsupported WAV format code {audio_format} (only PCM=1 and IEEE float=3 are supported)") return n_channels, sample_size, sample_rate, n_frames, data_raw, is_float @@ -96,9 +94,9 @@ def wav2mozzi(infile, outfile, tablename, output_bytes=1, symmetric_output=True) - symmetric_output: if True, negative range is the same as positive (e.g. -128 becomes -127 for 1 byte) """ n_channels, sample_size, sample_rate, n_frames, data_bytes, is_float = read_wav(infile) - print("opened " + infile) - print(" channels: %d, rate: %d Hz, sample size: %dbit, samples: %d, format: %s" % - (n_channels, sample_rate, sample_size * 8, n_frames, "float" if is_float else "PCM")) + print("opened", infile) + format_str = "float" if is_float else "PCM" + print(f" channels: {n_channels}, rate: {sample_rate} Hz, sample size: {sample_size * 8}bit, samples: {n_frames}, format: {format_str}") # for clarity, convert input to -1.0...1.0 first @@ -107,8 +105,7 @@ def wav2mozzi(infile, outfile, tablename, output_bytes=1, symmetric_output=True) if is_float: if sample_size != 4: - print("Unsupported float sample size: %d B" % sample_size) - sys.exit(1) + raise ValueError(f"Unsupported float sample size: {sample_size} B") else: if sample_size == 1: # 8-bit WAV is unsigned 0..255 @@ -128,8 +125,7 @@ def wav2mozzi(infile, outfile, tablename, output_bytes=1, symmetric_output=True) input_midpoint = 0 input_max = 2**31 - 1 else: - print("Unsupported sample size: %d B" % sample_size) - sys.exit(1) + raise ValueError(f"Unsupported sample size: {sample_size} B") # Decode raw bytes into samples (mono only - take first channel) values = [] @@ -181,25 +177,27 @@ def wav2mozzi(infile, outfile, tablename, output_bytes=1, symmetric_output=True) if out_values[i] == 33 and out_values[i+1] == 33 and out_values[i+2] == 33: out_values[i+2] = random.choice([32, 34]) - fout = open(os.path.expanduser(outfile), "w") - fout.write('#ifndef ' + tablename + '_H_\n') - fout.write('#define ' + tablename + '_H_\n\n') - fout.write('#include \n') - fout.write('#include "mozzi_pgmspace.h"\n\n') - fout.write('// Arguments: "' + ' '.join(sys.argv[1:]) + '"\n\n') - fout.write('#define ' + tablename + '_NUM_CELLS ' + str(len(out_values)) + '\n') - fout.write('#define ' + tablename + '_SAMPLERATE ' + str(sample_rate) + '\n\n') - fout.write('CONSTTABLE_STORAGE(' + c_type + ') ' + tablename + '_DATA [] = {\n') - outstring = '' - for v in out_values: - outstring += str(v) + ", " - outstring = textwrap.fill(outstring, width=80, initial_indent=' ', subsequent_indent=' ') - fout.write(outstring) - fout.write('\n};\n') - fout.write('\n#endif /* ' + tablename + '_H_ */\n') - fout.close() - print("wrote " + outfile) - print(" table name: " + tablename + ", type: " + c_type + (", symmetric" if symmetric_output else "")) + with open(outfile, "w") as fout: + fout.write(f'#ifndef {tablename}_H_\n') + fout.write(f'#define {tablename}_H_\n\n') + fout.write('// Generated by wav2mozzi.py"\n') + fout.write('// Arguments: "' + ' '.join(sys.argv[1:]) + '"\n\n') + fout.write('#include \n') + fout.write('#include "mozzi_pgmspace.h"\n\n') + fout.write(f'#define {tablename}_NUM_CELLS {len(out_values)}\n') + fout.write(f'#define {tablename}_SAMPLERATE {sample_rate}\n\n') + fout.write(f'CONSTTABLE_STORAGE({c_type}) {tablename}_DATA [] = {{\n') + outstring = '' + for v in out_values: + outstring += str(v) + ", " + outstring = textwrap.fill(outstring, width=80, initial_indent=' ', subsequent_indent=' ') + fout.write(outstring) + fout.write('\n};\n') + fout.write(f'\n#endif /* {tablename}_H_ */\n') + + print("wrote", outfile) + sym_str = ", symmetric" if symmetric_output else "" + print(f" table name: {tablename}, type: {c_type}{sym_str}") if __name__ == "__main__": @@ -219,8 +217,11 @@ def wav2mozzi(infile, outfile, tablename, output_bytes=1, symmetric_output=True) else: # derive from filename: strip extension, keep only alnum/underscore, uppercase tablename = re.sub(r'[^A-Za-z0-9_]', '_', os.path.splitext(os.path.basename(infile))[0]).upper() - outfile = args.outfile if args.outfile else os.path.splitext(infile)[0] + '.h' - - wav2mozzi(infile, outfile, tablename, - output_bytes=args.output_bits // 8, - symmetric_output=args.symmetric_output) + outfile = os.path.expanduser(args.outfile) if args.outfile else os.path.splitext(infile)[0] + '.h' + + try: + wav2mozzi(infile, outfile, tablename, + output_bytes=args.output_bits // 8, + symmetric_output=args.symmetric_output) + except (IOError, ValueError) as e: + print(f"Error: {e}", file=sys.stderr) From 04c828d5ed95f33123c9bf08a3a65fb4ad9b086c Mon Sep 17 00:00:00 2001 From: Paul Melnikov Date: Fri, 8 May 2026 17:09:59 +0000 Subject: [PATCH 5/6] Fix python3 shebang and make file executable on linux --- extras/python/wav2mozzi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 extras/python/wav2mozzi.py diff --git a/extras/python/wav2mozzi.py b/extras/python/wav2mozzi.py old mode 100644 new mode 100755 index 9c287deef..f7845b4c8 --- a/extras/python/wav2mozzi.py +++ b/extras/python/wav2mozzi.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 ##@file wav2mozzi.py # @ingroup util From 46fcad7d312a90c2d6032ea7277c0bd13554e7fe Mon Sep 17 00:00:00 2001 From: Paul Melnikov Date: Fri, 8 May 2026 20:38:10 +0000 Subject: [PATCH 6/6] Rework documentation, copy bits from char2mozzi --- extras/python/wav2mozzi.py | 76 ++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/extras/python/wav2mozzi.py b/extras/python/wav2mozzi.py index f7845b4c8..c95c2f2d5 100755 --- a/extras/python/wav2mozzi.py +++ b/extras/python/wav2mozzi.py @@ -1,36 +1,62 @@ #!/usr/bin/env python3 -##@file wav2mozzi.py -# @ingroup util -# A script for converting .WAV sound files to wavetables for Mozzi. -# -# Usage: -# >>>wav2mozzi.py infile [-t tablename] [-o outfile] [--output-bits {8,16}] [--no-symmetric-output] -# +""" +A script for converting .WAV files to wavetables for Mozzi. + +Reads bitness, sample format, and sample rate from the WAV header automatically. +Supports 8-bit unsigned, 16-bit signed, 24-bit signed, and 32-bit signed PCM WAV files, +as well as 32-bit IEEE float WAV files (samples in -1.0..1.0 range). + +All sample data is converted to signed 8-bit or 16-bit values. +Output values are centered around 0 and can be negated without overflow. +If audio file is stereo, only the first channel is used. + +Requires Python 3.9+, no dependencies. + +NOTE: Using Audacity to prepare sound files: + +For generated waveforms like sine or sawtooth, set the project +rate to the size of the wavetable you wish to create, which must +be a power of two (eg. 8192), and set the selection format +(beneath the editing window) to samples. Then you can generate +and save 1 second of a waveform and it will fit your table +length. + +For a recorded audio sample, set the project rate to the +MOZZI_AUDIO_RATE (16384 in the current version). +Samples can be any length, as long as they fit in your Arduino. + +Save the file by "Export" -> "Export as WAV". +To keep all the details, choose "32-bit float" encoding. +Other supported encodings are 8,16,24,32-bit PCM. + +Now use the file you just exported, as the "infile" to convert. + + +Author: Paul Melnikov, 2026-04 +""" + +# Usage: +# >>>wav2mozzi.py infile [-t tablename] [-o outfile] [-b {8,16}] [--no-symmetric-output] # Arguments: -# * infile The .WAV file to convert. +# * infile The .WAV file to convert. # * -t tablename (Optional) The name to give the table. Default: uppercase input filename. -# * -o outfile (Optional) The output .h file. Default: derived from input filename. -# * -b, --output-bits +# * -o outfile (Optional) The output .h file. Default: derived from input filename. +# * -b, --output-bits # (Optional) Output sample size in bits. Allowed: 8 or 16. Default: 8. -# * --symmetric-output, --no-symmetric-output +# * --symmetric-output, --no-symmetric-output # (Optional) Generate symmetric signed output range. Default: enabled. -# -# Reads bitness, sample format, and sample rate from the WAV header automatically. -# Supports 8-bit unsigned, 16-bit signed, 24-bit signed, and 32-bit signed PCM WAV files, -# as well as 32-bit IEEE float WAV files (samples in -1.0..1.0 range). -# -# All sample data is converted to signed 8-bit (-128..127). -# If audio is stereo, only the first channel is used. -# -# Requires Python 3.9+, no dependencies. -# + import sys, os, textwrap, struct, random, argparse, re def read_wav(infile): """Read a WAV file, supporting both PCM and IEEE float formats. - Returns (nchannels, sample_size, samplerate, nframes, raw_bytes, is_float).""" + Returns (nchannels, sample_size, samplerate, nframes, raw_bytes, is_float). + + Technically, python has built-in "wave" library for this, + but it doesn't support IEEE floats, so decode headers manually. + """ with open(infile, 'rb') as f: # Parse RIFF header riff = f.read(4) @@ -201,14 +227,16 @@ def wav2mozzi(infile, outfile, tablename, output_bytes=1, symmetric_output=True) if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Convert a .WAV file to a Mozzi wavetable header.') + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('infile', help='Input .WAV file') parser.add_argument('-t', '--tablename', help='Table name for the generated header (default: uppercase input filename)') parser.add_argument('-o', '--outfile', help='Output .h file (default: derived from input filename)') parser.add_argument('-b', '--output-bits', type=int, choices=(8, 16), default=8, help='Output sample size in bits (default: 8)') parser.add_argument('-s', '--symmetric-output', action=argparse.BooleanOptionalAction, default=True, - help='Generate a symmetric signed output range (default: enabled)') + help='Generate symmetric signed output range (default: enabled)') args = parser.parse_args() infile = os.path.expanduser(args.infile)