Created
May 12, 2026 15:48
-
-
Save EncodeTheCode/bf54cb0a211de985b8dd577db58c81e0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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