diff --git a/index.html b/index.html
index 07dd475..fe826bb 100755
--- a/index.html
+++ b/index.html
@@ -712,7 +712,7 @@
"content": "\n/*\nUtil\n\nHolds a bunch of methods for note math, conversions, some dom stuff.\n\nStatus: Active\n */\nvar clamp, composedPath, inverseScale, mod, noteCompare, notes, quantize, scale, staffNoteToPitch, wrap;\n\nscale = {\n \"-7\": {\n 0: -1,\n 1: 1,\n 2: 3,\n 3: 4,\n 4: 6,\n 5: 8,\n 6: 10\n },\n \"-6\": {\n 0: -1,\n 1: 1,\n 2: 3,\n 3: 5,\n 4: 6,\n 5: 8,\n 6: 10\n },\n \"-5\": {\n 0: 0,\n 1: 1,\n 2: 3,\n 3: 5,\n 4: 6,\n 5: 8,\n 6: 10\n },\n \"-4\": {\n 0: 0,\n 1: 1,\n 2: 3,\n 3: 5,\n 4: 7,\n 5: 8,\n 6: 10\n },\n \"-3\": {\n 0: 0,\n 1: 2,\n 2: 3,\n 3: 5,\n 4: 7,\n 5: 8,\n 6: 10\n },\n \"-2\": {\n 0: 0,\n 1: 2,\n 2: 3,\n 3: 5,\n 4: 7,\n 5: 9,\n 6: 10\n },\n \"-1\": {\n 0: 0,\n 1: 2,\n 2: 4,\n 3: 5,\n 4: 7,\n 5: 9,\n 6: 10\n },\n 0: {\n 0: 0,\n 1: 2,\n 2: 4,\n 3: 5,\n 4: 7,\n 5: 9,\n 6: 11\n },\n 1: {\n 0: 0,\n 1: 2,\n 2: 4,\n 3: 6,\n 4: 7,\n 5: 9,\n 6: 11\n },\n 2: {\n 0: 1,\n 1: 2,\n 2: 4,\n 3: 6,\n 4: 7,\n 5: 9,\n 6: 11\n },\n 3: {\n 0: 1,\n 1: 2,\n 2: 4,\n 3: 6,\n 4: 8,\n 5: 9,\n 6: 11\n },\n 4: {\n 0: 1,\n 1: 3,\n 2: 4,\n 3: 6,\n 4: 8,\n 5: 9,\n 6: 11\n },\n 5: {\n 0: 1,\n 1: 3,\n 2: 4,\n 3: 6,\n 4: 8,\n 5: 10,\n 6: 11\n },\n 6: {\n 0: 1,\n 1: 3,\n 2: 5,\n 3: 6,\n 4: 8,\n 5: 10,\n 6: 11\n },\n 7: {\n 0: 1,\n 1: 3,\n 2: 5,\n 3: 6,\n 4: 8,\n 5: 10,\n 6: 12\n }\n};\n\ninverseScale = Object.keys(scale[0]).reduce(function(m, key) {\n var value;\n value = scale[0][key];\n m[value] = key | 0;\n return m;\n}, {});\n\nnotes = \"C C# D D# E F F# G G# A A# B\".split(\" \");\n\nwrap = function(array, index) {\n return array[mod(index, array.length)];\n};\n\nmod = function(n, k) {\n return (n % k + k) % k;\n};\n\n\n/*\nQuantize a number, rounding it to the nearest nth unit.\n\n assert.equal quantize(61, 4), 60\n assert.equal quantize(63, 4), 64\n\n assert.equal quantize(0.77, 0.25), 0.75\n assert.equal quantize(0.55, 0.25), 0.5\n\nIf the unit is zero then return the number as is.\n\n assert.equal quantize(0.33333, 0), 0.33333\n */\n\nquantize = function(x, n) {\n if (n === 0) {\n return x;\n }\n return ((x / n + 1 / 2) | 0) * n;\n};\n\nnoteCompare = function(_arg, _arg1) {\n var na, nb, ta, tb;\n ta = _arg[0], na = _arg[1];\n tb = _arg1[0], nb = _arg1[1];\n if (ta < tb) {\n return -1;\n } else if (ta > tb) {\n return 1;\n } else {\n if (na < nb) {\n return -1;\n } else if (na > nb) {\n return 1;\n } else {\n return 0;\n }\n }\n};\n\nclamp = function(x, min, max) {\n return Math.min(max, Math.max(min, x));\n};\n\ncomposedPath = function(el) {\n var path;\n path = [];\n while (el) {\n path.push(el);\n if (el.tagName === 'HTML') {\n path.push(document);\n path.push(window);\n break;\n }\n el = el.parentElement;\n }\n return path;\n};\n\nstaffNoteToPitch = function(n, a, key) {\n var o, r;\n if (a == null) {\n a = 0;\n }\n if (key == null) {\n key = 0;\n }\n r = mod(n, 7);\n o = Math.floor(n / 7);\n return o * 12 + scale[key][r] + a;\n};\n\nmodule.exports = {\n accidentalToSymbol: function(accidental) {\n if (accidental === 1) {\n return \"♯\";\n } else if (accidental === -1) {\n return \"♭\";\n } else {\n return \"\";\n }\n },\n composedPath: composedPath,\n clamp: clamp,\n mod: mod,\n noteCompare: noteCompare,\n quantize: quantize,\n emptyElement: function(el) {\n var _results;\n _results = [];\n while (el.lastChild) {\n _results.push(el.lastChild.remove());\n }\n return _results;\n },\n inRange: function(range, _arg) {\n var n, n0, n1, t, t0, t1, _ref, _ref1;\n t = _arg[0], n = _arg[1];\n (_ref = range[0], t0 = _ref[0], t1 = _ref[1]), (_ref1 = range[1], n0 = _ref1[0], n1 = _ref1[1]);\n return (t0 <= t && t <= t1) && (n0 <= n && n <= n1);\n },\n\n /*\n noteName\n \n Translate a number to a music note name.\n \n -1 is B3\n 0 is C4\n 1 is C#4\n ...\n */\n noteName: function(noteNumber) {\n var note, octave;\n noteNumber |= 0;\n note = wrap(notes, noteNumber);\n octave = 4 + (noteNumber / 12) | 0;\n return \"\" + note + octave;\n },\n patternSampleNotes: function(pattern, offset) {\n if (offset == null) {\n offset = 0;\n }\n return pattern.notes().reduce(function(notes, _arg) {\n var accidental, instrument, instrumentPitchExists, pitch, staffNote, _;\n _ = _arg[0], staffNote = _arg[1], accidental = _arg[2], instrument = _arg[3];\n if (notes.length < 4) {\n pitch = staffNoteToPitch(staffNote + offset, accidental);\n instrumentPitchExists = notes.some(function(_arg1) {\n var i, p;\n i = _arg1[0], p = _arg1[1];\n return i === instrument && p === pitch;\n });\n if (!instrumentPitchExists) {\n notes.push([instrument, pitch]);\n }\n }\n return notes;\n }, []);\n },\n pitchToStaffNote: function(n) {\n var a, o, r, staffNote;\n r = mod(n, 12);\n o = Math.floor(n / 12);\n staffNote = inverseScale[r];\n if (staffNote != null) {\n a = 0;\n } else {\n staffNote = inverseScale[r - 1];\n a = 1;\n }\n return [o * 7 + staffNote, a];\n },\n staffNoteToPitch: staffNoteToPitch,\n wrap: wrap\n};\n"
},
"main": {
- "content": "var ButtonTemplate, closing, e, img, remote, util;\n\nrequire(\"./lib/extensions\");\n\nglobal.player = require(\"./player\")();\n\nutil = system.util;\n\nutil.applyStyle(system.ui.Style.all, \"system\");\n\nutil.applyStyle(require(\"./style\"), \"app\");\n\ndocument.body.appendChild(player.element);\n\nArray.prototype.forEach.call(document.querySelectorAll('canvas'), function(element) {\n return element.oncontextmenu = function(e) {\n return e.preventDefault();\n };\n});\n\nimg = new Image;\n\nimg.src = \"https://danielx.net/composer/box-art.jpg\";\n\ntry {\n if (location.host && !global.ELECTRON_VERSION) {\n navigator.serviceWorker.register('./sw.js');\n }\n} catch (_error) {\n e = _error;\n console.warn(e.message);\n}\n\nsetInterval(function() {\n if (!player.purchased()) {\n return player.purchase();\n }\n}, 1000 * 60 * 15);\n\nif (global.ELECTRON_VERSION) {\n ButtonTemplate = require(\"./templates/button\");\n remote = global.require(\"electron\").remote;\n player.purchased(true);\n player.removeUnloadHandler();\n closing = false;\n window.addEventListener(\"beforeunload\", function(e) {\n if (closing) {\n return;\n }\n player.confirmUnsaved().then(function() {\n closing = true;\n return remote.getCurrentWindow().close();\n });\n return e.returnValue = true;\n });\n player.element.querySelector('section.buttons').appendChild(ButtonTemplate({\n text: \"🚪 Exit\",\n \"class\": \"exit\",\n click: function() {\n return remote.getCurrentWindow().close();\n }\n }));\n}\n"
+ "content": "var ButtonTemplate, closing, e, img, remote, util;\n\nrequire(\"./lib/extensions\");\n\nglobal.player = require(\"./player\")();\nplayer.purchased(true)\nutil = system.util;\n\nutil.applyStyle(system.ui.Style.all, \"system\");\n\nutil.applyStyle(require(\"./style\"), \"app\");\n\ndocument.body.appendChild(player.element);\n\nArray.prototype.forEach.call(document.querySelectorAll('canvas'), function(element) {\n return element.oncontextmenu = function(e) {\n return e.preventDefault();\n };\n});\n\nimg = new Image;\n\nimg.src = \"https://danielx.net/composer/box-art.jpg\";\n\ntry {\n if (location.host && !global.ELECTRON_VERSION) {\n navigator.serviceWorker.register('./sw.js');\n }\n} catch (_error) {\n e = _error;\n console.warn(e.message);\n}\n\nsetInterval(function() {\n if (!player.purchased()) {\n return player.purchase();\n }\n}, 1000 * 60 * 15);\n\nif (global.ELECTRON_VERSION) {\n ButtonTemplate = require(\"./templates/button\");\n remote = global.require(\"electron\").remote;\n player.purchased(true);\n player.removeUnloadHandler();\n closing = false;\n window.addEventListener(\"beforeunload\", function(e) {\n if (closing) {\n return;\n }\n player.confirmUnsaved().then(function() {\n closing = true;\n return remote.getCurrentWindow().close();\n });\n return e.returnValue = true;\n });\n player.element.querySelector('section.buttons').appendChild(ButtonTemplate({\n text: \"🚪 Exit\",\n \"class\": \"exit\",\n click: function() {\n return remote.getCurrentWindow().close();\n }\n }));\n}\n"
},
"persistence": {
"content": "var Drop, Gist, Midi2Composer, Modal, Progress, PublishTemplate, ReadFile, clipboardCopy, publishView, _ref;\n\nGist = require(\"./lib/legacy/gist\");\n\nDrop = require(\"./lib/drop\");\n\nReadFile = require(\"./lib/read-file\");\n\nMidi2Composer = require(\"./lib/midi2composer\");\n\nPublishTemplate = require(\"./templates/publish\");\n\nclipboardCopy = require(\"./lib/clipboard-copy\");\n\n_ref = system.ui, Modal = _ref.Modal, Progress = _ref.Progress;\n\npublishView = Progress({\n message: \"Publishing...\"\n});\n\nmodule.exports = function(I, self) {\n var fileInput, jsonFromFile, prompted, unloadHandler;\n if (I == null) {\n I = {};\n }\n defaults(I, {\n unsaved: false\n });\n self.attrAccessor(\"unsaved\");\n jsonFromFile = function(file) {\n if (file.type.match(/^audio\\/mid/)) {\n if (self.purchased()) {\n return file.readAsArrayBuffer().then(function(buffer) {\n return Midi2Composer(new Uint8Array(buffer));\n });\n } else {\n self.purchase();\n return Promise.reject(\"Only full version can import MIDI\");\n }\n } else {\n return file.readAsJSON();\n }\n };\n Drop(document.documentElement, function(files) {\n return Promise.all(Array.prototype.map.call(files, function(file) {\n return jsonFromFile(file)[\"catch\"](function() {\n return null;\n });\n })).then(function(readFiles) {\n var json;\n json = readFiles.filter(function(f) {\n return !!f;\n })[0];\n if (json) {\n return self.fromJSON(json);\n }\n });\n });\n fileInput = ReadFile({\n accept: \".dxc,.mid,audio/mid,.midi,audio/midi\",\n change: function(file) {\n return jsonFromFile(file).then(self.fromJSON).then(function() {\n return Modal.hide();\n })[\"catch\"](console.error);\n }\n });\n fileInput.style.display = \"none\";\n document.body.appendChild(fileInput);\n prompted = false;\n unloadHandler = function(e) {\n if (prompted) {\n return;\n }\n prompted = true;\n setTimeout(function() {\n return prompted = false;\n });\n if (self.unsaved()) {\n e.returnValue = \"Your changes haven't yet been saved. If you leave now you will lose your work.\";\n }\n return e.returnValue;\n };\n window.addEventListener(\"beforeunload\", unloadHandler);\n setTimeout(function() {\n var hash;\n if (hash = location.hash) {\n return self.loadFromSlug(hash.substr(1));\n }\n }, 0);\n return self.extend({\n saveAs: function() {\n return Modal.prompt(\"Name\").then(function(name) {\n var data;\n if (!name) {\n return;\n }\n data = self.song().toJSON();\n localStorage[\"songs_\" + name] = JSON.stringify(data);\n return self.unsaved(false);\n });\n },\n saveFile: function() {\n return Modal.prompt(\"Name\", \"untitled\", \"Save Song\").then(function(name) {\n var blob;\n if (!name) {\n return;\n }\n blob = new Blob([JSON.stringify(self.song().toJSON())], {\n type: \"application/json\"\n });\n blob.download(\"\" + name + \".dxc\");\n return self.unsaved(false);\n });\n },\n openFile: function() {\n return fileInput.click();\n },\n loadFromSlug: function(slug) {\n return Promise.resolve().then(function() {\n if (slug.match(/^api-/)) {\n return self.loadAPIData(slug.substr(4));\n } else if (slug) {\n return self.loadGist(slug);\n }\n });\n },\n loadURL: function(url) {\n return fetch(url).then(function(result) {\n return result.json();\n }).then(self.fromJSON);\n },\n loadFromURLString: function() {\n return Modal.prompt(\"Paste shareable URL here:\", \"https://danielx.net/composer/#api-4IuXACKoqtSke9jTAThC5I_eWc4me53ze-1cri-L8kE\", \"Load from URL\").then(function(url) {\n var slug, _, _ref1;\n if (!url) {\n return;\n }\n if (url.indexOf(\"#\")) {\n _ref1 = url.split(\"#\"), _ = _ref1[0], slug = _ref1[1];\n return self.loadFromSlug(slug);\n } else {\n return self.loadURL(url);\n }\n });\n },\n loadDemo: function() {\n return self.confirmUnsaved().then(function() {\n Modal.hide();\n return self.fromJSON(require(\"./data/demo\"));\n });\n },\n loadLocalStorage: function() {\n return Modal.prompt(\"Name\").then(function(name) {\n if (!name) {\n return;\n }\n if (name) {\n return self.fromJSON(JSON.parse(localStorage[\"songs_\" + name]));\n }\n });\n },\n getState: function() {\n return JSON.parse(JSON.stringify(self.song().toJSON()));\n },\n restoreState: function(data) {\n self.song().fromJSON(JSON.parse(JSON.stringify(data)));\n return self.rerenderNotes();\n },\n confirmUnsaved: function() {\n return Promise.resolve().then(function() {\n if (self.unsaved()) {\n return Modal.confirm(\"If you continue your changes will be lost!\", \"Unsaved changes!\").then(function(confirmed) {\n if (!confirmed) {\n return Promise.reject();\n }\n });\n }\n });\n },\n fromJSON: function(data) {\n return self.confirmUnsaved().then(function() {\n return self._fromJSON(data);\n });\n },\n _fromJSON: function(data) {\n self.song().fromJSON(data);\n self.reset();\n return self.rerenderNotes();\n },\n removeUnloadHandler: function() {\n return window.removeEventListener(\"beforeunload\", unloadHandler);\n },\n publish: function() {\n Modal.show(publishView.element, {\n cancellable: false\n });\n return fetch(\"https://api.danielx.net/composer\", {\n method: 'POST',\n body: JSON.stringify(self.song()),\n headers: {\n 'Content-Type': 'text/plain'\n }\n }).then(function(res) {\n return res.json();\n }).then(function(id) {\n var e, url;\n url = \"https://danielx.net/composer/#api-\" + id;\n try {\n history.replaceState({}, \"\", url);\n } catch (_error) {\n e = _error;\n console.warn(e);\n }\n return Modal.show(PublishTemplate({\n value: url,\n status: Observable(\"\"),\n copy: function() {\n clipboardCopy(url);\n return this.status(\"Copied!\");\n },\n done: function() {\n return Modal.hide();\n }\n }), {\n cancellable: false\n });\n })[\"catch\"](function(e) {\n console.warn(e);\n return Modal.hide();\n });\n },\n loadAPIData: function(id) {\n return fetch(\"https://api.danielx.net/composer/data/\" + id, {\n method: 'GET',\n headers: {\n 'Content-Type': 'text/plain'\n }\n }).then(function(res) {\n return res.json();\n }).then(function(data) {\n self.fromJSON(data);\n return location.hash = \"api-\" + id;\n });\n },\n loadGist: function(id) {\n return Gist.load(id).then(function(data) {\n self.fromJSON(data);\n return location.hash = id;\n }, function() {\n return Modal.alert(\"Couldn't load gist with id: \" + id);\n });\n },\n loadGistPrompt: function() {\n var id;\n if (id = prompt(\"Gist id\", location.hash.substr(1) || \"0b4c4656a6eb1d246829\")) {\n return self.loadGist(id);\n }\n }\n });\n};\n"