Skip to content

Commit 1e42a3d

Browse files
committed
Add new audio2mozzy.py
The script allows to create mozzi tables from raw files and it's supposed to replace all the "*2mozzy.py" scripts thanks to its high configurability (it also supports int16/int32 tables in case they'll be supported in the future)
1 parent 091c319 commit 1e42a3d

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed

extras/python/audio2mozzi.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#!/usr/bin/env python3
2+
"""
3+
audio2mozzi.py
4+
5+
This file is part of Mozzi.
6+
7+
Copyright 2024 Leonardo Bellettini and the Mozzi Team.
8+
9+
Mozzi is licensed under the GNU Lesser General Public Licence (LGPL) Version 2.1 or later.
10+
11+
Script for converting raw audio files to Mozzi table
12+
13+
To generate waveforms using Audacity:
14+
Set the project rate to the size of the wavetable you wish to create, which must
15+
be a power of two (eg. 8192), and set the selection format
16+
(beneath the editing window) to samples. Then you can generate
17+
and save 1 second of a waveform and it will fit your table
18+
length.
19+
20+
To convert samples using Audacity:
21+
For a recorded audio sample, set the project rate to the
22+
MOZZI_AUDIO_RATE (16384 in the current version).
23+
Samples can be any length, as long as they fit in your Arduino.
24+
Save by exporting with the format set to "Other uncompressed formats",
25+
"Header: RAW(headerless)" and choose the encoding you prefer (Signed 8/16/32-bit PCM or 32-bit Float).
26+
27+
Alternative to convert samples via CLI using sox (http://sox.sourceforge.net/):
28+
sox <inputfile> -b <8/16/32> -c 1 -e <floating-point/signed-integer> -r 16384 <outputfile>
29+
30+
Now use the file you just exported, as the "input_file" to convert and
31+
set the other parameters according to what you chose for conversion.
32+
"""
33+
34+
import array
35+
import logging
36+
import random
37+
import sys
38+
import textwrap
39+
from argparse import ArgumentParser
40+
from pathlib import Path
41+
42+
import numpy as np
43+
44+
logger = logging.getLogger(__name__)
45+
46+
def map_value(value, in_min, in_max, out_min, out_max):
47+
return ((value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
48+
49+
def replace_three_33(arr):
50+
"""Mega2560 boards won't upload if there is 33, 33, 33 in the array,
51+
so dither the 3rd 33 if there is one
52+
"""
53+
for i in range(len(arr) - 2): # Ensure we don't go out of bounds
54+
# Check for three consecutive "33"
55+
if arr[i] == 33 and arr[i + 1] == 33 and arr[i + 2] == 33:
56+
arr[i] = random.choice((32, 34)) # Replace the first "33" in the group
57+
return arr
58+
59+
def float2mozzi(args):
60+
input_path = args.input_file.expanduser()
61+
output_path = (
62+
args.output_file.expanduser()
63+
if args.output_file is not None
64+
else input_path.with_suffix(".h")
65+
)
66+
67+
with input_path.open("rb") as fin, output_path.open("w") as fout:
68+
logger.debug("Opened %s", input_path)
69+
num_input_values = int(
70+
input_path.stat().st_size / (args.input_bits / 8),
71+
) # Adjust for number format (table at top of https://docs.python.org/3/library/array.html)
72+
73+
array_type = ""
74+
input_dtype = None
75+
if args.input_bits == 8:
76+
array_type = "b"
77+
input_dtype = np.int8
78+
elif args.input_bits == 16:
79+
array_type = "h"
80+
input_dtype = np.int16 if args.input_encoding == "int" else np.float16
81+
elif args.input_bits == 32:
82+
input_dtype = np.int32 if args.input_encoding == "int" else np.float32
83+
array_type = "f" if args.input_encoding == "float" else "i"
84+
else:
85+
raise Exception("Unrecognised --input-bits value")
86+
87+
valuesfromfile = array.array(array_type)
88+
try:
89+
valuesfromfile.fromfile(fin, num_input_values)
90+
except EOFError:
91+
pass
92+
in_values = valuesfromfile.tolist()
93+
input_array = np.array(in_values, dtype=input_dtype)
94+
95+
tablename = (
96+
args.table_name
97+
if args.table_name is not None
98+
else output_path.stem.replace("-", "_").upper()
99+
)
100+
101+
fout.write(f"#ifndef {tablename}_H_\n")
102+
fout.write(f"#define {tablename}_H_\n\n")
103+
fout.write("#include <Arduino.h>\n")
104+
fout.write('#include "mozzi_pgmspace.h"\n\n')
105+
fout.write(f"#define {tablename}_NUM_CELLS {len(in_values)}\n")
106+
fout.write(f"#define {tablename}_SAMPLERATE {args.sample_rate}\n\n")
107+
108+
table = f"CONSTTABLE_STORAGE(int{args.output_bits}_t) {tablename}_DATA [] = {{"
109+
110+
output_dtype = None
111+
if args.output_bits == 8:
112+
output_dtype = np.int8
113+
elif args.output_bits == 16:
114+
output_dtype = np.int16
115+
elif args.output_bits == 32:
116+
output_dtype = np.int32
117+
else:
118+
raise Exception("Unrecognised --input-bits value")
119+
120+
output_array = input_array.astype(output_dtype)
121+
replace_three_33(output_array)
122+
if args.make_symmetrical:
123+
min_value_to_normalize = np.iinfo(output_dtype).min
124+
max_value_to_normalize = np.iinfo(output_dtype).max
125+
min_final_value = np.iinfo(output_dtype).min + 1
126+
max_final_value = max_value_to_normalize
127+
if min_value_to_normalize in output_array:
128+
output_array = (
129+
map_value(
130+
value,
131+
min_value_to_normalize,
132+
max_value_to_normalize,
133+
min_final_value,
134+
max_final_value,
135+
)
136+
for value in output_array
137+
)
138+
139+
table += ", ".join(map(str, output_array))
140+
table += "};"
141+
table = textwrap.fill(table, 80)
142+
fout.write(table)
143+
fout.write("\n\n")
144+
fout.write(f"#endif /* {tablename}_H_ */\n")
145+
logger.debug("Wrote %s to %s", table, output_path)
146+
147+
return 0
148+
149+
150+
if __name__ == "__main__":
151+
parser = ArgumentParser(
152+
description="Script for converting a raw audio file to a Mozzi table",
153+
)
154+
parser.add_argument(
155+
"-e",
156+
"--input-encoding",
157+
choices=("float", "int"),
158+
default="int",
159+
help="Input encoding",
160+
)
161+
parser.add_argument(
162+
"--input-bits",
163+
type=int,
164+
choices=(8, 16, 32),
165+
default=8,
166+
help="Number of bits for the INPUT encoding",
167+
)
168+
parser.add_argument(
169+
"--output-bits",
170+
type=int,
171+
choices=(8, 16, 32),
172+
default=8,
173+
help="Number of bits for each element of the OUTPUT table",
174+
)
175+
parser.add_argument("input_file", type=Path, help="Path to the input file")
176+
parser.add_argument(
177+
"-o",
178+
"--output-file",
179+
type=Path,
180+
help="Path to the output file. It will be input_file.h if not provided",
181+
)
182+
parser.add_argument(
183+
"-t",
184+
"--table-name",
185+
type=str,
186+
help="Name of the output table. If not provided, the name of the output will be used",
187+
)
188+
parser.add_argument(
189+
"-s",
190+
"--sample-rate",
191+
type=int,
192+
default=16384,
193+
help="Sample rate. Value of 16384 recommended",
194+
)
195+
parser.add_argument(
196+
"--make-symmetrical",
197+
action="store_true",
198+
help="Normalize the output between the range +/- max",
199+
)
200+
parser.add_argument(
201+
"-v",
202+
"--verbose",
203+
action="store_true",
204+
help="Increase verbosity",
205+
)
206+
207+
args_ = parser.parse_args()
208+
if args_.verbose:
209+
logging.basicConfig(level=logging.DEBUG)
210+
211+
sys.exit(float2mozzi(args_))

0 commit comments

Comments
 (0)