Skip to content

Commit 099fca1

Browse files
Update index.html
1 parent 59b6eb0 commit 099fca1

File tree

1 file changed

+147
-111
lines changed

1 file changed

+147
-111
lines changed

index.html

Lines changed: 147 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -3,144 +3,180 @@
33
<head>
44
<meta charset="UTF-8">
55
<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>
78
<style>
89
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;
1520
}
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%;
2327
max-width: 600px;
24-
transition: 0.3s;
28+
text-align: center;
29+
transition: background 0.3s;
30+
cursor: pointer;
2531
}
26-
#dropzone.dragover {
27-
background-color: #002b29;
28-
border-color: #00ffaa;
32+
.drop-area.dragover {
33+
background: #ff6f61;
34+
color: #1e1e2f;
2935
}
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;
3743
white-space: pre-wrap;
38-
word-wrap: break-word;
44+
overflow-x: auto;
3945
}
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;
4555
}
4656
</style>
4757
</head>
4858
<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>
5865

5966
<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/"});
8568

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');
8972

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());
9474

95-
const hex = scaledFreq.toString(16).padStart(4, "0");
96-
buf.push(hex);
97-
}
75+
fileInput.addEventListener('change', e => handleFile(e.target.files[0]));
9876

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 => {
11778
e.preventDefault();
118-
dropzone.classList.add("dragover");
79+
dropArea.classList.add('dragover');
11980
});
120-
121-
dropzone.addEventListener("dragleave", () => {
122-
dropzone.classList.remove("dragover");
81+
dropArea.addEventListener('dragleave', e => {
82+
dropArea.classList.remove('dragover');
12383
});
124-
125-
dropzone.addEventListener("drop", async e => {
84+
dropArea.addEventListener('drop', e => {
12685
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.";
13193
return;
13294
}
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...";
135173
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;
139176
} catch (err) {
140-
console.error(err);
141-
output.textContent = "Error reading WAV file.";
177+
output.textContent = "Error: " + err;
142178
}
143-
});
179+
}
144180
</script>
145181
</body>
146182
</html>

0 commit comments

Comments
 (0)