forked from UnsignedArduino/Audio-to-MakeCode-Arcade
-
Notifications
You must be signed in to change notification settings - Fork 43
Expand file tree
/
Copy pathmain.py
More file actions
129 lines (107 loc) · 4.83 KB
/
Copy pathmain.py
File metadata and controls
129 lines (107 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import struct
from argparse import ArgumentParser
from pathlib import Path
import numpy as np
import scipy
parser = ArgumentParser(description="Convert audio to MakeCode Arcade hex buffers!")
parser.add_argument("-i", "--input", metavar="PATH", type=Path, required=True,
help="The input MONO WAV file.")
parser.add_argument("-o", "--output", metavar="PATH", type=Path,
required=False,
help="The output TypeScript file which contains MakeCode Arcade code.")
parser.add_argument("-p", "--period", metavar="MILLISECONDS", type=int, default=25,
help="The period in milliseconds between each DFT for the spectrogram.")
parser.add_argument("--debug", action="store_true",
help="Print human readable strings instead of hex buffers for debugging")
args = parser.parse_args()
debug_output = args.debug
can_log = args.output is not None or debug_output
spectrogram_period = args.period
if can_log:
print(f"Arguments received: {args}")
input_path = args.input.expanduser().resolve()
if can_log:
print(f"Opening audio {input_path}")
# Read WAV file
sample_rate, data = scipy.io.wavfile.read(input_path)
# Convert to mono if necessary
if len(data.shape) > 1 and data.shape[1] > 1:
print(f"Audio has {data.shape[1]} channels, using only the first channel.")
data = data[:, 0]
sample_count = data.shape[0]
track_length = sample_count / sample_rate
if can_log:
print(f"Audio has {sample_count} samples at {sample_rate} Hz, {track_length:.2f} seconds long.")
def constrain(value, min_value, max_value):
return min(max(value, min_value), max_value)
def create_sound_instruction(start_freq: int, end_freq: int, start_vol: int,
end_vol: int, duration: int) -> str:
return struct.pack("<BBHHHHH",
3, # sine waveform
0,
max(start_freq, 1),
duration,
constrain(start_vol, 0, 1024),
constrain(end_vol, 0, 1024),
max(end_freq, 1)
).hex()
def audio_to_makecode_arcade(data, sample_rate, period, gain=2.5) -> str:
"""Convert audio to MakeCode Arcade code with louder output"""
spectrogram_frequency = period / 1000
if can_log:
print(f"Generating spectrogram with a period of {period} ms.")
# Generate spectrogram
f, t, Sxx = scipy.signal.spectrogram(
data,
sample_rate,
nperseg=round(spectrogram_frequency * sample_rate)
)
frequency_buckets = [50, 159, 200, 252, 317, 400, 504, 635, 800, 1008,
1270, 1600, 2016, 2504, 3200, 4032, 5080, 7000, 9000, 10240]
max_freqs = 30
loudest_indices = np.argsort(Sxx, axis=0)[-max_freqs:]
loudest_frequencies = f[loudest_indices].transpose()
loudest_amplitudes = Sxx[loudest_indices, np.arange(Sxx.shape[1])].transpose()
# Apply gain and normalize per bucket
sound_instruction_buffers = [""] * len(frequency_buckets)
max_amp = np.max(loudest_amplitudes)
for slice_index in range(len(loudest_frequencies)):
for bucket_index in range(len(frequency_buckets)):
freqs = loudest_frequencies[slice_index]
low = frequency_buckets[bucket_index - 1] if bucket_index > 0 else 0
high = frequency_buckets[bucket_index]
freq_index = -1
for i in range(len(freqs) - 1, -1, -1):
if low <= freqs[i] <= high:
freq_index = i
break
if freq_index != -1:
freq = round(freqs[freq_index])
amp = round(min(1024, (loudest_amplitudes[slice_index, freq_index] / max_amp * 1024) * gain))
sound_instruction_buffers[bucket_index] += create_sound_instruction(freq, freq, amp, amp, period)
else:
sound_instruction_buffers[bucket_index] += create_sound_instruction(0, 0, 0, 0, period)
# Wrap buffers in hex`` properly
sound_instruction_buffers = [f"hex`{buf}`" for buf in sound_instruction_buffers]
# MakeCode output
code = (
"namespace music {\n"
" //% shim=music::queuePlayInstructions\n"
" export function queuePlayInstructions(timeDelta: number, buf: Buffer) { }\n"
"}\n\n"
"const soundInstructions = [\n"
" " + ",\n ".join(sound_instruction_buffers) + "\n"
"];\n\n"
"for (const instructions of soundInstructions) {\n"
" music.queuePlayInstructions(100, instructions);\n"
"}\n"
)
return code
code = audio_to_makecode_arcade(data, sample_rate, spectrogram_period)
if args.output is not None:
output_path = args.output.expanduser().resolve()
if can_log:
print(f"Writing to {output_path}")
output_path.write_text(code)
else:
print(code)