1/* 2 * musicshake_web â runs the original Lite3 "Make Music" page without Flash (2026-09-23). 3 * Loaded after the unchanged Lite3 scripts from /create/js/. It replaces only what needed Flash: 4 * __swf the four SWFs become JavaScript objects with the same method names, at the same element ids 5 * (Lite3 reaches them with document.getElementById(Lite3.id + "-swf-" + name)): 6 * bootstrap -> "Flash is ready"; mixer2 -> MusicshakeMixer2 (js/mixer2.js); 7 * preview -> plays one block through svc.php; encoder -> mixdown, MP3 and publish.php 8 * Lite3.bootstrap.run no Flash version check 9 * __xhr3 catalog JSONP over https (an https page can't load http scripts) 10 */ 11(function (window, $) { 12 "use strict"; 13 var SVC = '/musicshake_web/svc.php'; 14 15 // swfobject is only read for the Flash version (lite3-bootstrap.js check_browser) and the Quick Tour video 16 window.swfobject = window.swfobject || { 17 getFlashPlayerVersion: function () { return {major: 0, minor: 0, release: 0}; }, 18 hasFlashPlayerVersion: function () { return false; }, 19 embedSWF: function () {} 20 }; 21 22 // ---- stand-in "SWF" elements --------------------------------------------------------------------------- 23 function holder(name) { 24 var id = Lite3.id + '-swf-' + name; 25 $('#' + id).remove(); 26 var el = document.createElement('div'); 27 el.id = id; 28 el.style.display = 'none'; 29 document.body.appendChild(el); 30 return el; 31 } 32 33 function previewPlayer(el) { 34 var audio = new window.Audio(), timer = null; 35 audio.preload = 'auto'; 36 function stopped() { clearInterval(timer); timer = null; try { Lite3.preview.stopped(); } catch (e) {} } 37 audio.addEventListener('playing', function () { 38 try { Lite3.preview.starting(); } catch (e) {} 39 clearInterval(timer); 40 timer = setInterval(function () { 41 if (audio.duration) try { Lite3.preview.progress(Math.round(audio.currentTime / audio.duration * 100)); } catch (e) {} 42 }, 100); 43 }); 44 audio.addEventListener('ended', stopped); 45 audio.addEventListener('error', stopped); 46 el.preview_load = function (id, type) { 47 if (!/^[0-9]+$/.test(String(id)) || !/^[SDVRFE]$/.test(String(type))) return; 48 audio.src = SVC + '?do=sample&id=' + id + '&type=' + type; 49 var p = audio.play(); 50 if (p && p.catch) p.catch(stopped); 51 }; 52 el.preview_play = function () { var p = audio.play(); if (p && p.catch) p.catch(stopped); }; 53 el.preview_stop = function () { audio.pause(); stopped(); }; 54 } 55 56 // ---- encoder.swf: mixdown -> MP3 -> upload (publish) --------------------------------------------------- 57 // Same sequence and page callbacks as the SWF: Lite3.post.status_ready -> setparams(params) -> status_mixdown(%) 58 // -> status_encoding(%) -> status_upload -> done(song_num). Like encoder.swf the file ends with the text 59 // "musicshake" and an ID3v1 tag. Errors close the status window with a message (the SWF just hung). 60 var ID3_GENRE = {1: 7, 2: 17, 3: 14, 4: 3, 5: 52, 6: 13, 7: 116, 8: 8, 9: 86, 10: 10, 11: 42}; 61 function id3v1(title, artist, genre) { 62 var tag = new Uint8Array(128); 63 function put(text, at, len) { text = String(text || ''); for (var i = 0; i < len && i < text.length; i++) { var c = text.charCodeAt(i); tag[at + i] = c < 256 ? c : 63; } } 64 put('TAG', 0, 3); put(title, 3, 30); put(artist, 33, 30); put('Musicshake', 63, 30); 65 put(String(new Date().getFullYear()), 93, 4); put('License: CC BY-NC-SA 3.0', 97, 30); 66 tag[127] = ID3_GENRE[+genre] || 0; 67 return tag; 68 } 69 70 function encoder(el) { 71 var alive = true, worker = null, abort = window.AbortController ? new AbortController() : null; 72 function gone() { return !alive || !document.getElementById(el.id); } 73 function fail(message) { 74 alive = false; 75 if (worker) worker.terminate(); 76 $('#' + Lite3.id + '-post-status').remove(); 77 $('#mask').remove(); 78 __error(message || 'Posting failed. Please try again.'); 79 $('#' + el.id).remove(); 80 } 81 // Cancel in the status window unloads the "SWF": stop everything 82 new MutationObserver(function (m, obs) { if (!document.getElementById(el.id)) { alive = false; if (worker) worker.terminate(); if (abort) abort.abort(); obs.disconnect(); } }) 83 .observe(document.body, {childList: true}); 84 85 el.setparams = function (params) {
86 var msw = MusicshakeMixer2.data.serialize(), cb = function (name, arg) { try { Lite3.post[name](arg); } catch (e) {} }; 87 MusicshakeMixer2.mixdown(function (p) { if (!gone()) cb('status_mixdown', p); }).then(function (pcm) { 88 if (gone()) return; 89 return new Promise(function (resolve, reject) { 90 worker = new Worker(MusicshakeMixer2.mp3Worker || '/musicshake_web/js/mp3-worker.js'); 91 worker.onmessage = function (e) { 92 if (e.data.progress !== undefined) cb('status_encoding', e.data.progress); 93 else if (e.data.mp3) resolve(e.data.mp3); 94 else reject(new Error(e.data.error || 'encoding failed')); 95 }; 96 worker.onerror = function (e) { reject(new Error(e.message || 'encoding failed')); }; 97 worker.postMessage({left: pcm.left, right: pcm.right}, [pcm.left.buffer, pcm.right.buffer]); 98 }); 99 }).then(function (mp3) { 100 if (gone() || !mp3) return; 101 worker.terminate(); worker = null; 102 var mark = new TextEncoder().encode('musicshake'), tag = id3v1(params.title, params.user_id, params.genre); 103 var file = new Blob([mp3, mark, tag], {type: 'audio/mpeg'}); 104 cb('status_upload'); 105 var form = new FormData(); 106 ['title', 'description', 'genre', 'mood', 'theme', 'image', 'contest', 'allow_remix', 'show_instruments'].forEach(function (k) { 107 form.append(k, params[k] === undefined || params[k] === null ? '' : params[k]); 108 }); 109 ['tempo', 'key', 'fadeout', 'chords', 'tracks', 'blocks', 'effects', 'parent'].forEach(function (k) { form.append(k, msw[k]); }); 110 form.append('mp3', file, 'song.mp3'); 111 return fetch('/musicshake_web/publish.php', {method: 'POST', body: form, credentials: 'same-origin', signal: abort ? abort.signal : undefined}) 112 .then(function (r) { return r.json().catch(function () { return {error: 'Server error (' + r.status + ')'}; }); }) 113 .then(function (res) { 114 if (gone()) return; 115 if (res && res.song_num) { alive = false; cb('done', res.song_num); } 116 else fail(res && res.error); 117 }); 118 }).catch(function (err) { 119 if (!gone()) fail(err && err.name === 'AbortError' ? null : 'Posting failed: ' + (err && err.message || err)); 120 }); 121 }; 122 setTimeout(function () { if (!gone()) { try { Lite3.post.status_ready(); } catch (e) { fail(); } } }, 0); 123 } 124 125 window.__swf = { 126 required: 0, 127 load: function (swf) { 128 var el = holder(swf); 129 if (swf === 'bootstrap') { 130 setTimeout(function () { Lite3.bootstrap.ok(); }, 0); 131 } else if (swf === 'mixer2') { 132 var api = window.MusicshakeMixer2.create();
133 for (var k in api) el[k] = api[k]; 134 setTimeout(function () { Lite3.Mixer.event_connect(); }, 0); 135 } else if (swf === 'preview') { 136 previewPlayer(el); 137 } else if (swf === 'encoder') { 138 encoder(el); 139 } 140 }, 141 embed: function () {}, 142 unload: function (swf) { $('#' + Lite3.id + '-swf-' + swf).remove(); } 143 }; 144 145 // ---- use the whole page ---------------------------------------------------------------------------- 146 // Lite3 created the editor at a fixed 500x500 that could be moved (by its header) and resized (by its edges). 147 // Here it is fixed: it fills the page below the site's menu bar (in the browser) or the whole screen (installed 148 // app: no menu bar, see below) and follows the window's size and orientation. 149 var PAD = 42; // #lite3: padding 0 20px + 1px border each side 150 // the screen's safe area (iPhone notch / home bar; 0 elsewhere), read from a hidden probe padded with env() 151 function insets() { 152 var p = document.getElementById('msw-safe'); 153 if (!p) { 154 p = document.createElement('div'); 155 p.id = 'msw-safe'; 156 p.style.cssText = 'position:fixed;left:0;top:0;width:0;height:0;visibility:hidden;pointer-events:none;' + 157 'padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)'; 158 document.body.appendChild(p); 159 } 160 var c = window.getComputedStyle(p); 161 return {t: parseFloat(c.paddingTop) || 0, r: parseFloat(c.paddingRight) || 0, b: parseFloat(c.paddingBottom) || 0, l: parseFloat(c.paddingLeft) || 0}; 162 } 163 function fit() { 164 var app = $('#' + Lite3.id); 165 if (!app.length || !$('#' + Lite3.id + '-grid').length) return; // only the mixer view (not the Start button) 166 var safe = insets(), nav = $('#header'); 167 var top = (nav.is(':visible') ? nav.outerHeight() : safe.t) + 10; 168 var bottom = 10 + safe.b; // the footer links line is hidden (css/web.css) 169 // the real screen width: on phones the 500px editor first stretches the layout viewport, so innerWidth alone 170 // would keep it too wide 171 // (iOS reports screen.width as the portrait width even in landscape: use the larger side when landscape) 172 var sw = window.screen && screen.width ? (window.innerWidth > window.innerHeight ? Math.max(screen.width, screen.height) : screen.width) : Infinity; 173 var vw = Math.min(window.innerWidth, window.visualViewport ? window.visualViewport.width : Infinity, sw); 174 var w = Math.max(240, vw - 20 - safe.l - safe.r - PAD); 175 var h = Math.max(300, window.innerHeight - top - bottom); 176 Lite3.UI.__minimum_width = Math.min(500, w); 177 Lite3.UI.__minimum_height = Math.min(500, h); 178 app.css({left: 10 + safe.l, top: top, width: w, height: h}).toggleClass('narrow', w < 500); 179 Lite3.UI.resize_and_scroll(); 180 } 181 var fitTimer = null; 182 function fitSoon() { clearTimeout(fitTimer); fitTimer = setTimeout(fit, 60); } 183 // only the browser window's own resize: jQuery UI's "resize" events from the dialogs bubble up to window too 184 $(window).on('resize orientationchange', function (e) { if (e.target !== window) return; fitSoon(); }); 185 if (window.visualViewport) window.visualViewport.addEventListener('resize', fitSoon); 186 // no moving or resizing by hand: the editor's draggable/resizable widgets (and their edge handles) are removed 187 function fixed() { 188 var app = $('#' + Lite3.id); 189 try { if (app.data('draggable')) app.draggable('destroy'); } catch (e) {} 190 try { if (app.data('resizable')) app.resizable('destroy'); } catch (e) {} 191 app.children('.ui-resizable-handle').remove(); 192 } 193 var _create = Lite3.UI.create; 194 Lite3.UI.create = function () { var r = _create.apply(this, arguments); fixed(); arrangeHeader(); fitSoon(); return r; }; 195 var _sync = Lite3.Mixer.event_sync; 196 Lite3.Mixer.event_sync = function () { var r = _sync.apply(this, arguments); fitSoon(); return r; }; 197 window.MusicshakeWeb = {fit: fit}; 198 199 // ---- the editor's toolbar ------------------------------------------------------------------------------- 200 // In the browser: the site's menu bar is on top as on every eng page; the toolbar has [Save] [Install] [Classic]. 201 // Installed app (html.app-standalone, js/app.js): the whole screen is the editor. The menu bar (#header) is hidden 202 // (css/web.css) and its right side, #header-login â Login (logged out) or bell + user id + user menu (logged 203 // in), filled by Lite3's login script â is moved as it is, with its click handlers, into the toolbar:
204 // [Save] [Install] [bell] [user id | Login] [menu]; the menu has the site's page links and the Classic version. 205 // Both: "My saved songs" (Menu > My songs) and "Auto save" (always on) are dropped. 206 var APP = !!(window.MSApp && MSApp.standalone); 207 var SITE = [['Home', '/'], ['Listen', '/recent'], ['Chart', '/charts'], ['People', '/people'], ['Store', '/store']]; 208 function toolbar() { 209 var head = $('#' + Lite3.id + '-header'); 210 if (!head.length) return null; 211 var bar = $('#msw-toolbar'); 212 if (!bar.length) { 213 bar = $('<div id="msw-toolbar"></div>').append( 214 $('<a class="save" href="#" title="Save this song to My songs" style="display:none">Save</a>') 215 .click(function (e) { e.preventDefault(); Lite3.save.save_slot(); }), 216 $('<a class="install" href="#" title="Install Musicshake as an app: full screen, from your home screen" style="display:none">Install<span class="long"> app</span></a>') 217 .click(function (e) { e.preventDefault(); if (window.MSApp) MSApp.install(); }), 218 $('<a class="classic" href="/create/" target="_blank" title="The original Make Music (needs Adobe Flash)">Classic<span class="long"> Flash version</span></a>'), 219 $('<a class="sitemenu" href="#" title="Musicshake menu" aria-label="Musicshake menu" aria-haspopup="true"></a>') 220 .click(function (e) { e.preventDefault(); e.stopPropagation(); siteMenu(this); })); 221 head.append(bar); 222 } 223 var login = $('#header-login'); 224 if (APP && login.length && !login.parent().is(bar)) bar.children('.sitemenu').before(login); 225 return bar; 226 } 227 function siteMenu(anchor) { 228 var m = $('#msw-sitemenu'); 229 if (m.length) { m.remove(); return; } 230 m = $('<div id="msw-sitemenu" role="menu"></div>'); 231 $.each(SITE, function (i, l) { m.append($('<a role="menuitem"></a>').attr('href', l[1]).text(l[0])); }); 232 m.append('<div class="sep"></div>', '<a role="menuitem" href="/create/" target="_blank" title="The original Make Music (needs Adobe Flash)">Classic Flash version</a>'); 233 $('body').append(m); 234 var r = anchor.getBoundingClientRect(); 235 m.css({top: r.bottom + 8, right: Math.max(10, window.innerWidth - r.right)}); 236 } 237 $(document).on('click touchend', function (e) { 238 if (!$(e.target).closest('#msw-sitemenu, #msw-toolbar .sitemenu').length) $('#msw-sitemenu').remove(); 239 }); 240 // Lite3 adds the user menu (My profile, Settings, Logout...) to #header, which is hidden: it is moved to the page 241 // and opened under the user id 242 function placeUserMenu() { 243 var u = $('#header > #header-login-usermenu'); 244 if (!u.length) return; 245 var id = $('#header-login .usermenu')[0]; 246 u.addClass('msw-usermenu').appendTo('body'); 247 if (id) { var r = id.getBoundingClientRect(); u.css({top: r.bottom + 8, right: Math.max(10, window.innerWidth - r.right)}); } 248 } 249 function arrangeHeader() { 250 var login = $('#header-login'); 251 toolbar(); 252 if (!login.length) return; 253 var mysongs = login.children('a').filter(function () { return $(this).text() === 'My saved songs'; }), autosave = login.children('#autosave'); 254 if (mysongs.length || autosave.length) { mysongs.remove(); autosave.remove(); autosaveOn(); } 255 // logged out: Login only (as on the site, whose Signup link is hidden) 256 login.children('a').filter(function () { return $(this).text() === 'Signup'; }).hide(); 257 if (login.children('.usermenu').length && !login.children('.notification').length) { 258 var n = (Login.userdata && Login.userdata.new_notifications) ? parseInt(Login.userdata.new_notifications, 10) || 0 : 0; 259 login.prepend('<a href="/my_notifications" class="notification" title="Notifications"><span class="label">Notifications: </span><span class="val' + (n ? ' highlight' : '') + '">' + n + '</span></a>'); 260 } 261 } 262 $(function () { 263 var login = document.getElementById('header-login'), header = document.getElementById('header'); 264 if (window.MutationObserver) { 265 if (login) new MutationObserver(arrangeHeader).observe(login, {childList: true}); 266 if (header && APP) new MutationObserver(placeUserMenu).observe(header, {childList: true}); 267 } 268 arrangeHeader(); 269 }); 270 271 // ---- start with the last song (or One click start) ----------------------------------------------------- 272 // No "New song" window on arrival: the last song worked on in this browser is reopened â a saved song from the 273 // account (same user logged in), otherwise the copy kept here â or, with nothing kept, One click start. 274 // A Remix link (?sn=) still opens that song. The start window is still in Menu > New. 275 var LAST = 'msw.last', lastKept = '', starting = false;
276 function remember() { 277 try { 278 var d = MusicshakeMixer2.data, swf = Lite3.Mixer.swf, st = swf && swf.mixer_state ? swf.mixer_state() : ''; 279 // only a finished song (not one half-way through an edit or a load) 280 if (starting || (st !== 'pause' && st !== 'play') || !d.chords.length || !d.tracks.length || (d.complete && !d.complete())) return; 281 var user = Login.userdata && Login.userdata.num ? String(Login.userdata.num) : ''; 282 var msw = d.serialize(), key = user + '|' + (Lite3.slot.num || '') + '|' + JSON.stringify(msw); 283 if (key === lastKept) return; 284 lastKept = key; 285 localStorage.setItem(LAST, JSON.stringify({user: user, slot: Lite3.slot.num ? String(Lite3.slot.num) : '', title: Lite3.slot.title || '', msw: msw, at: Date.now()})); 286 } catch (e) {} 287 } 288 function forget() { try { localStorage.removeItem(LAST); } catch (e) {} lastKept = ''; } 289 function lastSong() { try { return JSON.parse(localStorage.getItem(LAST) || 'null'); } catch (e) { return null; } } 290 var _ok = Lite3.bootstrap.ok; 291 Lite3.bootstrap.ok = function () { 292 if (!Lite3.startopt) { 293 var last = lastSong(), me = Login.userdata && Login.userdata.num ? String(Login.userdata.num) : ''; 294 // a song kept by another account on this browser is not opened 295 if (last && last.user && last.user !== me) last = null; 296 if (last && last.slot && me) { 297 Lite3.startopt = {type: 'slot', slot_num: last.slot}; 298 setTimeout(function () { Lite3.slot.update(last.slot, last.title); }, 0); 299 } else if (last && last.msw) { 300 Lite3.startopt = {type: 'local', msw: last.msw}; 301 } else { 302 Lite3.startopt = {type: 'shake'}; 303 } 304 // if that song can't be opened (deleted, another account, bad copy), it is forgotten and a new one starts 305 if (Lite3.startopt.type !== 'shake') starting = true; 306 } 307 return _ok.apply(this, arguments); 308 }; 309 var _sync2 = Lite3.Mixer.event_sync; 310 Lite3.Mixer.event_sync = function () { starting = false; var r = _sync2.apply(this, arguments); setTimeout(remember, 0); return r; }; 311 window.addEventListener('pagehide', remember); 312 document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'hidden') remember(); }); 313 setInterval(function () { if (document.visibilityState !== 'hidden') remember(); }, 15000); // block on/off and volume changes don't re-sync 314 315 // ---- menu, genre and block variation: popovers next to their button --------------------------------- 316 // __dialog_sticky centres these windows over a white 30% mask; here the mask is clear (a tap outside still 317 // closes the window) and the window opens beside what was tapped, styled apart from the editor (css/web.css). 318 function popover(id, anchor, side) { 319 var d = $('#' + id), a = $(anchor); 320 if (!d.length) return; 321 d.addClass('msw-pop'); 322 $('#mask').addClass('msw-clear').off('click.msw').on('click.msw', function () { __dialog_close_sticky(); }); 323 if (!a.length) return; 324 var r = a[0].getBoundingClientRect(), w = d.outerWidth(), h = d.outerHeight(), vw = window.innerWidth, vh = window.innerHeight, top, left; 325 if (side === 'side') { 326 left = r.right + 10; if (left + w > vw - 10) left = r.left - w - 10; 327 top = r.top - 20; 328 } else { 329 left = r.left + r.width / 2 - w / 2; 330 top = r.top - h - 12; if (top < 10) top = r.bottom + 12; 331 } 332 left = Math.max(10, Math.min(vw - w - 10, left)); 333 top = Math.max(10, Math.min(vh - h - 10, top)); 334 d.css({position: 'fixed', left: left, top: top, marginLeft: 0}); 335 } 336 // Lite3's dim layer behind its windows was white 30% (#mask, no colour given): black 45% from the first frame, 337 // so no white flash while a window waits for a login check or a catalog request. See-through (0) and the 338 // post-status 80% black stay as they are. 339 var _mask = window.__mask; 340 window.__mask = function (on, opacity, bgcolor) { 341 if (on && opacity !== 0 && !bgcolor) { opacity = 0.45; bgcolor = '#000'; } 342 return _mask.call(this, on, opacity, bgcolor); 343 }; 344 345 // Save Song, Choose Tempo, My Songs: the same dark windows, centred over a dim background (tap outside closes) 346 function modal(id) { 347 var d = $('#' + id); 348 if (!d.length) return false;
349 d.addClass('msw-pop msw-modal'); 350 $('#mask').removeClass('msw-clear').addClass('msw-dim').off('click.msw').on('click.msw', function () { __dialog_close_sticky(); }); 351 return true; 352 } 353 function whenOpen(id) { // these windows open after a login check or a catalog request: wait for them 354 var tries = 0, t = setInterval(function () { if (modal(id) || ++tries > 40) clearInterval(t); }, 50); 355 } 356 ['save_slot', 'saveas_slot'].forEach(function (f) { 357 var orig = Lite3.save[f]; 358 if (orig) Lite3.save[f] = function () { var r = orig.apply(this, arguments); whenOpen(Lite3.id + '-save'); return r; }; 359 }); 360 var _tempo = Lite3.tempo.show; 361 Lite3.tempo.show = function () { var r = _tempo.apply(this, arguments); whenOpen(Lite3.id + '-tempo'); return r; }; 362 var _mysongs = Lite3.mysongs; 363 Lite3.mysongs = function () { var r = _mysongs.apply(this, arguments); whenOpen(Lite3.id + '-mysongs'); watchSongs(); return r; }; 364 // My Songs rows: one "â¦" button opens Load / Rename / Copy / Delete (the four buttons crowded narrow windows); 365 // tempo as "140 BPM" 366 function tidySongs() { 367 $('#' + Lite3.id + '-mysongs .item').each(function () { 368 var it = $(this); 369 var t = it.children('.tempo'); if (t.length && /Beats per minute/.test(t.text())) t.text(t.text().replace(/\s*Beats per minute/, ' BPM')); 370 if (it.children('.msw-more').length || !it.children('.buttons').length) return; 371 $('<button class="msw-more" aria-label="More">…</button>').insertBefore(it.children('.buttons')).click(function (e) { 372 e.stopPropagation(); 373 var open = it.hasClass('msw-open'); 374 $('#' + Lite3.id + '-mysongs .item.msw-open').removeClass('msw-open'); 375 if (!open) it.addClass('msw-open'); 376 }); 377 it.children('.buttons').on('click', 'button', function () { it.removeClass('msw-open'); }); 378 // tapping the song itself loads it (Load left the ⦠menu) 379 it.addClass('msw-loadable').on('click', function (e) { 380 if ($(e.target).closest('.msw-more, .buttons, .rename_box, .handle, input, button').length) return; 381 it.children('.buttons').find('button.load').trigger('click'); 382 }); 383 }); 384 } 385 var songsObserver = null; 386 function watchSongs() { 387 var tries = 0, t = setInterval(function () { 388 var list = document.querySelector('#' + Lite3.id + '-mysongs .alist'); 389 if (!list && ++tries < 40) return; 390 clearInterval(t); 391 if (!list || !window.MutationObserver) return; 392 if (songsObserver) songsObserver.disconnect(); 393 songsObserver = new MutationObserver(tidySongs); 394 songsObserver.observe(list, {childList: true, subtree: true}); 395 tidySongs(); 396 }, 50); 397 } 398 $(document).on('click', function (e) { if (!$(e.target).closest('.msw-more, .buttons').length) $('#' + Lite3.id + '-mysongs .item.msw-open').removeClass('msw-open'); }); 399 400 // confirm box ("A new will load and any unsaved information â¦"): the dark design, Confirm in orange 401 if (window.__msg) __msg.MIXER_CONFIRM_SHAKE = 'A new song will load and any unsaved changes to the current song will be lost.'; 402 var _confirm = window.__confirm; 403 window.__confirm = function () { 404 var r = _confirm.apply(this, arguments); 405 var d = $('div.dialog#confirm'); 406 if (d.length) { d.addClass('msw-confirm'); modal('confirm'); } 407 return r; 408 }; 409 410 // Choose Chord, Choose Instrument, Post Song: fit narrow windows; when opened as a step of a flow (Start from 411 // scratch, Add part / track) their background is the dim dark one, not Lite3's white 30% layer 412 // Wherever they are opened from (a part number, a track icon, + menu, a step of a flow), these windows look the 413 // same: fitted to the screen and centred (the chord list scrolls natively: css/web.css) 414 function fitDialog(id) { 415 var d = $('#' + id); 416 if (!d.length) return false; 417 d.addClass('msw-fit'); 418 if (id === Lite3.id + '-post') d.addClass('msw-pop msw-postdlg'); 419 var m = $('#mask'); 420 if (m.length && !m.hasClass('msw-clear')) m.addClass('msw-dim'); 421 centre(d); 422 return true; 423 } 424 function centre(d) { 425 if (!d || !d.length || !d.is(':visible')) return; 426 var vw = window.innerWidth, vh = window.innerHeight, w = d.outerWidth(), h = d.outerHeight(), safe = insets(); 427 d.css({position: 'fixed', marginLeft: 0, marginTop: 0, 428 left: Math.max(10 + safe.l, Math.round((vw - w) / 2)), top: Math.max(10 + safe.t, Math.round((vh - h) / 2))}); 429 } 430 var FITTED = ['chords', 'instruments', 'effect-browser', 'post']; 431 function whenFit(id) { 432 var tries = 0, last = -1, t = setInterval(function () { 433 var d = $('#' + id); 434 if (d.length && !d.hasClass('msw-fit')) fitDialog(id); 435 // re-centre while its content (catalog lists) arrives and changes its height 436 if (d.length && d.outerHeight() !== last) { last = d.outerHeight(); centre(d); } 437 if (++tries > 40 || (tries > 4 && !d.length)) clearInterval(t); 438 }, 50); 439 } 440 $(window).on('resize orientationchange', function (e) { 441 if (e.target !== window) return;
442 setTimeout(function () { FITTED.forEach(function (n) { centre($('#' + Lite3.id + '-' + n + '.msw-fit')); }); }, 100); 443 }); 444 var _cbrowse = Lite3.chords.browse; 445 Lite3.chords.browse = function () { var r = _cbrowse.apply(this, arguments); whenFit(Lite3.id + '-chords'); return r; }; 446 var _ibrowse = Lite3.instruments.browse; 447 Lite3.instruments.browse = function () { var r = _ibrowse.apply(this, arguments); whenFit(Lite3.id + '-instruments'); return r; }; 448 var _ebrowse = Lite3.effects.browse; 449 Lite3.effects.browse = function () { var r = _ebrowse.apply(this, arguments); whenFit(Lite3.id + '-effect-browser'); return r; }; 450 var _post = Lite3.post.start; 451 Lite3.post.start = function () { var r = _post.apply(this, arguments); whenFit(Lite3.id + '-post'); return r; }; 452 var _menu = Lite3.menu.show; 453 Lite3.menu.show = function () { var r = _menu.apply(this, arguments); popover(Lite3.id + '-menu', '#' + Lite3.id + '-control-menu'); return r; }; 454 var _genre = Lite3.genre.show; 455 Lite3.genre.show = function () { var r = _genre.apply(this, arguments); popover(Lite3.id + '-genre', '#' + Lite3.id + '-control-genre'); return r; }; 456 // Block Variation: a popover beside the block; the current variation (or the first) plays as soon as the list is 457 // there, like the chord and instrument windows. One timer: a re-open cancels the previous one. 458 var _bv = Lite3.blockvariations.show, bvTimer = null; 459 Lite3.blockvariations.show = function (row, col) { 460 var r = _bv.apply(this, arguments), id = Lite3.id + '-blockvariation', block = '#' + Lite3.id + '-block-' + row + '_' + col, dlg = $('#' + id)[0]; 461 popover(id, block, 'side'); 462 clearInterval(bvTimer); 463 var tries = 0, played = false; 464 bvTimer = setInterval(function () { 465 var d = $('#' + id); 466 if (!d.length || (dlg && d[0] !== dlg) || ++tries > 60) { clearInterval(bvTimer); return; } 467 popover(id, block, 'side'); // its list arrives later: place it again once it has its size 468 var list = d.find('.playable'); 469 if (!list.length || played) return; 470 played = true; 471 var item = list.filter('.selected').first(); 472 if (!item.length) item = list.first().trigger('click'); 473 item.children('.play').trigger('click'); 474 setTimeout(function () { popover(id, block, 'side'); clearInterval(bvTimer); }, 300); 475 }, 100); 476 return r; 477 }; 478 479 // ---- menu: My songs (the top "My saved songs" link is gone) -------------------------------------------- 480 var _buttons = Lite3.menu.buttons; 481 Lite3.menu.buttons = function () { 482 var list = _buttons.apply(this, arguments); 483 var mine = $('<div class="pad clickable"><div class="label b">My songs</div><p>Open and manage your saved songs</p></div>') 484 .click(function () { __dialog_close_sticky(); Login.check(function () { Lite3.mysongs(); }); }); 485 list.children().first().after(mine); 486 return list; 487 }; 488 489 // ---- New song window: no "My songs" (it is in the Menu); styled like the popovers, centred ------------------ 490 var _start = Lite3.startmenu.show; 491 Lite3.startmenu.show = function () { 492 var r = _start.apply(this, arguments), id = Lite3.id + '-startmenu'; 493 $('#' + id + ' .alist > .pad').filter(function () { return $(this).find('.label').text() === 'My songs'; }).remove(); 494 popover(id); 495 confirmNew(id); 496 return r; 497 }; 498 499 // ---- no "Save your work" panel beside the editor --------------------------------------------------------- 500 // Lite3 put it in the page's sidebar for every song not yet saved. Instead: Menu > Save (asks for a title the 501 // first time) and a Save button in the toolbar, shown while the song has no slot yet. Once saved, auto save 502 // (always on) keeps it up to date. 503 Lite3.save.create = function () {}; 504 function saveButton() { 505 var bar = toolbar(); 506 if (bar) bar.children('.save').toggle(!Lite3.slot.num); 507 } 508 // ---- Install app (js/app.js): the browser's install prompt, or the Add to Home Screen steps on iPhone/iPad ---- 509 function installButton() { 510 var bar = toolbar(); 511 if (bar && window.MSApp) bar.children('.install').toggle(MSApp.canInstall()); 512 } 513 document.addEventListener('msapp:installable', installButton); 514 var _slot = Lite3.slot.update; 515 Lite3.slot.update = function () { var r = _slot.apply(this, arguments); saveButton(); return r; }; 516 var _sync3 = Lite3.Mixer.event_sync; 517 Lite3.Mixer.event_sync = function () { var r = _sync3.apply(this, arguments); saveButton(); installButton(); return r; }; 518 var _saved = Lite3.Mixer.event_saved; // a first save sets the slot from the mixer's save callback 519 if (_saved) Lite3.Mixer.event_saved = function () { var r = _saved.apply(this, arguments); setTimeout(saveButton, 0); return r; }; 520 521 // ---- auto save is always on --------------------------------------------------------------------------- 522 function autosaveOn() { 523 if (window.__settings) window.__settings.AUTOSAVE = 1; 524 if (Lite3.Mixer.swf && Lite3.Mixer.swf.mixer_autosave) Lite3.Mixer.swf.mixer_autosave(1); 525 }
526 var _connect = Lite3.Mixer.event_connect; 527 Lite3.Mixer.event_connect = function () { if (window.__settings) window.__settings.AUTOSAVE = 1; return _connect.apply(this, arguments); }; 528 529 // ---- no Flash check -------------------------------------------------------------------------------- 530 Lite3.bootstrap.run = function () { 531 if ($('#' + Lite3.id).length > 0) $('#' + Lite3.id).remove(); 532 __swf.load('bootstrap', '300', '300'); 533 }; 534 535 // ---- catalog over https ---------------------------------------------------------------------------- 536 window.__xhr3 = function (url, data, success, failure) { 537 if (Login.userdata && Login.userdata.num) data += '&user_num=' + Login.userdata.num; 538 $.ajax({ 539 url: 'https://usminimsgs.musicshake.com/msgs' + url, 540 type: 'GET', 541 data: data, 542 cache: true, 543 dataType: 'jsonp', 544 success: function (d) { if (d && typeof d === 'object' && success) success(d); }, 545 error: function () { 546 if (failure) { failure(); return; } 547 // callers mostly pass no failure handler: close what waits for this and say so 548 $('#alert, #alert2').remove(); 549 __dialog_close_sticky(); 550 __error(navigator.onLine === false ? OFFLINE : "The music catalog isn't responding. Please try again in a moment."); 551 } 552 }); 553 }; 554 555 // ---- 2026-09-24 audit fixes ------------------------------------------------------------------------------ 556 var OFFLINE = "You're offline. Check your connection and try again."; 557 558 // no ads, no dead social widgets: the ad frames Lite3 still builds (every shake, preview and post) are dropped 559 window.__advertisement = function () {}; 560 var _html = $.fn.html; 561 $.fn.html = function (v) { 562 if (typeof v === 'string' && v.indexOf('/ads/create-adsense') >= 0) return _html.call(this, ''); 563 return _html.apply(this, arguments); 564 }; 565 566 // server calls (/xhr/*): a failed request says so (Lite3 dropped the error silently, and windows waited forever) 567 window.__xhr = function (request, data, success, failure) { 568 $.ajax({ 569 url: '/xhr/' + request, type: 'POST', data: data || '', dataType: 'json', 570 error: function () { 571 $('#alert, #alert2').remove(); 572 if (typeof failure === 'function') failure({result: 'failure'}); 573 __error(navigator.onLine === false ? OFFLINE : __msg.SYSTEM_FAULT); 574 }, 575 success: function (d) { 576 if (!d || typeof d !== 'object') return; 577 if (d.result !== 'success') { 578 if (d.output && typeof d.output === 'string') __error(d.output); 579 if (typeof failure === 'function') failure(d); 580 } else if (typeof success === 'function') success(d); 581 } 582 }); 583 }; 584 585 // genre / mood / theme names: read-only, so fetched by GET with retries; a built-in genre list until then 586 // (without them the editor could not draw) 587 var GENRES = {1: 'Hiphop', 2: 'Rock/Metal', 3: 'R&B', 4: 'Dance/Disco', 5: 'Electronic/Techno', 6: 'Pop', 7: 'Ballad', 8: 'Jazz', 9: 'Latin', 10: 'Classical/New Age'}; 588 Lite3.category.init = function () { 589 var tries = 0; 590 Lite3.category.genre = Lite3.category.genre || $.extend({}, GENRES); 591 (function get() { 592 $.ajax({url: '/xhr/category', type: 'GET', dataType: 'json', timeout: 10000}).done(function (d) { 593 if (!d || !d.output || !d.output.genre) { fail(); return; } 594 delete d.output.genre[11]; 595 Lite3.category.genre = d.output.genre; 596 Lite3.category.mood = d.output.mood; 597 Lite3.category.theme = d.output.theme; 598 }).fail(fail); 599 function fail() { if (++tries < 3) setTimeout(get, 1500 * tries); } 600 })(); 601 }; 602 var _gname = Lite3.genre.name; 603 Lite3.genre.name = function () { if (!Lite3.category.genre) Lite3.category.genre = $.extend({}, GENRES); return _gname.apply(this, arguments); }; 604 // no genre (Start from scratch): a placeholder instead of an empty box 605 var _gselect = Lite3.genre.select; 606 Lite3.genre.select = function () { 607 var r = _gselect.apply(this, arguments), b = $('#' + Lite3.id + '-control-genre'); 608 if (!$.trim(b.text())) b.text('Choose genre'); 609 return r; 610 }; 611 612 // engine messages: looked up directly (Lite3 used eval on the code), the "Preparing..." layer is removed, and
613 // sounds that failed are fetched again when the connection is back 614 $.extend(__msg, { 615 MIXER_DOWNLOAD_ERROR: "Some sounds couldn't be downloaded. Check your connection; they'll load on your next change.", 616 MIXER_UNDO_ERROR: "Sorry, that couldn't be undone.", 617 SLOT_SAVE_ERROR: "Your song couldn't be saved just now. It will be saved with your next change.", 618 SONG_TOO_LONG: 'This song is too long to post (30 minutes at most).' 619 }); 620 var _merror = Lite3.Mixer.error, lastError = 0; 621 Lite3.Mixer.error = function (code) { 622 code = /^[A-Z][A-Z0-9_]{1,40}$/.test(String(code)) ? String(code) : 'MIXER_DOWNLOAD_ERROR'; 623 if (!$('div.dialog.sticky:visible').length) $('#mask').remove(); 624 $('#alert, #alert2').remove(); 625 if (code === 'PLEASE_WAIT') return; 626 if (starting) { // the remembered song could not be opened: forget it and start a new one 627 starting = false; forget(); 628 setTimeout(function () { var s = Lite3.Mixer.swf; if (s && s.mixer_load) s.mixer_load({type: 'shake'}); }, 0); 629 } 630 if (code === 'MIXER_DOWNLOAD_ERROR') window.addEventListener('online', function () { var s = Lite3.Mixer.swf; if (s && s.mixer_retry) s.mixer_retry(); }, {once: true}); 631 if (Date.now() - lastError < 1500) return; // one message per burst 632 lastError = Date.now(); 633 if (navigator.onLine === false && /DOWNLOAD|SAVE|MSW/.test(code)) { __error(OFFLINE); return; } 634 if (__msg[code] && !/^(MIXER_MSW_ERROR|INVALID_VERSION|LOGIN_REQUIRED)$/.test(code)) { __error(__msg[code]); return; } 635 return _merror.call(this, code); 636 }; 637 638 // New song options replace the current song: ask first, like Shake does (the handlers are private to Lite3's 639 // start menu, so each button's own click handler is wrapped) 640 function confirmNew(id) { 641 $('#' + id + ' .alist > .pad').each(function () { 642 var pad = $(this), label = pad.find('.label').text(), ev = $._data(this, 'events'); 643 if (!/^(One click start|Genre tempo wizard|Start from scratch)$/.test(label) || pad.data('msw-confirm') || !ev || !ev.click) return; 644 var handlers = ev.click.map(function (h) { return h.handler; }); 645 pad.data('msw-confirm', 1).off('click').on('click', function (e) { 646 var self = this, run = function () { handlers.forEach(function (h) { h.call(self, e); }); }; 647 if (!MusicshakeMixer2.data.tracks.length) { run(); return; } 648 __dialog_close_sticky(); 649 __confirm(__msg.MIXER_CONFIRM_SHAKE, run); 650 }); 651 }); 652 } 653 654 // a window reused by Lite3 (__dialog_sticky empties the open one and renames it) starts clean: no classes or 655 // position left over from the previous window, no leftover tap-outside handler on the mask 656 var _sticky = window.__dialog_sticky; 657 window.__dialog_sticky = function () { 658 var old = $('div.dialog.sticky'); 659 if (old.length) { 660 old.removeClass(function (i, c) { return (c.match(/\bmsw-\S+/g) || []).join(' '); }); 661 old.css({position: '', left: '', top: '', marginLeft: '', marginTop: ''}); 662 } 663 $('#mask').off('click.msw'); 664 return _sticky.apply(this, arguments); 665 }; 666 var _mask2 = window.__mask; 667 window.__mask = function (on) { if (!on) $('#mask').off('click.msw'); return _mask2.apply(this, arguments); }; 668 669 // the login window: always the dark centred one (it was plain when opened from Save or My songs) 670 if (typeof window.open_loginform === 'function') { 671 var _login = window.open_loginform; 672 window.open_loginform = function () { var r = _login.apply(this, arguments); modal('login-form'); return r; }; 673 } 674 675 // Instrument window: Select with nothing chosen says what to do 676 $(document).on('click', '#' + Lite3.id + '-instruments .bottom button', function () { 677 var d = $('#' + Lite3.id + '-instruments')[0]; 678 setTimeout(function () { 679 var now = $('#' + Lite3.id + '-instruments'); 680 if (!now.length || now[0] !== d || now.find('.msw-hint').length) return; 681 $('<span class="msw-hint">Choose an instrument and a sound first</span>').appendTo(now.find('.bottom')); 682 setTimeout(function () { now.find('.msw-hint').remove(); }, 2500); 683 }, 250); 684 }); 685 686 // the track / part / effect menus beside the grid stay on screen 687 function clampMenus() { 688 $('.grid_menu:visible').each(function () { 689 var m = $(this), r = this.getBoundingClientRect(), vw = window.innerWidth, dx = 0; 690 if (r.right > vw - 6) dx = vw - 6 - r.right; 691 if (r.left + dx < 6) dx = 6 - r.left; 692 if (dx) m.css('left', (parseFloat(m.css('left')) || 0) + dx); 693 }); 694 }
695 $(document).on('click touchend', '#' + Lite3.id + '-grid', function () { setTimeout(clampMenus, 30); }); 696 697 // volume: â and + step it by 10% 698 $(document).on('click', '#' + Lite3.id + '-control-volume .minus, #' + Lite3.id + '-control-volume .plus', function () { 699 var s = $('#' + Lite3.id + '-control-volume .ui-slider'); 700 if (!s.length) return; 701 var v = Math.max(0, Math.min(1, Math.round((s.slider('value') + ($(this).hasClass('plus') ? 0.1 : -0.1)) * 10) / 10)); 702 s.slider('value', v); 703 Lite3.Mixer.volume(v); 704 }); 705 706 // a new song from a template starts with every block off: say how to start (once) 707 var hinted = false; 708 var _sync4 = Lite3.Mixer.event_sync; 709 Lite3.Mixer.event_sync = function (d) { 710 var r = _sync4.apply(this, arguments); 711 try { 712 if (!hinted && d && d.blocks && d.blocks.length && !d.blocks.some(function (row) { return row.some(function (b) { return b.on; }); })) { 713 hinted = true; 714 setTimeout(function () { if (!$('div.dialog:visible, #error:visible').length) __alert2('Tap the blocks to turn them on, then press Play.', true); }, 1500); 715 } 716 } catch (e) {} 717 return r; 718 }; 719 720 // keyboard and screen readers: the editor's controls are buttons with names 721 var LABELS = {'control-play': 'Play', 'control-pause': 'Pause', 'control-shake': 'Shake: a new song', 'control-menu': 'Menu', 'control-genre': 'Genre'}; 722 function label() { 723 $.each(LABELS, function (id, text) { 724 var el = $('#' + Lite3.id + '-' + id); 725 if (el.length && !el.attr('role')) el.attr({role: 'button', tabindex: 0, 'aria-label': text}); 726 }); 727 $('.dialog .top .close').not('[role]').attr({role: 'button', tabindex: 0, 'aria-label': 'Close'}); 728 } 729 $(document).on('keydown', '[role=button]', function (e) { 730 if ((e.key === 'Enter' || e.key === ' ') && !/^(A|BUTTON|INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) { e.preventDefault(); $(this).trigger('click'); } 731 }); 732 var _create2 = Lite3.UI.create; 733 Lite3.UI.create = function () { var r = _create2.apply(this, arguments); setTimeout(label, 0); return r; }; 734 var _sync5 = Lite3.Mixer.event_sync; 735 Lite3.Mixer.event_sync = function () { var r = _sync5.apply(this, arguments); setTimeout(label, 0); return r; }; 736})(window, window.jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.