Greasy Fork is available in English.
Bypass all limits natively! Force 128 bars, 10 octaves, custom synths, dark mode, and local JSON loading/exporting.
// ==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.");
}
});
});
})();