<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>libopenmpt Note Viewer</title> <!-- 1. Load Tailwind CSS for styling --> <!-- Import Tailwind CSS for styling --><script src="https://js.1ink.us/tailwinds.1ijs"></script> <style> /* Custom styles for a dark, tracker-like theme */ body { font-family: 'Inter', sans-serif; } /* Style for the file input button */ input[type="file"]::file-selector-button { background-color: #3b82f6; /* bg-blue-600 */ color: white; padding: 0.5rem 1rem; border-radius: 0.375rem; border: none; cursor: pointer; transition: background-color 0.2s; } input[type="file"]::file-selector-button:hover { background-color: #2563eb; /* bg-blue-700 */ } /* Style for disabled buttons */ button:disabled { background-color: #4b5563; /* bg-gray-600 */ cursor: not-allowed; } </style> </head> <body class="bg-gray-900 text-gray-200 min-h-screen p-4 md:p-8"> <div class="max-w-7xl mx-auto"> <header class="mb-6"> <h1 class="text-3xl font-bold text-white mb-2">libopenmpt Note Data Viewer</h1> <p id="status" class="text-lg text-yellow-400">Loading library...</p> </header> <!-- 2. UI Controls --> <section class="bg-gray-800 p-4 rounded-lg shadow-lg mb-6 flex flex-wrap gap-4 items-center"> <input type="file" id="file-input" class="text-sm text-gray-300" disabled> <button id="play-button" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition-colors" disabled> Play </button> <button id="stop-button" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-lg transition-colors" disabled> Stop </button> </section> <!-- 3. Module Information Display --> <section class="bg-gray-800 p-4 rounded-lg shadow-lg mb-6 text-sm"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div> <span class="font-bold text-gray-400">Title:</span> <span id="song-title" class="ml-2">...</span> </div> <div> <span class="font-bold text-gray-400">Position:</span> <span class="ml-2">Order: <span id="current-order" class="font-semibold text-white">--</span></span> <span class="ml-4">Row: <span id="current-row" class="font-semibold text-white">--</span></span> </div> <div> <span class="font-bold text-gray-400">Tempo:</span> <span class="ml-2"><span id="current-bpm" class="font-semibold text-white">--</span> BPM</span> </div> </div> </section> <!-- 4. Real-time Pattern Data Display --> <section class="bg-black p-4 rounded-lg shadow-inner"> <pre id="pattern-display" class="font-mono text-sm text-green-400 overflow-x-auto whitespace-pre h-96"> ... Waiting for module to play ... </pre> </section> </div> <!-- 5. Load libopenmpt.js --> <!-- This CDN link points to a build of the library. You can replace this with your local "libopenmpt.js" file. --> <!-- <script src="https://cdn.jsdelivr.net/npm/libopenmpt-js/libopenmpt.js"></script> --> <!-- 6. Main Application Logic --> <script> // --- Global Variables --- // let libopenmpt; // The Emscripten Module object <-- Removed this redundant declaration let currentModulePtr = 0; // Pointer to the loaded module in Emscripten heap let audioContext; let scriptNode; let isPlaying = false; let uiUpdateHandle = 0; const sampleRate = 48000; const bufferSize = 4096; // Audio buffer size let rowBuffer = {}; // Cache for pattern rows to avoid flicker let bpmEl, patternDisplayEl; // --- Emscripten Module Setup --- // We must define Module *before* the script loads. // onRuntimeInitialized is our main() function. // // FIX: Change 'Module' to 'libopenmpt' to match your custom GCS script's // build settings (-sEXPORT_NAME=libopenmpt) var libopenmpt = { onRuntimeInitialized: () => { // libopenmpt = Module; // No longer need this, 'libopenmpt' is the module console.log('libopenmpt runtime initialized.'); // --- FIX: Add missing Emscripten helper functions (SELF-CONTAINED) --- // Converts a C-style string (a pointer) *from* memory to a JavaScript string. libopenmpt.UTF8ToString = (ptr) => { let str = ''; let i = 0; if (!ptr) return str; // Handle null pointer // Check for HEAPU8 existence, otherwise fallback to HEAP8 const heap = libopenmpt.HEAPU8 || libopenmpt.HEAP8; if (!heap) return ''; // Critical failure while (true) { const char = heap[ptr + i]; if (char === 0) break; str += String.fromCharCode(char); i++; } return str; }; // Converts a JavaScript string *to* a C-style string (a pointer to memory). // This is a self-contained polyfill because the build is missing the helpers. libopenmpt.stringToUTF8 = (jsString) => { let utf8Bytes = []; for (let i = 0; i < jsString.length; i++) { let charcode = jsString.charCodeAt(i); if (charcode < 0x80) { utf8Bytes.push(charcode); } else if (charcode < 0x800) { utf8Bytes.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f)); } else if (charcode < 0xd800 || charcode >= 0xe000) { utf8Bytes.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f)); } else { // Surrogates i++; charcode = 0x10000 + (((charcode & 0x3ff) << 10) | (jsString.charCodeAt(i) & 0x3ff)); utf8Bytes.push(0xf0 | (charcode >> 18), 0x80 | ((charcode >> 12) & 0x3f), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f)); } } // Add null terminator utf8Bytes.push(0); const ptr = libopenmpt._malloc(utf8Bytes.length); // Check for HEAPU8 existence, otherwise fallback to HEAP8 const heap = libopenmpt.HEAPU8 || libopenmpt.HEAP8; if (!heap) return 0; // Critical failure heap.set(utf8Bytes, ptr); return ptr; }; // --- End of FIX --- // Get all UI elements statusEl = document.getElementById('status'); fileInputEl = document.getElementById('file-input'); playButtonEl = document.getElementById('play-button'); stopButtonEl = document.getElementById('stop-button'); titleEl = document.getElementById('song-title'); orderEl = document.getElementById('current-order'); rowEl = document.getElementById('current-row'); bpmEl = document.getElementById('current-bpm'); patternDisplayEl = document.getElementById('pattern-display'); // Setup event listeners fileInputEl.addEventListener('change', loadModuleFile); playButtonEl.addEventListener('click', playMusic); stopButtonEl.addEventListener('click', stopMusic); // Enable the UI statusEl.textContent = 'Ready. Please select a module file.'; fileInputEl.disabled = false; } }; // --- File Loading --- function loadModuleFile(event) { if (isPlaying) { stopMusic(); } if (currentModulePtr !== 0) { // Clean up the previous module try { libopenmpt._openmpt_module_destroy(currentModulePtr); } catch(e) { console.error(e); } currentModulePtr = 0; } rowBuffer = {}; // Clear pattern cache const file = event.target.files[0]; if (!file) return; statusEl.textContent = `Loading "${file.name}"...`; const reader = new FileReader(); reader.onload = (e) => { // --- THIS IS THE NEXT EXPECTED ERROR --- // Your custom 'libopenmptjs.js' file does not export the C API // functions like '_openmpt_module_create_from_memory2'. // It only exports your C++ 'ModulePlayer' class. // // To fix this, you must re-compile 'libopenmptjs.js' // and *also* export the C API functions. // // You will see an error in the console like: // "TypeError: libopenmpt._openmpt_module_create_from_memory2 is not a function" // // This is not a bug in the HTML, it's a problem with the // compiled .js file. It doesn't contain the functions // this player app needs to work. try { const fileData = new Uint8Array(e.target.result); // 1. Allocate memory in the Emscripten heap const bufferPtr = libopenmpt._malloc(fileData.length); // 2. Copy the file data into the heap // Check for HEAPU8 existence, otherwise fallback to HEAP8 const heap = libopenmpt.HEAPU8 || libopenmpt.HEAP8; if (!heap) throw new Error("Emscripten heap is not available."); heap.set(fileData, bufferPtr); // 3. Load the module from memory. We use create2 to get error info. // We'll just log errors to the console for this demo. currentModulePtr = libopenmpt._openmpt_module_create_from_memory2( bufferPtr, fileData.length, 0, 0, // logfunc, loguser 0, 0, // errfunc, erruser 0, 0, // error, error_message 0 // ctls ); // 4. Free the heap memory libopenmpt._free(bufferPtr); if (currentModulePtr === 0) { statusEl.textContent = `Error loading "${file.name}". See console for details.`; playButtonEl.disabled = true; stopButtonEl.disabled = true; return; } // --- Load Metadata --- // 1. Convert JS string "title" to a C string pointer const titleKeyPtr = libopenmpt.stringToUTF8("title"); // 2. Call the function const titleValuePtr = libopenmpt._openmpt_module_get_metadata(currentModulePtr, titleKeyPtr); // 3. Convert the C string *result* to a JS string titleEl.textContent = libopenmpt.UTF8ToString(titleValuePtr); // 4. Free *both* pointers libopenmpt._free(titleKeyPtr); libopenmpt._openmpt_free_string(titleValuePtr); // IMPORTANT: Free strings! statusEl.textContent = `Loaded "${file.name}". Ready to play.`; playButtonEl.disabled = false; stopButtonEl.disabled = false; // Pre-cache the pattern data for faster UI updates preCachePatternData(); } catch (e) { console.error("Failed to load module:", e); statusEl.textContent = "Error: Failed to load module. See console."; if (e.name === "TypeError") { statusEl.textContent = "Error: Your libopenmptjs.js file is missing the C API functions."; } } }; reader.readAsArrayBuffer(file); } // --- Audio Playback --- function playMusic() { if (isPlaying || currentModulePtr === 0) return; try { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: sampleRate }); } // Resume context if it was suspended (autoplay policy) if(audioContext.state === 'suspended') { audioContext.resume(); } scriptNode = audioContext.createScriptProcessor(bufferSize, 0, 2); // 0 input, 2 output channels // Allocate persistent heap buffers for audio const leftBufferPtr = libopenmpt._malloc(bufferSize * 4); // 4 bytes per float const rightBufferPtr = libopenmpt._malloc(bufferSize * 4); scriptNode.onaudioprocess = (e) => { try { const leftOutput = e.outputBuffer.getChannelData(0); const rightOutput = e.outputBuffer.getChannelData(1); // Render audio frames const frames = libopenmpt._openmpt_module_read_float_stereo( currentModulePtr, sampleRate, bufferSize, leftBufferPtr, rightBufferPtr ); if (frames === 0) { // Song has ended // We must do this asynchronously to avoid deadlocking the audio thread setTimeout(stopMusic, 0); return; } // Copy audio from heap to the Web Audio buffers // We must create a new view of the heap *inside* the callback // Check for HEAPF32 existence const heapF32 = libopenmpt.HEAPF32; if (!heapF32) return; // Critical failure const leftHeap = new Float32Array(heapF32.buffer, leftBufferPtr, frames); const rightHeap = new Float32Array(heapF32.buffer, rightBufferPtr, frames); leftOutput.set(leftHeap); rightOutput.set(rightHeap); } catch (audioErr) { console.error("Error in audio process:", audioErr); setTimeout(stopMusic, 0); } }; scriptNode.connect(audioContext.destination); isPlaying = true; playButtonEl.disabled = true; stopButtonEl.disabled = false; // Start the UI update loop uiUpdateHandle = requestAnimationFrame(updateUI); } catch(e) { console.error("Failed to start music:", e); statusEl.textContent = "Error: Failed to start playback. See console."; } } function stopMusic() { if (!isPlaying) return; if (scriptNode) { scriptNode.disconnect(); scriptNode = null; } isPlaying = false; playButtonEl.disabled = false; stopButtonEl.disabled = true; cancelAnimationFrame(uiUpdateHandle); try { // Reset position to start if (currentModulePtr !== 0) { libopenmpt._openmpt_module_set_position_order_row(currentModulePtr, 0, 0); } } catch(e) { console.error(e); } // Clear UI orderEl.textContent = "--"; rowEl.textContent = "--"; bpmEl.textContent = "--"; patternDisplayEl.textContent = "... Stopped ..."; } // --- UI Update Loop --- function updateUI() { if (!isPlaying) return; try { // Get current playback info const order = libopenmpt._openmpt_module_get_current_order(currentModulePtr); const row = libopenmpt._openmpt_module_get_current_row(currentModulePtr); const bpm = libopenmpt._openmpt_module_get_current_estimated_bpm(currentModulePtr); // Update info display orderEl.textContent = String(order).padStart(2, '0'); rowEl.textContent = String(row).padStart(2, '0'); bpmEl.textContent = Math.round(bpm); // --- Render Pattern Display --- const currentPattern = libopenmpt._openmpt_module_get_order_pattern(currentModulePtr, order); const numRows = libopenmpt._openmpt_module_get_pattern_num_rows(currentModulePtr, currentPattern); let patternHtml = ""; const contextRows = 8; // Show 8 rows before and after for (let r = row - contextRows; r <= row + contextRows; r++) { if (r < 0 || r >= numRows) { patternHtml += "\n"; // Empty line for padding continue; } let line = (r === row) ? "> " : " "; // Show cursor line += String(r).padStart(3, '0') + " |"; // Get the pre-cached row data const rowKey = `${order}-${r}`; if (rowBuffer[rowKey]) { line += rowBuffer[rowKey]; } patternHtml += line + "\n"; } patternDisplayEl.textContent = patternHtml; } catch(e) { console.error("Error in UI update:", e); } // Loop uiUpdateHandle = requestAnimationFrame(updateUI); } // --- Pattern Caching --- // This function builds a text representation of the entire module // so that the UI update function can run quickly. function preCachePatternData() { if (currentModulePtr === 0) return; statusEl.textContent = "Caching pattern data..."; rowBuffer = {}; // Clear old cache try { const numOrders = libopenmpt._openmpt_module_get_num_orders(currentModulePtr); const numChannels = libopenmpt._openmpt_module_get_num_channels(currentModulePtr); // This can take a moment, so we do it asynchronously setTimeout(() => { for (let o = 0; o < numOrders; o++) { const pattern = libopenmpt._openmpt_module_get_order_pattern(currentModulePtr, o); if (pattern >= libopenmpt._openmpt_module_get_num_patterns(currentModulePtr)) { continue; // Skip invalid patterns } const numRows = libopenmpt._openmpt_module_get_pattern_num_rows(currentModulePtr, pattern); for (let r = 0; r < numRows; r++) { let line = ""; for (let c = 0; c < numChannels; c++) { // Get the formatted string for the *entire* channel cell const commandPtr = libopenmpt._openmpt_module_format_pattern_row_channel( currentModulePtr, pattern, r, c, 12, // Width (e.g., C-5 01 000) 1 // Padded ); const commandStr = libopenmpt.UTF8ToString(commandPtr); libopenmpt._openmpt_free_string(commandPtr); // Free the string! line += " " + commandStr + " |"; } rowBuffer[`${o}-${r}`] = line; // Store in cache } } statusEl.textContent = `Loaded "${titleEl.textContent}". Ready to play.`; console.log("Pattern data cached."); }, 50); // 50ms delay to allow UI to update } catch (e) { console.error("Failed to cache pattern data:", e); statusEl.textContent = "Error: Failed to cache patterns. See console."; } } </script> <!-- FIX: Load YOUR custom-built, working script from your GCS bucket. This script is known to be compatible with the environment's security policy. --> <script src="https://wasm.noahcohn.com/libmpt/libopenmptjs.js"></script> </body> </html>