Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save EncodeTheCode/bf54cb0a211de985b8dd577db58c81e0 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/bf54cb0a211de985b8dd577db58c81e0 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>24 Voice Polyphonic Chiptune Player</title>
<style>
body {
margin: 0;
background: #0d0d12;
color: #f5f5f5;
font-family: Arial, sans-serif;
overflow-x: hidden;
}
.container {
max-width: 1100px;
margin: auto;
padding: 24px;
}
.player {
background: #171722;
border-radius: 16px;
padding: 24px;
box-shadow: 0 0 30px rgba(0,0,0,0.4);
}
h1 {
margin-top: 0;
font-size: 32px;
}
.drop-zone {
border: 2px dashed #666;
border-radius: 16px;
padding: 40px;
text-align: center;
transition: 0.25s;
margin-bottom: 20px;
background: rgba(255,255,255,0.02);
}
.drop-zone.dragover {
border-color: #66d9ef;
background: rgba(102,217,239,0.08);
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 18px;
}
button {
border: none;
background: #66d9ef;
color: black;
padding: 12px 18px;
border-radius: 10px;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
}
button:hover {
transform: scale(1.05);
}
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.sliders {
margin-top: 24px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
}
.slider-group {
background: rgba(255,255,255,0.03);
padding: 16px;
border-radius: 12px;
}
label {
display: block;
margin-bottom: 8px;
font-size: 14px;
}
input[type="range"] {
width: 100%;
}
canvas {
width: 100%;
height: 120px;
background: black;
border-radius: 12px;
margin-top: 22px;
}
.status {
margin-top: 12px;
color: #a8a8a8;
}
.file-name {
margin-top: 12px;
font-weight: bold;
color: #66d9ef;
}
.footer {
margin-top: 20px;
opacity: 0.6;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<div class="player">
<h1>24 Voice Polyphonic Chiptune Converter</h1>
<div class="drop-zone" id="dropZone">
Drag & Drop MP3 / WAV Here<br />
<small>Realtime conversion into classic early 2000s Sony-style polyphonic ringtone tone synthesis</small>
</div>
<input type="file" id="fileInput" accept="audio/*" hidden />
<div class="controls">
<button id="browseBtn">Browse</button>
<button id="playBtn" disabled>Play</button>
<button id="pauseBtn" disabled>Pause</button>
<button id="stopBtn" disabled>Stop</button>
<button id="saveBtn" disabled>Save Polyphonic MP3</button>
</div>
<div class="sliders">
<div class="slider-group">
<label>Bit Crush Depth</label>
<input type="range" id="bitDepth" min="2" max="12" value="5" />
</div>
<div class="slider-group">
<label>Sample Rate Reduction</label>
<input type="range" id="sampleRate" min="4000" max="22050" value="9000" />
</div>
<div class="slider-group">
<label>Polyphony Voices</label>
<input type="range" id="polyphony" min="4" max="24" value="24" />
</div>
<div class="slider-group">
<label>Delay / Resonance</label>
<input type="range" id="delayMix" min="0" max="1" step="0.01" value="0.24" />
</div>
</div>
<canvas id="visualizer"></canvas>
<div class="file-name" id="fileName">No file loaded</div>
<div class="status" id="status">Idle</div>
<div class="footer">
Engine: Web Audio API | 24 Voice Polyphonic Synth Emulation | Memory Optimized Streaming
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/lamejs@1.2.1/lame.min.js"></script>
<script>
const audioCtx = new (window.AudioContext || window.webkitAudioContext)({ latencyHint: 'interactive' });
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const browseBtn = document.getElementById('browseBtn');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const stopBtn = document.getElementById('stopBtn');
const saveBtn = document.getElementById('saveBtn');
const fileName = document.getElementById('fileName');
const statusEl = document.getElementById('status');
const bitDepthSlider = document.getElementById('bitDepth');
const sampleRateSlider = document.getElementById('sampleRate');
const polyphonySlider = document.getElementById('polyphony');
const delayMixSlider = document.getElementById('delayMix');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
let sourceNode;
let audioBuffer;
let analyser;
let processor;
let delayNode;
let feedbackNode;
let lowpass;
let mediaDest;
let mediaRecorder;
let recordedChunks = [];
let isPaused = false;
let startTime = 0;
let pauseOffset = 0;
let animationId;
const VOICE_COUNT = 24;
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
function setStatus(msg) {
statusEl.textContent = msg;
}
browseBtn.onclick = () => fileInput.click();
fileInput.onchange = e => {
const file = e.target.files[0];
if (file) loadFile(file);
};
dropZone.addEventListener('dragover', e => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file) loadFile(file);
});
async function loadFile(file) {
setStatus('Loading and preparing realtime polyphonic synthesis...');
const arrayBuffer = await file.arrayBuffer();
audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
fileName.textContent = file.name;
playBtn.disabled = false;
stopBtn.disabled = false;
saveBtn.disabled = false;
setStatus('Ready');
}
function createChiptuneProcessor() {
processor = audioCtx.createScriptProcessor(2048, 2, 2);
let phase = 0;
let heldSample = 0;
let sampleCounter = 0;
processor.onaudioprocess = e => {
const inputL = e.inputBuffer.getChannelData(0);
const outputL = e.outputBuffer.getChannelData(0);
const outputR = e.outputBuffer.getChannelData(1);
const bits = parseInt(bitDepthSlider.value);
const norm = Math.pow(2, bits - 1);
const reduction = parseInt(sampleRateSlider.value);
const sampleStep = audioCtx.sampleRate / reduction;
const polyphony = parseInt(polyphonySlider.value);
for (let i = 0; i < inputL.length; i++) {
sampleCounter++;
if (sampleCounter >= sampleStep) {
sampleCounter = 0;
heldSample = inputL[i];
}
let crushed = Math.round(heldSample * norm) / norm;
let synth = 0;
for (let v = 0; v < polyphony; v++) {
const harmonic = v + 1;
const freqScale = 1 + harmonic * 0.003;
phase += (0.02 * freqScale);
const square = Math.sin(phase * harmonic) > 0 ? 1 : -1;
synth += square * crushed * (1 / harmonic);
}
synth /= polyphony * 0.3;
outputL[i] = synth;
outputR[i] = synth;
}
};
return processor;
}
function setupAudioGraph() {
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
lowpass = audioCtx.createBiquadFilter();
lowpass.type = 'lowpass';
lowpass.frequency.value = 7200;
delayNode = audioCtx.createDelay();
delayNode.delayTime.value = 0.08;
feedbackNode = audioCtx.createGain();
feedbackNode.gain.value = delayMixSlider.value;
mediaDest = audioCtx.createMediaStreamDestination();
processor = createChiptuneProcessor();
processor.connect(lowpass);
lowpass.connect(delayNode);
delayNode.connect(feedbackNode);
feedbackNode.connect(delayNode);
delayNode.connect(analyser);
analyser.connect(audioCtx.destination);
analyser.connect(mediaDest);
}
function cleanup() {
if (sourceNode) {
try { sourceNode.stop(); } catch (e) {}
sourceNode.disconnect();
}
if (processor) processor.disconnect();
if (delayNode) delayNode.disconnect();
if (feedbackNode) feedbackNode.disconnect();
if (lowpass) lowpass.disconnect();
cancelAnimationFrame(animationId);
}
playBtn.onclick = async () => {
if (!audioBuffer) return;
cleanup();
setupAudioGraph();
await audioCtx.resume();
sourceNode = audioCtx.createBufferSource();
sourceNode.buffer = audioBuffer;
sourceNode.connect(processor);
setStatus('Realtime 24 voice polyphonic rendering...');
recordedChunks = [];
mediaRecorder = new MediaRecorder(mediaDest.stream);
mediaRecorder.ondataavailable = e => {
if (e.data.size > 0) recordedChunks.push(e.data);
};
mediaRecorder.start();
const offset = isPaused ? pauseOffset : 0;
startTime = audioCtx.currentTime - offset;
sourceNode.start(0, offset);
playBtn.disabled = true;
pauseBtn.disabled = false;
isPaused = false;
visualize();
sourceNode.onended = () => {
if (!isPaused) {
stopPlayback();
}
};
};
pauseBtn.onclick = () => {
if (!sourceNode) return;
pauseOffset = audioCtx.currentTime - startTime;
isPaused = true;
cleanup();
playBtn.disabled = false;
setStatus('Paused');
};
stopBtn.onclick = stopPlayback;
function stopPlayback() {
cleanup();
pauseOffset = 0;
isPaused = false;
playBtn.disabled = false;
pauseBtn.disabled = true;
setStatus('Stopped');
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
}
saveBtn.onclick = async () => {
if (!recordedChunks.length) {
alert('Play the track fully before exporting.');
return;
}
setStatus('Encoding MP3...');
const blob = new Blob(recordedChunks, { type: 'audio/webm' });
const audio = await blob.arrayBuffer();
const decoded = await audioCtx.decodeAudioData(audio);
const samples = decoded.getChannelData(0);
const mp3encoder = new lamejs.Mp3Encoder(1, decoded.sampleRate, 128);
const sampleBlockSize = 1152;
let mp3Data = [];
for (let i = 0; i < samples.length; i += sampleBlockSize) {
const sampleChunk = samples.subarray(i, i + sampleBlockSize);
const int16 = new Int16Array(sampleChunk.length);
for (let j = 0; j < sampleChunk.length; j++) {
int16[j] = sampleChunk[j] * 32767;
}
const mp3buf = mp3encoder.encodeBuffer(int16);
if (mp3buf.length > 0) {
mp3Data.push(mp3buf);
}
}
const end = mp3encoder.flush();
if (end.length > 0) {
mp3Data.push(end);
}
const mp3Blob = new Blob(mp3Data, { type: 'audio/mp3' });
const url = URL.createObjectURL(mp3Blob);
const a = document.createElement('a');
a.href = url;
a.download = 'polyphonic_ringtone.mp3';
a.click();
setStatus('MP3 exported');
};
function visualize() {
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
function draw() {
animationId = requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i] / 2;
ctx.fillStyle = `rgb(${barHeight + 80},255,180)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
draw();
}
delayMixSlider.oninput = () => {
if (feedbackNode) {
feedbackNode.gain.value = delayMixSlider.value;
}
};
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment