|
3 | 3 | <head> |
4 | 4 | <meta charset="UTF-8"> |
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
6 | | -<title>WAV → MakeCode Converter</title> |
| 6 | +<title>MakeCode Arcade WAV Converter</title> |
| 7 | +<script src="https://cdn.jsdelivr.net/pyodide/v0.26.3/full/pyodide.js"></script> |
7 | 8 | <style> |
8 | 9 | body { |
9 | | - font-family: Arial, sans-serif; |
10 | | - text-align: center; |
11 | | - background: #181818; |
12 | | - color: #eee; |
13 | | - margin: 0; |
14 | | - padding: 40px; |
| 10 | + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; |
| 11 | + background: #1e1e2f; |
| 12 | + color: #e0e0e0; |
| 13 | + display: flex; |
| 14 | + flex-direction: column; |
| 15 | + align-items: center; |
| 16 | + padding: 2rem; |
| 17 | + } |
| 18 | + h1 { |
| 19 | + color: #ff6f61; |
15 | 20 | } |
16 | | - h1 { color: #00ffcc; } |
17 | | - #dropzone { |
18 | | - border: 2px dashed #00ffcc; |
19 | | - border-radius: 10px; |
20 | | - padding: 40px; |
21 | | - margin: 20px auto; |
22 | | - width: 80%; |
| 21 | + .drop-area { |
| 22 | + border: 2px dashed #ff6f61; |
| 23 | + padding: 2rem; |
| 24 | + margin: 1rem; |
| 25 | + border-radius: 12px; |
| 26 | + width: 100%; |
23 | 27 | max-width: 600px; |
24 | | - transition: 0.3s; |
| 28 | + text-align: center; |
| 29 | + transition: background 0.3s; |
| 30 | + cursor: pointer; |
25 | 31 | } |
26 | | - #dropzone.dragover { |
27 | | - background-color: #002b29; |
28 | | - border-color: #00ffaa; |
| 32 | + .drop-area.dragover { |
| 33 | + background: #ff6f61; |
| 34 | + color: #1e1e2f; |
29 | 35 | } |
30 | | - pre { |
31 | | - background: #111; |
32 | | - color: #0f0; |
33 | | - padding: 15px; |
34 | | - border-radius: 8px; |
35 | | - text-align: left; |
36 | | - overflow-x: auto; |
| 36 | + #output { |
| 37 | + width: 100%; |
| 38 | + max-width: 600px; |
| 39 | + margin-top: 1rem; |
| 40 | + padding: 1rem; |
| 41 | + background: #2a2a3d; |
| 42 | + border-radius: 12px; |
37 | 43 | white-space: pre-wrap; |
38 | | - word-wrap: break-word; |
| 44 | + overflow-x: auto; |
39 | 45 | } |
40 | | - #instructions { |
41 | | - margin-top: 20px; |
42 | | - background: #202020; |
43 | | - border-radius: 8px; |
44 | | - padding: 10px; |
| 46 | + button { |
| 47 | + margin-top: 1rem; |
| 48 | + padding: 0.5rem 1rem; |
| 49 | + border: none; |
| 50 | + border-radius: 6px; |
| 51 | + background: #ff6f61; |
| 52 | + color: #1e1e2f; |
| 53 | + font-weight: bold; |
| 54 | + cursor: pointer; |
45 | 55 | } |
46 | 56 | </style> |
47 | 57 | </head> |
48 | 58 | <body> |
49 | | - <h1>🎵 WAV → MakeCode Converter</h1> |
50 | | - <div id="dropzone">Drag and drop a WAV file here</div> |
51 | | - |
52 | | - <div id="instructions"> |
53 | | - <!-- INSTRUCTIONS HERE --> |
54 | | - </div> |
55 | | - |
56 | | - <h2>Generated MakeCode Output:</h2> |
57 | | - <pre id="output"></pre> |
| 59 | +<h1>MakeCode Arcade WAV Converter</h1> |
| 60 | +<div class="drop-area" id="drop-area"> |
| 61 | + Drag & Drop WAV File Here<br>or Click to Select |
| 62 | +</div> |
| 63 | +<input type="file" id="file-input" accept=".wav" style="display:none;"> |
| 64 | +<pre id="output">Output will appear here...</pre> |
58 | 65 |
|
59 | 66 | <script> |
60 | | -function readWavFile(file) { |
61 | | - return new Promise((resolve, reject) => { |
62 | | - const reader = new FileReader(); |
63 | | - reader.onload = e => resolve(e.target.result); |
64 | | - reader.onerror = reject; |
65 | | - reader.readAsArrayBuffer(file); |
66 | | - }); |
67 | | -} |
68 | | - |
69 | | -function wavToMakeCode(buffer) { |
70 | | - const view = new DataView(buffer); |
71 | | - const numChannels = view.getUint16(22, true); |
72 | | - const sampleRate = view.getUint32(24, true); |
73 | | - const bitsPerSample = view.getUint16(34, true); |
74 | | - |
75 | | - let offset = 44; |
76 | | - let samples = []; |
77 | | - for (; offset < view.byteLength; offset += 2) { |
78 | | - const sample = view.getInt16(offset, true); |
79 | | - samples.push(sample / 32768); |
80 | | - } |
81 | | - |
82 | | - const step = Math.max(1, Math.floor(samples.length / 300)); |
83 | | - const filtered = samples.filter((_, i) => i % step === 0); |
84 | | - const buf = []; |
| 67 | +let pyodideReadyPromise = loadPyodide({indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.3/full/"}); |
85 | 68 |
|
86 | | - for (let i = 0; i < filtered.length; i++) { |
87 | | - const amp = Math.abs(filtered[i]); |
88 | | - let freq = 200 + amp * 1800; |
| 69 | +const dropArea = document.getElementById('drop-area'); |
| 70 | +const fileInput = document.getElementById('file-input'); |
| 71 | +const output = document.getElementById('output'); |
89 | 72 |
|
90 | | - // --- Frequency scaling fix (better pitch) --- |
91 | | - let scaledFreq = Math.sqrt(freq) * 16; |
92 | | - if (scaledFreq < 80) scaledFreq = 80; |
93 | | - if (scaledFreq > 4000) scaledFreq = 4000; |
| 73 | +dropArea.addEventListener('click', () => fileInput.click()); |
94 | 74 |
|
95 | | - const hex = scaledFreq.toString(16).padStart(4, "0"); |
96 | | - buf.push(hex); |
97 | | - } |
| 75 | +fileInput.addEventListener('change', e => handleFile(e.target.files[0])); |
98 | 76 |
|
99 | | - return ` |
100 | | -namespace music { |
101 | | - //% shim=music::queuePlayInstructions |
102 | | - export function queuePlayInstructions(timeDelta: number, buf: Buffer) { } |
103 | | -} |
104 | | -
|
105 | | -const soundInstructions = [ |
106 | | - hex\`${buf.join("")}\` |
107 | | -]; |
108 | | -for (const instructions of soundInstructions) { |
109 | | - music.playInstructions(100, instructions); |
110 | | -}`; |
111 | | -} |
112 | | - |
113 | | -const dropzone = document.getElementById("dropzone"); |
114 | | -const output = document.getElementById("output"); |
115 | | - |
116 | | -dropzone.addEventListener("dragover", e => { |
| 77 | +dropArea.addEventListener('dragover', e => { |
117 | 78 | e.preventDefault(); |
118 | | - dropzone.classList.add("dragover"); |
| 79 | + dropArea.classList.add('dragover'); |
119 | 80 | }); |
120 | | - |
121 | | -dropzone.addEventListener("dragleave", () => { |
122 | | - dropzone.classList.remove("dragover"); |
| 81 | +dropArea.addEventListener('dragleave', e => { |
| 82 | + dropArea.classList.remove('dragover'); |
123 | 83 | }); |
124 | | - |
125 | | -dropzone.addEventListener("drop", async e => { |
| 84 | +dropArea.addEventListener('drop', e => { |
126 | 85 | e.preventDefault(); |
127 | | - dropzone.classList.remove("dragover"); |
128 | | - const file = e.dataTransfer.files[0]; |
129 | | - if (!file || !file.name.endsWith(".wav")) { |
130 | | - output.textContent = "Please drop a valid .wav file."; |
| 86 | + dropArea.classList.remove('dragover'); |
| 87 | + handleFile(e.dataTransfer.files[0]); |
| 88 | +}); |
| 89 | + |
| 90 | +async function handleFile(file) { |
| 91 | + if (!file.name.endsWith('.wav')) { |
| 92 | + output.textContent = "Please provide a WAV file."; |
131 | 93 | return; |
132 | 94 | } |
133 | | - output.textContent = "Processing..."; |
134 | | - |
| 95 | + output.textContent = "Loading Python runtime..."; |
| 96 | + const pyodide = await pyodideReadyPromise; |
| 97 | + |
| 98 | + const arrayBuffer = await file.arrayBuffer(); |
| 99 | + const wavBytes = new Uint8Array(arrayBuffer); |
| 100 | + |
| 101 | + // Python code: audio conversion script |
| 102 | + const pyCode = ` |
| 103 | +import struct |
| 104 | +import numpy as np |
| 105 | +import scipy |
| 106 | +from js import wavBytes |
| 107 | +
|
| 108 | +def constrain(value, min_value, max_value): |
| 109 | + return min(max(value, min_value), max_value) |
| 110 | +
|
| 111 | +def create_sound_instruction(start_freq, end_freq, start_vol, end_vol, duration): |
| 112 | + return struct.pack("<BBHHHHH", |
| 113 | + 3, 0, |
| 114 | + max(start_freq, 1), |
| 115 | + duration, |
| 116 | + constrain(start_vol, 0, 1024), |
| 117 | + constrain(end_vol, 0, 1024), |
| 118 | + max(end_freq, 1) |
| 119 | + ).hex() |
| 120 | +
|
| 121 | +def audio_to_makecode_arcade(wav_bytes, period=25, gain=2.5): |
| 122 | + from io import BytesIO |
| 123 | + sample_rate, data = scipy.io.wavfile.read(BytesIO(wav_bytes.to_py())) |
| 124 | +
|
| 125 | + if len(data.shape) > 1 and data.shape[1] > 1: |
| 126 | + data = data[:, 0] |
| 127 | +
|
| 128 | + f, t, Sxx = scipy.signal.spectrogram(data, sample_rate, nperseg=round(period/1000 * sample_rate)) |
| 129 | + frequency_buckets = [50, 159, 200, 252, 317, 400, 504, 635, 800, 1008, |
| 130 | + 1270, 1600, 2016, 2504, 3200, 4032, 5080, 7000, 9000, 10240] |
| 131 | +
|
| 132 | + max_freqs = 30 |
| 133 | + loudest_indices = np.argsort(Sxx, axis=0)[-max_freqs:] |
| 134 | + loudest_frequencies = f[loudest_indices].transpose() |
| 135 | + loudest_amplitudes = Sxx[loudest_indices, np.arange(Sxx.shape[1])].transpose() |
| 136 | + sound_instruction_buffers = [""] * len(frequency_buckets) |
| 137 | + max_amp = np.max(loudest_amplitudes) |
| 138 | +
|
| 139 | + for slice_index in range(len(loudest_frequencies)): |
| 140 | + for bucket_index in range(len(frequency_buckets)): |
| 141 | + freqs = loudest_frequencies[slice_index] |
| 142 | + low = frequency_buckets[bucket_index-1] if bucket_index>0 else 0 |
| 143 | + high = frequency_buckets[bucket_index] |
| 144 | + freq_index = -1 |
| 145 | + for i in range(len(freqs)-1,-1,-1): |
| 146 | + if low <= freqs[i] <= high: |
| 147 | + freq_index = i |
| 148 | + break |
| 149 | + if freq_index != -1: |
| 150 | + freq = round(freqs[freq_index]) |
| 151 | + amp = round(min(1024, (loudest_amplitudes[slice_index,freq_index]/max_amp*1024)*gain)) |
| 152 | + sound_instruction_buffers[bucket_index] += create_sound_instruction(freq,freq,amp,amp,period) |
| 153 | + else: |
| 154 | + sound_instruction_buffers[bucket_index] += create_sound_instruction(0,0,0,0,period) |
| 155 | +
|
| 156 | + sound_instruction_buffers = [f"hex`{buf}`" for buf in sound_instruction_buffers] |
| 157 | +
|
| 158 | + code = ( |
| 159 | + "namespace music:\\n" |
| 160 | + " //% shim=music::queuePlayInstructions\\n" |
| 161 | + " export function queuePlayInstructions(timeDelta: number, buf: Buffer) { }\\n\\n" |
| 162 | + "const soundInstructions = [\\n" |
| 163 | + " " + ",\\n ".join(sound_instruction_buffers) + "\\n];\\n\\n" |
| 164 | + "for (const instructions of soundInstructions) {\\n" |
| 165 | + " music.queuePlayInstructions(100, instructions);\\n}" |
| 166 | + ) |
| 167 | + return code |
| 168 | +
|
| 169 | +audio_to_makecode_arcade(wavBytes) |
| 170 | +`; |
| 171 | + |
| 172 | + output.textContent = "Converting..."; |
135 | 173 | try { |
136 | | - const data = await readWavFile(file); |
137 | | - const code = wavToMakeCode(data); |
138 | | - output.textContent = code; |
| 174 | + const result = await pyodide.runPythonAsync(pyCode); |
| 175 | + output.textContent = result; |
139 | 176 | } catch (err) { |
140 | | - console.error(err); |
141 | | - output.textContent = "Error reading WAV file."; |
| 177 | + output.textContent = "Error: " + err; |
142 | 178 | } |
143 | | -}); |
| 179 | +} |
144 | 180 | </script> |
145 | 181 | </body> |
146 | 182 | </html> |
0 commit comments