Skip to content

Commit 2c93c47

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 2c93c47

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed

extras/python/audio2mozzi.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
"""
3+
For converting raw audio files to Mozzi table
4+
5+
To generate waveforms using Audacity:
6+
Set the project rate to the size of the wavetable you wish to create, which must
7+
be a power of two (eg. 8192), and set the selection format
8+
(beneath the editing window) to samples. Then you can generate
9+
and save 1 second of a waveform and it will fit your table
10+
length.
11+
12+
To convert samples using Audacity:
13+
For a recorded audio sample, set the project rate to the
14+
MOZZI_AUDIO_RATE (16384 in the current version).
15+
Samples can be any length, as long as they fit in your Arduino.
16+
Save by exporting with the format set to "Other uncompressed formats",
17+
"Header: RAW(headerless)" and choose the encoding you prefer (Signed 8/16/32-bit PCM or 32-bit Float).
18+
19+
Alternative to convert samples via CLI using sox (http://sox.sourceforge.net/):
20+
sox <inputfile> -b <8/16/32> -c 1 -e <floating-point/signed-integer> -r 16384 <outputfile>
21+
22+
Now use the file you just exported, as the "input_file" to convert and
23+
set the other parameters according to what you chose for conversion.
24+
"""
25+
26+
import array
27+
import math
28+
import random
29+
import sys
30+
import textwrap
31+
from argparse import ArgumentParser
32+
from pathlib import Path
33+
34+
35+
def float2mozzi(args):
36+
input_path = args.input_file.expanduser()
37+
output_path = (
38+
args.output_file.expanduser()
39+
if args.output_file is not None
40+
else input_path.with_suffix(".h")
41+
)
42+
43+
with input_path.open("rb") as fin, output_path.open("w") as fout:
44+
print(f"opened {input_path}")
45+
num_input_values = int(
46+
input_path.stat().st_size / (args.input_bits / 8)
47+
) # Adjust for number format (table at top of https://docs.python.org/3/library/array.html)
48+
49+
array_type = ""
50+
if args.input_bits == 8:
51+
array_type = "b"
52+
elif args.input_bits == 16:
53+
array_type = "h"
54+
elif args.input_bits == 32:
55+
array_type = "f" if args.input_encoding == "float" else "i"
56+
57+
valuesfromfile = array.array(array_type)
58+
try:
59+
valuesfromfile.fromfile(fin, num_input_values)
60+
except EOFError:
61+
pass
62+
in_values = valuesfromfile.tolist()
63+
64+
tablename = (
65+
args.table_name
66+
if args.table_name is not None
67+
else output_path.stem.replace("-", "_").upper()
68+
)
69+
70+
fout.write(f"#ifndef {tablename}_H_\n")
71+
fout.write(f"#define {tablename}_H_\n\n")
72+
fout.write("#include <Arduino.h>\n")
73+
fout.write('#include "mozzi_pgmspace.h"\n\n')
74+
fout.write(f"#define {tablename}_NUM_CELLS {len(in_values)}\n")
75+
fout.write(f"#define {tablename}_SAMPLERATE {args.sample_rate}\n\n")
76+
77+
table = f"CONSTTABLE_STORAGE(int{args.output_bits}_t) {tablename}_DATA [] = {{"
78+
max_output_value = 2 << (args.output_bits - 2) # Halved because signed
79+
out_values = []
80+
for value in in_values:
81+
cnt_33 = 0
82+
out_values.append(
83+
math.trunc((value * max_output_value) + 0.5)
84+
if args.input_encoding == "float"
85+
else value
86+
)
87+
# Mega2560 boards won't upload if there is 33, 33, 33 in the array, so dither the 3rd 33 if there is one
88+
if value == 33:
89+
cnt_33 += 1
90+
if cnt_33 == 3:
91+
out_values.append(random.choice((32, 34)))
92+
cnt_33 = 0
93+
else:
94+
cnt_33 = 0
95+
if args.make_symmetrical:
96+
value_to_remove = -max_output_value - 1
97+
if value_to_remove in out_values:
98+
in_min = value_to_remove
99+
in_max = max_output_value
100+
out_max = max_output_value
101+
out_min = -max_output_value
102+
out_values = (((value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min) for value in out_values)
103+
104+
table += ", ".join(map(str, out_values))
105+
table += "};"
106+
table = textwrap.fill(table, 80)
107+
fout.write(table)
108+
fout.write("\n\n")
109+
fout.write(f"#endif /* {tablename}_H_ */\n")
110+
print(f"wrote {table} to {output_path}")
111+
112+
return 0
113+
114+
115+
if __name__ == "__main__":
116+
parser = ArgumentParser(
117+
description="Script for converting a raw audio file to a Mozzi table",
118+
)
119+
parser.add_argument(
120+
"-e",
121+
"--input-encoding",
122+
choices=("float", "int"),
123+
default="int",
124+
help="Input encoding",
125+
)
126+
parser.add_argument(
127+
"--input-bits",
128+
type=int,
129+
choices=(8, 16, 32),
130+
default=8,
131+
help="Number of bits for the INPUT encoding",
132+
)
133+
parser.add_argument(
134+
"--output-bits",
135+
type=int,
136+
choices=(8, 16, 32),
137+
default=8,
138+
help="Number of bits for each element of the OUTPUT table",
139+
)
140+
parser.add_argument("input_file", type=Path, help="Path to the input file")
141+
parser.add_argument(
142+
"-o",
143+
"--output-file",
144+
type=Path,
145+
help="Path to the output file. It will be input_file.h if not provided",
146+
)
147+
parser.add_argument(
148+
"-t",
149+
"--table-name",
150+
type=str,
151+
help="Name of the output table. If not provided, the name of the output will be used",
152+
)
153+
parser.add_argument(
154+
"-s",
155+
"--sample-rate",
156+
type=int,
157+
default=16384,
158+
help="Sample rate. Value of 16384 recommended",
159+
)
160+
parser.add_argument(
161+
"--make-symmetrical",
162+
action="store_true",
163+
help="Normalize the output between the range +/- max",
164+
)
165+
166+
args_ = parser.parse_args()
167+
sys.exit(float2mozzi(args_))

0 commit comments

Comments
 (0)