CML Song Maker+

Bypass all limits natively! Force 128 bars, 10 octaves, custom synths, dark mode, and local JSON loading/exporting.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

Advertisement:

// ==UserScript==
// @name         CML Song Maker+
// @namespace    http://tampermonkey.net/
// @version      3.12
// @description  Bypass all limits natively! Force 128 bars, 10 octaves, custom synths, dark mode, and local JSON loading/exporting.
// @author       DominumNetwork
// @match        *://musiclab.chromeexperiments.com/Song-Maker/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    window.__CML_INTERCEPTED_DATA = null;

    // ==========================================
    // 1. OMNI-INTERCEPTORS (Catches State Instantly)
    // ==========================================

    function getSafeData(data) {
        const defaults = {
            bars: 4, beats: 4, subdivision: 4, octaves: 2, scale: "chromatic",
            rootNote: 48, rootPitch: 0, rootOctave: 4, tempo: 120, notes: [], percussionNotes: []
        };
        const res = { ...defaults, ...data };
        if (data.options) res.options = { ...defaults, ...data.options };

        // Ensure arrays
        const ensureArray = (obj, key) => { if (!Array.isArray(obj[key])) obj[key] = []; };
        ensureArray(res, 'notes');
        ensureArray(res, 'percussionNotes');
        if (res.options) {
            ensureArray(res.options, 'notes');
            ensureArray(res.options, 'percussionNotes');
        }
        return res;
    }

    function getCurrentSongId() {
        const match = window.location.href.match(/song\/([^/?#]+)/);
        return match ? match[1] : null;
    }

    // Aggressive fallback capture
    const _stringify = JSON.stringify;
    JSON.stringify = function(val, replacer, space) {
        const res = _stringify.call(this, val, replacer, space);
        if (typeof res === 'string' && res.length > 50 && (res.includes('"notes"') || res.includes('"tempo"'))) {
            window.__CML_INTERCEPTED_DATA = res;
            window.__CML_GOT_DATA = true;
        }
        return res;
    };

    function verifyAndSave(str) {
        if (!str || typeof str !== 'string') return;
        if (str.includes('"tempo"') && (str.includes('"bars"') || str.includes('"notes"'))) {
            try {
                JSON.parse(str);
                window.__CML_INTERCEPTED_DATA = str;
                window.__CML_GOT_DATA = true;
            } catch(e) {}
        }
    }

    const _send = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function(body) {
        this.__requestBody = body;

        try {
            if (typeof body === 'string') {
                verifyAndSave(body);
            } else if (typeof FormData !== 'undefined' && body instanceof FormData) {
                for (let pair of body.entries()) {
                    if (typeof pair[1] === 'string') {
                        verifyAndSave(pair[1]);
                    } else if (pair[1] instanceof Blob) {
                        pair[1].text().then(t => verifyAndSave(t));
                    }
                }
            } else if (body instanceof Blob || body instanceof File) {
                const r = new FileReader();
                r.onload = () => {
                    if (typeof r.result === 'string') verifyAndSave(r.result);
                };
                r.readAsText(body);
            } else if (body instanceof Uint8Array) {
                verifyAndSave(new TextDecoder().decode(body));
            }
        } catch(e) {}

        return _send.apply(this, arguments);
    };

    const _fetch = window.fetch;
    window.fetch = async function(...args) {
        try {
            const url = (typeof args[0] === 'string') ? args[0] : (args[0] && args[0].url);

            // XHR/Fetch mock router for injected generic IDs
            const maybeId = getCurrentSongId();
            if (maybeId && url.includes('/data/')) {
                console.log("CML-MOD: Intercepted FETCH for ID:", maybeId);
                const modData = localStorage.getItem("CML_INJECT_" + maybeId);
                if (modData) {
                    console.log("CML-MOD: Found localStorage data for injection.");
                    verifyAndSave(modData);
                    return new Response(modData, {
                        status: 200,
                        headers: { 'Content-Type': 'application/json' }
                    });
                }
            }

            // Fallback for older MOD_LOAD_ code paths
            if (url && typeof url === 'string' && (url.includes('MOD_LOAD_') || url.includes('MODLOAD') || url.includes('999999999'))) {
                const parts = url.split('/');
                const id = parts.find(p => p.startsWith('MOD_LOAD_') || p.startsWith('MODLOAD') || p.startsWith('999999999'));
                if (id) {
                    const rawId = id.split('?')[0];
                    console.log("CML-MOD: Intercepted FETCH for", rawId);
                    const data = localStorage.getItem(rawId);
                    if (data) {
                        verifyAndSave(data);
                        return new Response(data, {
                            status: 200,
                            headers: { 'Content-Type': 'application/json' }
                        });
                    }
                }
            }

            // Sniff outgoing POST bodies
            const opts = args[1];
            if (opts && opts.method && opts.method.toUpperCase() === 'POST' && opts.body) {
                if (typeof opts.body === 'string') {
                    verifyAndSave(opts.body);
                } else if (typeof FormData !== 'undefined' && opts.body instanceof FormData) {
                    for (let pair of opts.body.entries()) {
                        if (typeof pair[1] === 'string') verifyAndSave(pair[1]);
                        else if (pair[1] instanceof Blob) pair[1].text().then(t => verifyAndSave(t));
                    }
                } else if (opts.body instanceof Blob) {
                    opts.body.text().then(t => verifyAndSave(t));
                } else if (opts.body instanceof Uint8Array) {
                    verifyAndSave(new TextDecoder().decode(opts.body));
                }
            }
        } catch(e) {}

        // Execute original fetch and intercept its response to grab loaded songs
        return _fetch.apply(this, args).then(response => {
            try {
                const url = (typeof args[0] === 'string') ? args[0] : (args[0] && args[0].url);
                if (url && url.includes('/data/')) {
                    const clonedRes = response.clone();
                    clonedRes.text().then(text => verifyAndSave(text)).catch(e=>{});
                }
            } catch(e) {}
            return response;
        });
    };

    // Override XHR response to inject custom JSON payload
    const originalRText = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText');
    if (originalRText) {
        Object.defineProperty(XMLHttpRequest.prototype, 'responseText', {
            get: function() {
                if (this.__mod_url) {
                    const maybeId = getCurrentSongId();
                    if (maybeId && this.__mod_url.includes('/data/')) {
                        const modData = localStorage.getItem("CML_INJECT_" + maybeId);
                        if (modData) {
                            console.log("CML-MOD: Intercepted XHR for ID:", maybeId);
                            verifyAndSave(modData);
                            return modData;
                        }
                    }
                }

                if (this.__mod_url && (this.__mod_url.includes('MOD_LOAD_') || this.__mod_url.includes('999999999'))) {
                    const parts = this.__mod_url.split('/');
                    const id = parts.find(p => p.startsWith('MOD_LOAD_') || p.startsWith('999999999'));
                    if (id) {
                        const rawId = id.split('?')[0];
                        console.log("CML-MOD: Intercepted XHR for", rawId);
                        const saved = localStorage.getItem(rawId);
                        if (saved) {
                            verifyAndSave(saved);
                            return saved;
                        }
                    }
                }
                return originalRText.get.call(this);
            }
        });
    }

    const _open = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        this.__mod_url = url;

        this.addEventListener('load', function() {
            // Also grab it when the save request completes just in case
            if (method.toUpperCase() === 'POST' && this.__requestBody) {
                 if (typeof this.__requestBody === 'string') {
                     verifyAndSave(this.__requestBody);
                 }
            } else if (method.toUpperCase() === 'GET' && this.__mod_url && this.__mod_url.includes('/data/')) {
                 if (typeof this.responseText === 'string') {
                     verifyAndSave(this.responseText);
                 }
            }
        });

        _open.apply(this, arguments);
    };

    // ==========================================
    // 2. SYNTH OVERRIDES
    // ==========================================
    window.__CML_CUSTOM_OSC_TYPE = "";
    const oStart = (window.OscillatorNode || window.webkitOscillatorNode).prototype.start;
    (window.OscillatorNode || window.webkitOscillatorNode).prototype.start = function(...args) {
        if (window.__CML_CUSTOM_OSC_TYPE) {
            try { this.type = window.__CML_CUSTOM_OSC_TYPE; } catch(e){}
        }
        oStart.apply(this, args);
    };

    // ==========================================
    // 3. UI INJECTION & MOD MENU
    // ==========================================
    window.addEventListener('load', () => {
        // Build style container
        const gui = document.createElement('div');
        gui.id = 'cml-mod-gui';

        gui.innerHTML = `
            <div id="cml-mod-header" style="background:linear-gradient(90deg, #14b8a6, #d946ef); color:#fff; padding:12px; cursor:move; font-weight:bold; display:flex; justify-content:space-between; align-items:center; border-radius:12px 12px 0 0; user-select: none;">
                <span style="letter-spacing:1px; text-shadow:0 1px 2px rgba(0,0,0,0.5);">CML PLUS MENU</span>
                <button id="cml-mod-toggle" style="background:transparent; border:none; color:#fff; font-weight:bold; cursor:pointer; font-size:18px;">─</button>
            </div>
            <div id="cml-mod-body" style="padding:16px; background:rgba(9, 9, 11, 0.95); backdrop-filter:blur(8px); border:1px solid #27272a; border-top:none; border-radius:0 0 12px 12px;">

                <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-bottom:1px solid #27272a; padding-bottom:10px;">
                    <label style="color:#e4e4e7; font-size:14px; font-weight:600; cursor:pointer; display:flex; align-items:center; gap:8px;">
                        <input type="checkbox" id="cml-mod-dark" style="width:16px; height:16px; accent-color:#14b8a6;">
                        Dark Mode
                    </label>
                </div>

                <div style="margin-bottom: 15px;">
                    <label style="font-size:13px; color:#14b8a6; font-weight:bold; margin-bottom:6px; display:block;">🎹 Synth Waveform Override:</label>
                    <select id="cml-mod-synth" style="width:100%; padding:8px; background:#18181b; color:#14b8a6; border:1px solid #27272a; border-radius:6px; outline:none; font-family:inherit; cursor:pointer;">
                        <option value="">Default (No Override)</option>
                        <option value="sawtooth">🎸 Sawtooth (Aggressive/Lead)</option>
                        <option value="square">👾 Square (Chiptune 8-bit)</option>
                        <option value="triangle">☁️ Triangle (Soft Pad)</option>
                        <option value="sine">🌊 Sine (Pure/Sub)</option>
                    </select>
                </div>

                <div style="margin-bottom: 15px; background:#18181b; border:1px solid #27272a; border-radius:8px; padding:12px;">
                    <label style="font-size:13px; color:#d946ef; font-weight:bold; margin-bottom:8px; display:block;">🚀 Force Limit Injector</label>
                    <p style="font-size:11px; color:#a1a1aa; margin-bottom:10px; line-height:1.4;">Bypass internal limits seamlessly. Click the normal Save button first, then click these!</p>

                    <div style="display:grid; grid-template-columns:1fr 1fr; gap:6px;">
                        <button class="limit-btn" data-key="bars" data-val="64" style="background:#27272a; color:#fff; border:none; padding:6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold; transition:0.2s;">64 Bars</button>
                        <button class="limit-btn" data-key="bars" data-val="128" style="background:#27272a; color:#fff; border:none; padding:6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold; transition:0.2s;">128 Bars</button>
                        <button class="limit-btn" data-key="octaves" data-val="5" style="background:#27272a; color:#fff; border:none; padding:6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold; transition:0.2s;">5 Octaves</button>
                        <button class="limit-btn" data-key="octaves" data-val="10" style="background:#27272a; color:#fff; border:none; padding:6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold; transition:0.2s;">10 Octaves</button>
                    </div>

                    <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #3f3f46;">
                        <p style="font-size:11px; color:#d946ef; margin-bottom:8px; font-weight:bold;">Custom Values</p>
                        <div style="display:flex; gap:6px; margin-bottom:6px;">
                            <input type="number" id="cml-custom-val-bars" placeholder="Len (Bars)" style="flex:1; width:50%; background:#18181b; color:#fff; border:1px solid #3f3f46; border-radius:4px; padding:4px 6px; font-size:11px;" min="1" max="500">
                            <button class="custom-val-btn" data-key="bars" style="flex:1; background:#14b8a6; color:#000; border:none; padding:4px 6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold;">Set Bars</button>
                        </div>
                        <div style="display:flex; gap:6px; margin-bottom:6px;">
                            <input type="number" id="cml-custom-val-octaves" placeholder="Octaves" style="flex:1; width:50%; background:#18181b; color:#fff; border:1px solid #3f3f46; border-radius:4px; padding:4px 6px; font-size:11px;" min="1" max="24">
                            <button class="custom-val-btn" data-key="octaves" style="flex:1; background:#14b8a6; color:#000; border:none; padding:4px 6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold;">Set Octaves</button>
                        </div>
                        <div style="display:flex; gap:6px;">
                            <input type="number" id="cml-custom-val-rootOctave" placeholder="Start Octave (e.g. 2)" title="Shifts all octaves down or up. Default is 4." style="flex:1; width:50%; background:#18181b; color:#fff; border:1px solid #3f3f46; border-radius:4px; padding:4px 6px; font-size:11px;" min="0" max="8">
                            <button class="custom-val-btn" data-key="rootOctave" style="flex:1; background:#14b8a6; color:#000; border:none; padding:4px 6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold;">Set Start Oct.</button>
                        </div>
                        <div style="display:flex; gap:6px; margin-top:6px;">
                            <input type="number" id="cml-custom-val-tempo" placeholder="Tempo (e.g. 500)" title="Can go beyond 240 or below 40!" style="flex:1; width:50%; background:#18181b; color:#fff; border:1px solid #3f3f46; border-radius:4px; padding:4px 6px; font-size:11px;">
                            <button class="custom-val-btn" data-key="tempo" style="flex:1; background:#14b8a6; color:#000; border:none; padding:4px 6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold;">Set Tempo</button>
                        </div>
                        <div style="margin-top:6px;">
                            <button id="cml-mod-unlock-tempo" style="width:100%; background:#71717a; color:#fff; border:none; padding:6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold; transition:0.2s;">🔓 Unlock Live Tempo Limits (1-3000)</button>
                        </div>
                    </div>
                    <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #3f3f46;">
                        <p style="font-size:11px; color:#10b981; margin-bottom:8px; font-weight:bold;">Import MIDI / MP3</p>
                        <input type="file" id="cml-mod-audio-upload" accept=".mid,.midi,.mp3,.wav" style="font-size:11px; background:#18181b; color:#fff; width:100%; border:1px solid #3f3f46; border-radius:4px; padding:4px;" />
                        <button id="cml-mod-audio-process" style="width:100%; background:#10b981; color:#000; border:none; padding:6px; border-radius:4px; font-size:11px; cursor:pointer; font-weight:bold; margin-top:6px;">Process File to Song</button>
                        <p style="font-size:10px; color:#a1a1aa; margin-top:4px; line-height: 1.2;">MIDI perfectly supported. MP3 tries onset detection (experimental). Replaces current song.</p>
                    </div>
                </div>

                <div style="display:flex; gap:8px;">
                    <button id="cml-mod-export" style="flex:1; background:#14b8a6; color:#000; border:none; border-radius:6px; padding:10px; cursor:pointer; font-weight:bold; transition:all 0.2s;">
                        💾 Export JSON
                    </button>
                    <button id="cml-mod-import" style="flex:1; background:#d946ef; color:#fff; border:none; border-radius:6px; padding:10px; cursor:pointer; font-weight:bold; transition:all 0.2s;">
                        📂 Load JSON
                    </button>
                </div>
            </div>
        `;

        Object.assign(gui.style, {
            position: 'fixed',
            top: '20px',
            right: '20px',
            width: '300px',
            zIndex: '999999',
            fontFamily: '"Segoe UI", system-ui, sans-serif',
            boxShadow: '0 10px 30px rgba(0,0,0,0.5)',
            borderRadius: '12px',
            transition: 'opacity 0.2s'
        });

        document.body.appendChild(gui);

        // -- Draggable Logic --
        const header = document.getElementById('cml-mod-header');
        let isDragging = false, offsetX = 0, offsetY = 0;

        header.addEventListener('mousedown', (e) => {
            if (e.target.id === 'cml-mod-toggle') return;
            isDragging = true;
            offsetX = e.clientX - gui.getBoundingClientRect().left;
            offsetY = e.clientY - gui.getBoundingClientRect().top;
            gui.style.opacity = '0.8';
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const x = e.clientX - offsetX;
            const y = e.clientY - offsetY;
            gui.style.left = Math.max(0, x) + 'px';
            gui.style.top = Math.max(0, y) + 'px';
            gui.style.right = 'auto';
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                gui.style.opacity = '1';
            }
        });

        // -- Minimizable Logic --
        const toggleBtn = document.getElementById('cml-mod-toggle');
        const bodyWrap = document.getElementById('cml-mod-body');
        let collapsed = false;
        toggleBtn.addEventListener('click', () => {
            collapsed = !collapsed;
            bodyWrap.style.display = collapsed ? 'none' : 'block';
            toggleBtn.textContent = collapsed ? '+' : '─';
        });

        // -- Button Hovers --
        document.querySelectorAll('.limit-btn').forEach(b => {
             b.onmouseenter = () => b.style.background = '#3f3f46';
             b.onmouseleave = () => b.style.background = '#27272a';
        });

        // -- Data Modifier Function --
        const injectAndReload = (key, val) => {
            if (!window.__CML_INTERCEPTED_DATA) {
                alert("⚠️ Please click the standard Chrome Music Lab SAVE button at the bottom right FIRST. Wait a second, then try this again.");
                return;
            }
            try {
                const parts = window.location.pathname.split('/').filter(p => p.trim() !== '');
                let originalId = getCurrentSongId() || "CML_LOCAL_TEST";

                let data = JSON.parse(window.__CML_INTERCEPTED_DATA);
                data = getSafeData(data);

                console.log("CML-MOD: Injecting", key, "with val", val, "into data:", data);

                // Update the data, checking both root and options
                if (key === 'rootOctave') {
                    let ro = parseInt(val);
                    data.rootOctave = ro;
                    data.rootNote = ro * 12 + (data.rootPitch || 0);
                    if (data.options) {
                        data.options.rootOctave = ro;
                        data.options.rootNote = ro * 12 + (data.options.rootPitch || 0);
                    }
                } else {
                    data[key] = parseInt(val);
                    if (data.options) {
                        data.options[key] = parseInt(val);
                    }
                }

                console.log("CML-MOD: New data:", data);

                const strData = JSON.stringify(data);
                localStorage.setItem("CML_INJECT_" + originalId, strData);

                alert("✅ Injected! Reloading the grid with new injected limits...");
                window.location.reload();
            } catch(e) {
                console.error(e);
                alert("❌ Injection failed. Invalid grid data captured.");
            }
        };

        // -- UI Events --
        document.getElementById('cml-mod-dark').addEventListener('change', (e) => {
            const checked = e.target.checked;
            document.documentElement.style.filter = checked ? "invert(1) hue-rotate(180deg)" : "none";
            document.body.style.background = checked ? "#111" : "";
        });

        document.getElementById('cml-mod-synth').addEventListener('change', (e) => {
            window.__CML_CUSTOM_OSC_TYPE = e.target.value;
        });

        document.getElementById('cml-mod-unlock-tempo').addEventListener('click', () => {
            const inputNum = document.querySelector('input.input-number[name="tempo"]');
            const inputRange = document.querySelector('#tempo-slider input[type="range"]');
            if (inputNum) {
                inputNum.min = 1;
                inputNum.max = 3000;
            }
            if (inputRange) {
                inputRange.min = 1;
                inputRange.max = 3000;
            }
            if (inputNum || inputRange) {
                alert("🔓 UI Tempo limits removed! You can drag the slider or type freely (1 to 3000)\n\nNote: Visual bugs on the slider UI might occur, but the engine handles it.");
            } else {
                alert("⚠️ Couldn't find the tempo sliders on the page. Are you fully loaded?");
            }
        });

        document.querySelectorAll('.limit-btn').forEach(btn => {
             btn.addEventListener('click', (e) => {
                 injectAndReload(e.target.dataset.key, e.target.dataset.val);
             });
        });

        document.querySelectorAll('.custom-val-btn').forEach(btn => {
             btn.addEventListener('click', (e) => {
                 const key = e.target.dataset.key;
                 const inputEl = document.getElementById('cml-custom-val-' + key);
                 const val = inputEl.value;
                 if (val && !isNaN(val)) {
                     injectAndReload(key, val);
                 } else {
                     alert("⚠️ Please enter a valid number");
                 }
             });
        });

        document.getElementById('cml-mod-audio-process').addEventListener('click', async () => {
            const fileInput = document.getElementById('cml-mod-audio-upload');
            if (!fileInput.files.length) {
                alert("⚠️ Please select a file first.");
                return;
            }
            const file = fileInput.files[0];

            // Load Tonejs/Midi dynamically if needed
            if (!window.Midi) {
                await new Promise((resolve, reject) => {
                    const script = document.createElement('script');
                    script.src = "https://unpkg.com/@tonejs/midi";
                    script.onload = resolve;
                    script.onerror = reject;
                    document.head.appendChild(script);
                });
            }

            try {
                const arrayBuffer = await file.arrayBuffer();

                if (file.name.toLowerCase().endsWith('.mid') || file.name.toLowerCase().endsWith('.midi')) {
                    // --- MIDI PROCESSING ---
                    console.log("CML-MOD: Processing MIDI...");
                    const midi = new window.Midi(arrayBuffer);

                    if (midi.tracks.length === 0) {
                        alert("⚠️ No MIDI tracks found.");
                        console.log("CML-MOD: No MIDI tracks.");
                        return;
                    }
                    console.log("CML-MOD: MIDI tracks found:", midi.tracks.length);

                    let intercepted = window.__CML_INTERCEPTED_DATA ? getSafeData(JSON.parse(window.__CML_INTERCEPTED_DATA)) : getSafeData({});

                    let targetData = intercepted.options || intercepted;
                    targetData.scale = "chromatic";

                    const PPQ = midi.header.ppq || 480;
                    let lowestNote = 255;
                    let highestNote = 0;
                    let maxTicks = 0;

                    midi.tracks.forEach(track => {
                        track.notes.forEach(note => {
                            if (note.midi < lowestNote) lowestNote = note.midi;
                            if (note.midi > highestNote) highestNote = note.midi;
                            const endTick = note.ticks + note.durationTicks;
                            if (endTick > maxTicks) maxTicks = endTick;
                        });
                    });

                    if (lowestNote === 255) { lowestNote = 48; highestNote = 72; }

                    let rootMidi = lowestNote;
                    targetData.rootOctave = Math.floor(rootMidi / 12);
                    targetData.rootPitch = rootMidi % 12;
                    targetData.rootNote = rootMidi;

                    let octaveSpan = Math.ceil((highestNote - lowestNote + 1) / 12);
                    if (octaveSpan < 1) octaveSpan = 1;
                    targetData.octaves = octaveSpan;

                    const originalTempo = midi.header.tempos.length > 0 ? midi.header.tempos[0].bpm : 120;
                    targetData.tempo = Math.round(originalTempo);

                    let ticksPerBeat = PPQ;
                    let totalBeats = maxTicks / ticksPerBeat;
                    targetData.beats = 4;
                    let reqBars = Math.ceil(totalBeats / 4);
                    if (reqBars < 1) reqBars = 1;
                    targetData.bars = reqBars;

                    targetData.subdivision = 4;
                    const cmlNotes = [];
                    const ticksPerSubdivision = ticksPerBeat / targetData.subdivision;

                    midi.tracks.forEach(track => {
                        track.notes.forEach(note => {
                            const pitchIndex = note.midi - rootMidi;
                            const timeIndex = Math.round(note.ticks / ticksPerSubdivision);
                            cmlNotes.push(pitchIndex);
                            cmlNotes.push(timeIndex);
                        });
                    });

                    targetData.notes = cmlNotes;

                    const percussionTrack = midi.tracks.find(t => t.channel === 9);
                    if (percussionTrack) {
                        const cmlPerc = [];
                        percussionTrack.notes.forEach(note => {
                            const percIndex = note.midi < 40 ? 0 : 1;
                            const timeIndex = Math.round(note.ticks / ticksPerSubdivision);
                            cmlPerc.push(percIndex);
                            cmlPerc.push(timeIndex);
                        });
                        if (intercepted.options) {
                            intercepted.options.percussionNotes = cmlPerc;
                        } else {
                            intercepted.percussionNotes = cmlPerc;
                        }
                    }

                    finishInjection(intercepted, targetData);

                } else {
                    // --- AUDIO PROCESSING (MP3/WAV) ---
                    console.log("CML-MOD: Processing AUDIO...");
                    alert("⏳ Processing MP3/WAV. This is experimental and does monophonic pitch detection. Please wait...");
                    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                    console.log("CML-MOD: AudioContext created. Decoding...");
                    const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
                    console.log("CML-MOD: Audio decoded.");

                    // Basic parameters
                    const durationInSeconds = audioBuffer.duration;
                    const channelData = audioBuffer.getChannelData(0);
                    const sampleRate = audioBuffer.sampleRate;

                    // Very naive tempo guess = 120
                    const bpm = 120;
                    const beatsPerSecond = bpm / 60;
                    const totalBeats = Math.ceil(durationInSeconds * beatsPerSecond);

                    let intercepted = window.__CML_INTERCEPTED_DATA ? JSON.parse(window.__CML_INTERCEPTED_DATA) : {
                        bars: 4, beats: 4, subdivision: 4, octaves: 2, scale: "chromatic",
                        rootNote: 48, rootPitch: 0, rootOctave: 4, instrument: "marimba",
                        percussion: "electronic", percussionNotes: 2, tempo: bpm, notes: []
                    };

                    let targetData = intercepted.options || intercepted;
                    targetData.scale = "chromatic";
                    targetData.tempo = bpm;
                    targetData.subdivision = 4;
                    targetData.beats = 4;
                    targetData.bars = Math.max(1, Math.ceil(totalBeats / 4));

                    const cmlNotes = [];
                    const timePerSubdiv = 60 / bpm / targetData.subdivision;
                    const samplesPerSubdiv = Math.floor(timePerSubdiv * sampleRate);

                    let minNote = 1000, maxNote = -1;

                    // Simple Yin/auto-correlation for pitch detection per subdivision
                    for (let i = 0; i < targetData.bars * targetData.beats * targetData.subdivision; i++) {
                        let offset = i * samplesPerSubdiv;
                        if (offset + samplesPerSubdiv > channelData.length) break;

                        let chunk = channelData.slice(offset, offset + samplesPerSubdiv);

                        // RMS Energy to ignore silence
                        let rms = 0;
                        for(let j=0; j<chunk.length; j++) rms += chunk[j]*chunk[j];
                        rms = Math.sqrt(rms / chunk.length);

                        if (rms > 0.05) { // Threshold
                            // Autocorrelation to find fundamental freq
                            let bestLag = 0;
                            let bestCorr = 0;
                            // Search freq from 60Hz to 1000Hz -> lags from sampleRate/1000 to sampleRate/60
                            let minLag = Math.floor(sampleRate / 1000);
                            let maxLag = Math.floor(sampleRate / 60);
                            for (let lag = minLag; lag < maxLag; lag++) {
                                let corr = 0;
                                for (let j = 0; j < chunk.length - lag; j++) {
                                    corr += chunk[j] * chunk[j + lag];
                                }
                                if (corr > bestCorr) {
                                    bestCorr = corr;
                                    bestLag = lag;
                                }
                            }
                            if (bestLag > 0 && (bestCorr / rms) > 10) { // arbitrary confidence
                                let freq = sampleRate / bestLag;
                                let midiNote = Math.round(69 + 12 * Math.log2(freq / 440));
                                if (midiNote >= 21 && midiNote <= 108) {
                                    cmlNotes.push({ midi: midiNote, time: i });
                                    if (midiNote < minNote) minNote = midiNote;
                                    if (midiNote > maxNote) maxNote = midiNote;
                                }
                            }
                        }
                    }

                    if (cmlNotes.length === 0) {
                        alert("⚠️ Audio detected as silent or couldn't find pitches.");
                        return;
                    }

                    targetData.rootOctave = Math.floor(minNote / 12);
                    targetData.rootPitch = minNote % 12;
                    targetData.rootNote = minNote;
                    targetData.octaves = Math.max(1, Math.ceil((maxNote - minNote + 1) / 12));

                    const finalNotes = [];
                    for(let n of cmlNotes) {
                        finalNotes.push(n.midi - minNote); // pitch index
                        finalNotes.push(n.time);           // time index
                    }

                    targetData.notes = finalNotes;
                    finishInjection(intercepted, targetData);
                }

                function finishInjection(intercepted, targetData) {
                    console.log("CML-MOD: finishInjection called.");
                    let finalJson = intercepted;
                    if (window.__CML_INTERCEPTED_DATA && finalJson.options) {
                        finalJson.options = targetData;
                    } else {
                        finalJson = targetData;
                    }
                    const jsonStr = JSON.stringify(finalJson);
                    console.log("CML-MOD: Saving JSON to localStorage.");
                    verifyAndSave(jsonStr);

                    // Reload the page logic
                    const parts = window.location.pathname.split('/').filter(p => p.trim() !== '');
                    let originalId = parts[parts.length - 1];
                    if (!originalId || originalId === '' || originalId.toLowerCase() === 'song-maker' || originalId.toLowerCase() === 'song') {
                        originalId = "CML_LOCAL_TEST";
                        window.history.pushState({}, '', '/Song-Maker/song/' + originalId);
                    }

                    console.log("CML-MOD: Setting localStorage key for ID:", originalId);
                    localStorage.setItem("CML_INJECT_" + originalId, jsonStr);
                    alert("✅ Imported! Playing... It might need a reload. To properly save, click the official Save button in the UI.");
                    window.location.reload();
                }

            } catch (err) {
                console.error(err);
                alert("⚠️ Error processing file: " + err.message);
            }
        });
        document.getElementById('cml-mod-export').addEventListener('click', () => {
            if (window.__CML_INTERCEPTED_DATA) {
                navigator.clipboard.writeText(window.__CML_INTERCEPTED_DATA).then(() => {
                    alert("✅ Successfully copied raw song JSON to clipboard!");
                }).catch(() => {
                    const area = document.createElement("textarea");
                    area.value = window.__CML_INTERCEPTED_DATA;
                    document.body.appendChild(area);
                    area.select();
                    document.execCommand("copy");
                    document.body.removeChild(area);
                    alert("✅ Successfully copied raw song JSON to clipboard using fallback.");
                });
            } else {
                alert("⚠️ Intercept Empty: Click the main standard 'Save' checkmark button FIRST to capture your data, then click Export.");
            }
        });

        document.getElementById('cml-mod-import').addEventListener('click', () => {
            const pasted = prompt("Paste your exported JSON here:");
            if (pasted && (pasted.includes('"tempo"') || pasted.includes('"bars"'))) {
                let originalId = getCurrentSongId();
                if (!originalId) {
                    alert("⚠️ To load JSON, you first need to be on a saved song's page. (Click save and open the link)");
                    return;
                }
                localStorage.setItem("CML_INJECT_" + originalId, pasted);
                alert("✅ Loaded! Reloading...");
                window.location.reload();
            } else if (pasted) {
                alert("❌ Invalid JSON format.");
            }
        });

    });

})();