11import struct
22from argparse import ArgumentParser
33from pathlib import Path
4-
54import numpy as np
65import scipy
76
2120can_log = args .output is not None or debug_output
2221spectrogram_period = args .period
2322
24- input_path = args .input .expanduser ()
23+ if can_log :
24+ print (f"Arguments received: { args } " )
25+
26+ input_path = args .input .expanduser ().resolve ()
27+ if can_log :
28+ print (f"Opening audio { input_path } " )
2529
2630# Read WAV file
2731sample_rate , data = scipy .io .wavfile .read (input_path )
@@ -44,7 +48,8 @@ def constrain(value, min_value, max_value):
4448def create_sound_instruction (start_freq : int , end_freq : int , start_vol : int ,
4549 end_vol : int , duration : int ) -> str :
4650 return struct .pack ("<BBHHHHH" ,
47- 3 , 0 ,
51+ 3 , # sine waveform
52+ 0 ,
4853 max (start_freq , 1 ),
4954 duration ,
5055 constrain (start_vol , 0 , 1024 ),
@@ -53,65 +58,52 @@ def create_sound_instruction(start_freq: int, end_freq: int, start_vol: int,
5358 ).hex ()
5459
5560
56- def moving_average_1d (arr , window_size = 3 ):
57- return np .convolve (arr , np .ones (window_size )/ window_size , mode = "same" )
58-
59-
60- def moving_average_2d (arr , window_size = 3 ):
61- smoothed = np .zeros_like (arr )
62- for i in range (arr .shape [0 ]):
63- smoothed [i ] = moving_average_1d (arr [i ], window_size )
64- return smoothed
65-
61+ def audio_to_makecode_arcade (data , sample_rate , period , gain = 2.5 ) -> str :
62+ """Convert audio to MakeCode Arcade code with louder output"""
63+ spectrogram_frequency = period / 1000
64+ if can_log :
65+ print (f"Generating spectrogram with a period of { period } ms." )
6666
67- def audio_to_makecode_arcade ( data , sample_rate , period ) -> str :
67+ # Generate spectrogram
6868 f , t , Sxx = scipy .signal .spectrogram (
6969 data ,
7070 sample_rate ,
71- nperseg = round (( period / 1000 ) * sample_rate )
71+ nperseg = round (spectrogram_frequency * sample_rate )
7272 )
7373
7474 frequency_buckets = [50 , 159 , 200 , 252 , 317 , 400 , 504 , 635 , 800 , 1008 ,
75- 1270 , 1600 , 2016 , 2504 , 3200 , 4032 , 5080 , 7000 , 9000 ]
75+ 1270 , 1600 , 2016 , 2504 , 3200 , 4032 , 5080 , 7000 , 9000 , 10240 ]
7676
7777 max_freqs = 30
7878 loudest_indices = np .argsort (Sxx , axis = 0 )[- max_freqs :]
7979 loudest_frequencies = f [loudest_indices ].transpose ()
8080 loudest_amplitudes = Sxx [loudest_indices , np .arange (Sxx .shape [1 ])].transpose ()
81- max_amp = np .max (Sxx )
8281
83- # Smooth amplitudes
84- loudest_amplitudes = moving_average_2d (loudest_amplitudes , window_size = 3 )
85-
86- # Create dedicated threads per bucket
87- threads = [[] for _ in frequency_buckets ]
82+ # Apply gain and normalize per bucket
83+ sound_instruction_buffers = ["" ] * len (frequency_buckets )
84+ max_amp = np .max (loudest_amplitudes )
8885
8986 for slice_index in range (len (loudest_frequencies )):
90- prev_bucket_high = 0
91- for bucket_index , bucket_high in enumerate (frequency_buckets ):
87+ for bucket_index in range (len (frequency_buckets )):
9288 freqs = loudest_frequencies [slice_index ]
93- amps = loudest_amplitudes [slice_index ]
94-
95- # pick the loudest frequency in this bucket range
96- freq_idx = - 1
97- for i in range (len (freqs )- 1 , - 1 , - 1 ):
98- if prev_bucket_high <= freqs [i ] <= bucket_high :
99- freq_idx = i
89+ low = frequency_buckets [bucket_index - 1 ] if bucket_index > 0 else 0
90+ high = frequency_buckets [bucket_index ]
91+ freq_index = - 1
92+ for i in range (len (freqs ) - 1 , - 1 , - 1 ):
93+ if low <= freqs [i ] <= high :
94+ freq_index = i
10095 break
101-
102- if freq_idx != - 1 :
103- freq = round (freqs [freq_idx ])
104- amp = round (amps [freq_idx ] / max_amp * 1024 )
105- threads [bucket_index ].append (create_sound_instruction (freq , freq , amp , amp , period ))
96+ if freq_index != - 1 :
97+ freq = round (freqs [freq_index ])
98+ amp = round (min (1024 , (loudest_amplitudes [slice_index , freq_index ] / max_amp * 1024 ) * gain ))
99+ sound_instruction_buffers [bucket_index ] += create_sound_instruction (freq , freq , amp , amp , period )
106100 else :
107- threads [bucket_index ].append (create_sound_instruction (0 , 0 , 0 , 0 , period ))
108-
109- prev_bucket_high = bucket_high
101+ sound_instruction_buffers [bucket_index ] += create_sound_instruction (0 , 0 , 0 , 0 , period )
110102
111- # Wrap each buffer in hex`` properly
112- sound_instruction_buffers = [f"hex`{ '' . join ( thread ) } `" for thread in threads ]
103+ # Wrap buffers in hex`` properly
104+ sound_instruction_buffers = [f"hex`{ buf } `" for buf in sound_instruction_buffers ]
113105
114- # MakeCode TS code (only queuePlayInstructions)
106+ # MakeCode output
115107 code = (
116108 "namespace music {\n "
117109 " //% shim=music::queuePlayInstructions\n "
@@ -128,13 +120,10 @@ def audio_to_makecode_arcade(data, sample_rate, period) -> str:
128120
129121
130122code = audio_to_makecode_arcade (data , sample_rate , spectrogram_period )
131-
132- # Always overwrite output
133123if args .output is not None :
134- output_path = Path (args .output )
135- output_path .parent .mkdir (parents = True , exist_ok = True )
136- output_path .write_text (code )
124+ output_path = args .output .expanduser ().resolve ()
137125 if can_log :
138- print (f"Written output to { output_path } " )
126+ print (f"Writing to { output_path } " )
127+ output_path .write_text (code )
139128else :
140129 print (code )
0 commit comments