Created
May 12, 2026 16:18
-
-
Save EncodeTheCode/90774becda470278ffcc0894db253b1b 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>Warmth</label> | |
| <input type="range" id="warmthSlider" min="0" max="2" step="0.01" value="1.2" /> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Tone Body</label> | |
| <input type="range" id="bodySlider" min="0" max="2" step="0.01" value="1.1" /> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Stereo Width</label> | |
| <input type="range" id="stereoSlider" min="0" max="1" step="0.01" value="0.2" /> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Jitter Reduction</label> | |
| <input type="range" id="jitterSlider" min="0" max="1" step="0.01" value="0.92" /> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Dither Removal</label> | |
| <input type="range" id="ditherSlider" min="0" max="1" step="0.01" value="0.88" /> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Harmonic Smoothness</label> | |
| <input type="range" id="smoothSlider" min="0" max="1" step="0.01" value="0.82" /> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Polyphonic Resonance</label> | |
| <input type="range" id="resonanceSlider" min="0" max="2" step="0.01" value="1.0" /> | |
| </div> | |
| <div class="slider-group"> | |
| <label>Speaker Emulation</label> | |
| <input type="range" id="speakerSlider" min="0" max="1" step="0.01" value="0.38" /> | |
| </div> | |
| <div class="slider-group"> | |
| <label><input type="checkbox" id="enableJitter" checked /> Enable Jitter Cleanup</label> | |
| </div> | |
| <div class="slider-group"> | |
| <label><input type="checkbox" id="enableDither" checked /> Enable Dither Cleanup</label> | |
| </div> | |
| <div class="slider-group"> | |
| <label><input type="checkbox" id="enableStereo" checked /> Enable Stereo Widening</label> | |
| </div> | |
| <div class="slider-group"> | |
| <label><input type="checkbox" id="enableSpeaker" checked /> Enable Sony Speaker Emulation</label> | |
| </div> | |
| <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 warmthSlider = document.getElementById('warmthSlider'); | |
| const bodySlider = document.getElementById('bodySlider'); | |
| const stereoSlider = document.getElementById('stereoSlider'); | |
| const jitterSlider = document.getElementById('jitterSlider'); | |
| const ditherSlider = document.getElementById('ditherSlider'); | |
| const smoothSlider = document.getElementById('smoothSlider'); | |
| const resonanceSlider = document.getElementById('resonanceSlider'); | |
| const speakerSlider = document.getElementById('speakerSlider'); | |
| const enableJitter = document.getElementById('enableJitter'); | |
| const enableDither = document.getElementById('enableDither'); | |
| const enableStereo = document.getElementById('enableStereo'); | |
| const enableSpeaker = document.getElementById('enableSpeaker'); | |
| 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(4096, 2, 2); | |
| let sampleHold = 0; | |
| let sampleCounter = 0; | |
| const phaseMemory = []; | |
| for (let i = 0; i < VOICE_COUNT; i++) { | |
| phaseMemory.push({ | |
| fundamental: 0, | |
| harmonicA: 0, | |
| harmonicB: 0, | |
| shimmer: 0 | |
| }); | |
| } | |
| let smoothEnvelope = 0; | |
| let toneBody = 0; | |
| let warmth = 0; | |
| let stereoWidth = 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; | |
| sampleHold = inputL[i]; | |
| } | |
| let crushed = Math.round(sampleHold * norm) / norm; | |
| const jitterReduction = parseFloat(jitterSlider.value); | |
| const ditherReduction = parseFloat(ditherSlider.value); | |
| const smoothness = parseFloat(smoothSlider.value); | |
| if (enableJitter.checked) { | |
| crushed = crushed * jitterReduction + sampleHold * (1 - jitterReduction); | |
| } | |
| if (enableDither.checked) { | |
| crushed *= (1 - (0.015 * ditherReduction)); | |
| } | |
| crushed = crushed * smoothness + sampleHold * (1 - smoothness); | |
| const amplitude = Math.abs(crushed); | |
| smoothEnvelope = smoothEnvelope * 0.992 + amplitude * 0.008; | |
| toneBody = toneBody * 0.985 + amplitude * 0.015; | |
| warmth = warmth * 0.978 + amplitude * 0.022; | |
| stereoWidth = stereoWidth * 0.995 + amplitude * 0.005; | |
| let polyphonicMix = 0; | |
| for (let v = 0; v < polyphony; v++) { | |
| const phases = phaseMemory[v]; | |
| const spread = 1 + (v * 0.0025); | |
| const detune = Math.sin(v * 0.7) * 0.35; | |
| const baseFreq = 110 * spread + detune; | |
| const harmonicFreq = 220 * spread + detune; | |
| const upperFreq = 440 * spread + detune; | |
| const shimmerFreq = 1760 * spread; | |
| phases.fundamental += (2 * Math.PI * baseFreq) / audioCtx.sampleRate; | |
| phases.harmonicA += (2 * Math.PI * harmonicFreq) / audioCtx.sampleRate; | |
| phases.harmonicB += (2 * Math.PI * upperFreq) / audioCtx.sampleRate; | |
| phases.shimmer += (2 * Math.PI * shimmerFreq) / audioCtx.sampleRate; | |
| const fundamentalTone = ( | |
| Math.sin(phases.fundamental) + | |
| Math.sin(phases.fundamental * 0.5) * 0.22 | |
| ) * smoothEnvelope; | |
| const harmonicTone = ( | |
| Math.sin(phases.harmonicA) + | |
| Math.sin(phases.harmonicA * 2) * 0.08 | |
| ) * toneBody; | |
| const upperTone = Math.sin(phases.harmonicB) * warmth; | |
| const shimmerTone = Math.sin(phases.shimmer) * stereoWidth * 0.06; | |
| const warmthAmount = parseFloat(warmthSlider.value); | |
| const bodyAmount = parseFloat(bodySlider.value); | |
| const resonanceAmount = parseFloat(resonanceSlider.value); | |
| const sonyVoice = ( | |
| fundamentalTone * (0.58 * bodyAmount) + | |
| harmonicTone * (0.27 * warmthAmount) + | |
| upperTone * (0.11 * resonanceAmount) + | |
| shimmerTone * 0.04 | |
| ); | |
| polyphonicMix += sonyVoice / Math.pow(v + 1, 0.42); | |
| } | |
| polyphonicMix /= (polyphony * 0.16); | |
| const rounded = Math.tanh(polyphonicMix * 1.35); | |
| let sonyColor = ( | |
| rounded * 0.88 + | |
| Math.sin(rounded * 1.2) * 0.08 + | |
| crushed * 0.04 | |
| ); | |
| if (enableSpeaker.checked) { | |
| const speakerAmount = parseFloat(speakerSlider.value); | |
| sonyColor = sonyColor * (1 - speakerAmount * 0.12) + Math.tanh(sonyColor * 1.5) * (speakerAmount * 0.12); | |
| } | |
| if (enableStereo.checked) { | |
| const stereoAmount = parseFloat(stereoSlider.value); | |
| outputL[i] = sonyColor * (1 + stereoAmount * 0.03); | |
| outputR[i] = sonyColor * (1 - stereoAmount * 0.03); | |
| } else { | |
| } | |
| outputL[i] = sonyColor; | |
| outputR[i] = sonyColor; | |
| } | |
| }; | |
| 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