diff --git a/index.html b/index.html
index 3b99af4..35e5209 100755
--- a/index.html
+++ b/index.html
@@ -37,7 +37,7 @@
"content": "AudioContext = window.AudioContext or window.webkitAudioContext\n\nmodule.exports = new AudioContext\n sampleRate: 44100\n"
},
"main.coffee": {
- "content": "require \"./lib/extensions\"\n\nglobal.player = require(\"./player\")()\n\n# Apply SystemClient UI Style\n{ util } = system\nutil.applyStyle system.ui.Style.all, \"system\"\n# Add our style after player so we can override the base UI styles\nutil.applyStyle require(\"./style\"), \"app\"\n\ndocument.body.appendChild player.element\n\n# Remove right click context menu from canvases\nArray::forEach.call document.querySelectorAll('canvas'), (element) ->\n element.oncontextmenu = (e) -> e.preventDefault()\n\n# Preload Box Art Image\nimg = new Image\nimg.src = \"https://danielx.net/composer/box-art.jpg\"\n\n# Service Worker\n# TODO: this probably needs to be added to the HTML prometheus publishes\ntry\n # Can't register in blob urls or other places where there is no host\n # Don't register in Electron\n if location.host and !global.ELECTRON_VERSION\n navigator.serviceWorker.register('./sw.js')\ncatch e\n console.warn e.message\n\n# Display purchase prompt every 15 minutes on free version\nsetInterval ->\n unless player.purchased()\n player.purchase()\n, 1000 * 60 * 15\n\n# Electron App Exit Button and purchased state\nif global.ELECTRON_VERSION\n ButtonTemplate = require \"./templates/button\"\n {remote} = global.require(\"electron\")\n\n # Set purchased state\n player.purchased(true)\n\n # Override beforeUnload for the electron world\n player.removeUnloadHandler()\n closing = false\n window.addEventListener \"beforeunload\", (e) ->\n if closing\n return\n\n player.confirmUnsaved()\n .then ->\n closing = true\n 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: ->\n remote.getCurrentWindow().close()\n"
+ "content": "require \"./lib/extensions\"\n\nglobal.player = require(\"./player\")()\n\n# Apply SystemClient UI Style\n{ util } = system\nutil.applyStyle system.ui.Style.all, \"system\"\n# Add our style after player so we can override the base UI styles\nutil.applyStyle require(\"./style\"), \"app\"\n\ndocument.body.appendChild player.element\n\n# Remove right click context menu from canvases\nArray::forEach.call document.querySelectorAll('canvas'), (element) ->\n element.oncontextmenu = (e) -> e.preventDefault()\n\n# Preload Box Art Image\nimg = new Image\nimg.src = \"assets/box-art.jpg\"\n\n# Service Worker\n# TODO: this probably needs to be added to the HTML prometheus publishes\ntry\n # Can't register in blob urls or other places where there is no host\n # Don't register in Electron\n if location.host and !global.ELECTRON_VERSION\n navigator.serviceWorker.register('./sw.js')\ncatch e\n console.warn e.message\n\n# Display purchase prompt every 15 minutes on free version\nsetInterval ->\n unless player.purchased()\n player.purchase()\n, 1000 * 60 * 15\n\n# Electron App Exit Button and purchased state\nif global.ELECTRON_VERSION\n ButtonTemplate = require \"./templates/button\"\n {remote} = global.require(\"electron\")\n\n # Set purchased state\n player.purchased(true)\n\n # Override beforeUnload for the electron world\n player.removeUnloadHandler()\n closing = false\n window.addEventListener \"beforeunload\", (e) ->\n if closing\n return\n\n player.confirmUnsaved()\n .then ->\n closing = true\n 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: ->\n remote.getCurrentWindow().close()\n"
},
"persistence.coffee": {
"content": "Gist = require \"./lib/legacy/gist\"\nDrop = require \"./lib/drop\"\nReadFile = require \"./lib/read-file\"\nMidi2Composer = require \"./lib/midi2composer\"\n\nPublishTemplate = require \"./templates/publish\"\nclipboardCopy = require \"./lib/clipboard-copy\"\n\n{Modal, Progress} = system.ui\n\npublishView = Progress\n message: \"Publishing...\"\n\nmodule.exports = (I={}, self) ->\n # Autoload from location hash\n defaults I,\n unsaved: false\n\n self.attrAccessor \"unsaved\"\n\n jsonFromFile = (file) ->\n if file.type.match /^audio\\/mid/\n if self.purchased()\n file.readAsArrayBuffer()\n .then (buffer) ->\n Midi2Composer new Uint8Array(buffer)\n else\n # Display Purchase Modal\n self.purchase()\n Promise.reject(\"Only full version can import MIDI\")\n else\n file.readAsJSON()\n\n Drop document.documentElement, (files) ->\n Promise.all Array::map.call files, (file) ->\n jsonFromFile(file)\n .catch -> null\n .then (readFiles) ->\n json = readFiles.filter((f) -> !!f)[0]\n\n if json\n self.fromJSON(json)\n\n fileInput = ReadFile\n accept: \".dxc,.mid,audio/mid,.midi,audio/midi\"\n change: (file) ->\n jsonFromFile(file)\n .then self.fromJSON\n .then ->\n Modal.hide()\n .catch console.error\n\n fileInput.style.display = \"none\"\n document.body.appendChild fileInput\n\n # NOTE: Track `prompted` so in an iframe it won't trigger twice\n prompted = false\n unloadHandler = (e) ->\n return if prompted\n prompted = true\n setTimeout ->\n 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 window.addEventListener \"beforeunload\", unloadHandler\n\n setTimeout ->\n if hash = location.hash\n self.loadFromSlug hash.substr(1)\n , 0\n\n self.extend\n saveAs: ->\n Modal.prompt \"Name\"\n .then (name) ->\n return unless name\n data = self.song().toJSON()\n localStorage[\"songs_#{name}\"] = JSON.stringify data\n self.unsaved false\n\n saveFile: ->\n Modal.prompt \"Name\", \"untitled\", \"Save Song\"\n .then (name) ->\n return unless name\n\n blob = new Blob [JSON.stringify self.song().toJSON()],\n type: \"application/json\"\n\n blob.download(\"#{name}.dxc\")\n self.unsaved false\n\n openFile: ->\n fileInput.click()\n\n loadFromSlug: (slug) ->\n Promise.resolve()\n .then ->\n if slug.match /^api-/\n self.loadAPIData(slug.substr(4))\n else if slug\n self.loadGist slug\n\n loadURL: (url) ->\n fetch(url)\n .then (result) ->\n result.json()\n .then self.fromJSON\n\n loadFromURLString: ->\n Modal.prompt \"Paste shareable URL here:\", \"https://danielx.net/composer/#api-4IuXACKoqtSke9jTAThC5I_eWc4me53ze-1cri-L8kE\", \"Load from URL\"\n .then (url) ->\n return unless url\n if url.indexOf(\"#\")\n [_, slug] = url.split(\"#\")\n self.loadFromSlug(slug)\n else\n self.loadURL(url)\n\n loadDemo: ->\n self.confirmUnsaved()\n .then ->\n Modal.hide()\n self.fromJSON(require(\"./data/demo\"))\n\n loadLocalStorage: ->\n Modal.prompt \"Name\"\n .then (name) ->\n return unless name\n\n if name\n self.fromJSON JSON.parse localStorage[\"songs_#{name}\"] \n\n getState: ->\n JSON.parse JSON.stringify self.song().toJSON()\n\n restoreState: (data) ->\n self.song().fromJSON(JSON.parse JSON.stringify data)\n self.rerenderNotes()\n\n # Returns a promise that resolves if there are no unsaved changes or the\n # the user confirms they are willing to lose unsaved changes.\n confirmUnsaved: ->\n Promise.resolve()\n .then ->\n if self.unsaved()\n Modal.confirm \"If you continue your changes will be lost!\", \"Unsaved changes!\"\n .then (confirmed) ->\n return Promise.reject() unless confirmed\n\n fromJSON: (data) ->\n self.confirmUnsaved()\n .then ->\n self._fromJSON(data)\n\n _fromJSON: (data) ->\n self.song().fromJSON(data)\n\n self.reset()\n self.rerenderNotes()\n\n removeUnloadHandler: ->\n window.removeEventListener \"beforeunload\", unloadHandler\n\n publish: ->\n Modal.show publishView.element,\n cancellable: false\n\n fetch \"https://api.danielx.net/composer\",\n method: 'POST'\n body: JSON.stringify(self.song())\n headers:\n 'Content-Type': 'text/plain'\n .then (res) -> res.json()\n .then (id) ->\n url = \"https://danielx.net/composer/#api-#{id}\"\n\n try\n history.replaceState({}, \"\", url)\n catch e\n console.warn e\n\n Modal.show PublishTemplate(\n value: url\n status: Observable \"\"\n copy: ->\n clipboardCopy(url)\n @status \"Copied!\"\n done: ->\n Modal.hide()\n ),\n cancellable: false\n .catch (e) ->\n console.warn e\n Modal.hide()\n\n loadAPIData: (id) ->\n fetch \"https://api.danielx.net/composer/data/#{id}\",\n method: 'GET'\n headers:\n 'Content-Type': 'text/plain'\n .then (res) -> res.json()\n .then (data) ->\n self.fromJSON(data)\n location.hash = \"api-\" + id\n\n # Load legacy format songs from when we could save them as anonymous Github\n # gists.\n loadGist: (id) ->\n Gist.load(id).then (data) ->\n self.fromJSON(data)\n location.hash = id\n , ->\n Modal.alert \"Couldn't load gist with id: #{id}\"\n\n loadGistPrompt: ->\n if id = prompt \"Gist id\", location.hash.substr(1) || \"0b4c4656a6eb1d246829\"\n self.loadGist(id)\n\n"
@@ -52,7 +52,7 @@
"content": "# Player is the `app`. It requires and includes all the components then renders\n# them into the templates.\n\nrequire \"./lib/extensions\"\n\n{Progress, Modal} = system.ui\n\nExportTemplate = require \"./templates/export\"\nOptionTemplate = require \"./templates/option\"\n\nSample = require \"./sample\"\nSong = require \"./song-v2\"\n\nStaffView = require \"./staff-view\"\n\nPattern = require \"./lib/pattern\"\nUndo = require \"./lib/undo\"\n\n{purchase} = require \"./lib/stripe-checkout\"\n\nmodule.exports = (I={}, self=Model(I)) ->\n defaults I,\n patterns: []\n\n self.include require \"./lib/hotkeys\"\n self.include require \"./lib/midi-input\"\n\n self.attrObservable \"activeSection\", \"purchased\"\n\n self.attrModels \"patterns\", Pattern\n self.attrModel \"song\", Song\n\n song = self.song()\n\n # Loading default sample pack\n Sample.loadPack()\n .then song.loadSettings\n .then ->\n self.applySampleCSS Sample.image\n\n self.extend\n notePosition: Observable \"\"\n\n samples: song.settings\n\n addNote: (note) ->\n self.unsaved true\n self.song().addNote note\n self.pushState self.getState()\n\n removeNote: (note, nearby) ->\n self.unsaved true\n removed = self.song().removeNote note, nearby\n self.pushState self.getState()\n \n return removed\n\n addPattern: (beatNote, pattern) ->\n self.unsaved true\n self.song().addPattern beatNote, pattern\n self.pushState self.getState()\n self.triggerRerender()\n\n deleteRange: (range) ->\n self.unsaved true\n self.song().deleteRange range\n\n self.pushState self.getState()\n self.triggerRerender()\n\n copyRange: (range) ->\n pattern = self.song().copyRange range\n\n if pattern.notes().length\n self.activeToolIndex 3 # Pattern palette\n # Create and select pattern\n self.activePatternIndex self.patterns.push(pattern) - 1\n\n moveNotes: (notes, beatDelta, staffDelta) ->\n self.unsaved true\n\n # Adjust every note\n notes.forEach (note) ->\n note[0] += beatDelta\n note[1] += staffDelta\n\n # Notes need to get re-sectioned if they cross a section boundary!\n if beatDelta != 0\n self.song().resection(notes)\n\n self.pushState self.getState()\n self.triggerRerender(true)\n\n exportSprites: ->\n Sample.exportPNG(self.samples())\n\n # Create a style element containing the background images for notes from a\n # list of samples. Applies background image when the class of the note is\n # the sample name.\n applySampleCSS: (image) ->\n style = document.querySelector \"style.samples\"\n style ?= document.createElement \"style\"\n style.classList.add \"samples\"\n n = image.width / 48\n style.innerHTML = [0...n].map (i) ->\n x = -i * 48\n \"\"\"\n note.i#{i}:after {background: url(#{image.src}) #{x}px 0;}\n note.i#{i}.active:after {background: url(#{image.src}) #{x}px 48px;}\n tools > .i#{i} {background: url(#{image.src}) #{x}px 0px;}\n tools > .i#{i}.active {background: #FFC107 url(#{image.src}) #{x}px 48px;}\n \"\"\"\n .join(\"\\n\")\n document.head.appendChild(style)\n\n loop: song.loop\n toggleLoop: ->\n self.loop.toggle()\n loopButtonClass: ->\n \"active\" if self.loop()\n\n playButtonClass: ->\n \"active\" if self.playing()\n\n # Delegating to song\n length: song.length\n sections: song.sections\n sectionAt: song.sectionAt\n\n # Delegate to active section\n tempo: ->\n self.activeSection().tempo.apply(null, arguments)\n presetName: ->\n self.activeSection().presetName.apply(null, arguments)\n keySignature: ->\n self.activeSection().keySignature.apply(null, arguments)\n timeSignature: ->\n self.activeSection().timeSignature.apply(null, arguments)\n\n # TODO: Clear song vs clear section\n clearDisabled: ->\n !self.activeSection().notes().length\n\n clear: ->\n Modal.confirm \"Clear entire song?\"\n .then (confirmed) ->\n if confirmed\n song.clear()\n self.unsaved true\n self.pushState self.getState()\n self.triggerRerender()\n\n about: ->\n Modal.show aboutTemplate,\n cancellable: true\n\n showHelp: ->\n Modal.show helpTemplate,\n cancellable: true\n\n showPersistenceModal: ->\n Modal.show persistenceElement\n\n hiddenIfPurchased: ->\n \"hidden\" if self.purchased()\n\n hiddenUnlessPurchased: ->\n \"hidden\" unless self.purchased()\n\n purchase: ->\n Modal.show buyNowTemplate,\n cancellable: true\n\n discord: ->\n window.open \"https://discord.gg/wcpWDNk\", \"_blank\"\n\n oldVersion: ->\n window.location = \"https://danielx.net/composer/0.2.0-pre.0/\"\n\n exportAudio: ->\n name = Observable \"song\"\n selectedType = Observable \"mp3\"\n formatTypes = [\"mp3\", \"wav\"]\n\n Modal.show ExportTemplate\n name: name\n title: generateExportTitle()\n selectedType: selectedType\n formatOptionElements: ->\n formatTypes.map (type) ->\n OptionTemplate\n text: type\n value: type\n\n cancel: (e) ->\n e.preventDefault()\n Modal.hide()\n submit: (e) ->\n e.preventDefault()\n\n self.exportSong self.song(),\n name: name()\n type: selectedType()\n\n upcomingNotes: song.upcomingNotes\n\n fullscreen: Observable document.fullscreenElement\n\n loadFromURL: (url) ->\n progressView = Progress\n value: 0\n message: \"Loading...\"\n\n Modal.show progressView.element,\n cancellable: false\n\n ajax.ajax\n url: url\n responseType: \"json\"\n .progress ({lengthComputable, loaded, total}) ->\n if lengthComputable\n progressView.value loaded / total\n .then self.fromJSON\n .then ->\n Modal.hide()\n .catch (e) ->\n if e.statusText\n Modal.alert \"An error has occurred: #{e.status} - #{e.statusText}\"\n else\n Modal.alert \"An error has occurred: #{e.message}\"\n\n # This is just copied from the README, update there, it is the canonical source\n hotkeysTableElement: ->\n div = document.createElement 'div'\n div.innerHTML = \"\"\"\n
\n \n \n | Action | \n Key | \n
\n \n \n | Select Instrument | \n 0-9 | \n
\n \n | Select Instrument | \n <backtick> 1-7 | \n
\n \n | Select Pattern | \n Shift+0-9 | \n
\n \n | Eraser Tool | \n e | \n
\n \n | Selection Tool | \n s | \n
\n \n | Undo | \n Ctrl+z, ⌘+z | \n
\n \n | Redo | \n Ctrl+y, ⌘+y | \n
\n \n | Play/Pause | \n Space | \n
\n \n | Play from Beginning | \n Enter | \n
\n \n | | \n | \n
\n \n | Selection | \n | \n
\n \n | Copy | \n c | \n
\n \n | Cut | \n x | \n
\n \n | Delete | \n Delete | \n
\n \n | Reset | \n Esc | \n
\n \n | | \n | \n
\n \n | Data | \n | \n
\n \n | Save | \n Ctrl+s, ⌘+s | \n
\n \n | Open | \n Ctrl+o, ⌘+o | \n
\n \n | Export | \n Ctrl+r, ⌘+r | \n
\n \n | | \n | \n
\n \n | Meta | \n | \n
\n \n | About | \n F1 | \n
\n \n | Toggle Full screen | \n F11 | \n
\n \n | Help | \n ? | \n
\n
\n \"\"\"\n return div.children[0]\n\n self.include require \"./player-audio\"\n self.include require \"./persistence\"\n self.include require \"./tools\"\n self.include StaffView\n\n Undo(self)\n self.pushState self.getState()\n\n # Init active section\n self.activeSection self.sections()[0]\n\n # This common convention passes self as a model to a sub-view and attaches the\n # view's element to be rendered in our templates.\n self.noteControlElement = require(\"./views/note-control\")(self).element\n\n # Analyser View\n # stereoAnalyserView = require(\"./views/stereo-analyser\")(context)\n # self.stereoAnalyserElement = stereoAnalyserView.element\n\n # Meter Picker\n MeterPicker = require(\"./views/meter-picker\")\n self.meterPickerElement = MeterPicker(self).element\n\n # FX Picker\n FXPicker = require(\"./views/fx-picker\")\n self.fxPickerElement = FXPicker({\n presetName: self.presetName\n }).element\n\n # TODO: Make fx bg part of section\n # Update background style and fx preset when the chosen preset changes\n Observable ->\n name = self.presetName()\n self.fxStyle FXPicker.styleFor(name)\n\n # Each section display its own meter background based on the time signature\n # The classes are n2 - n7 based on the numerator of the time signature\n require(\"./lib/meter-background\")\n .then (timeSignatureMeterBackgroundUrls) ->\n style = document.createElement \"style\"\n style.classList.add \"meter\"\n style.innerHTML = Object.keys(timeSignatureMeterBackgroundUrls).map (n) ->\n url = timeSignatureMeterBackgroundUrls[n]\n \"\"\"\n song-section.n#{n} {background-image: url(#{url});}\n \"\"\"\n .join(\"\\n\")\n document.head.appendChild(style)\n\n # Demo song picker\n require(\"./views/demo-picker\")(self)\n\n # Sample / Sprite Config\n require(\"./views/settings\")(self)\n\n # Arranger\n require(\"./views/arranger\")(self)\n\n Template = require(\"./templates/app\")\n self.element = Template\n actionsElement: require(\"./views/actions\")(self).element\n toolsElement: require(\"./views/tools\")(self).element\n patternsElement: require(\"./views/patterns\")(self).element\n staffElement: self.staffElement\n notePosition: self.notePosition\n\n aboutTemplate = require(\"./templates/about\")(self)\n helpTemplate = require(\"./templates/help\")(self)\n buyNowTemplate = require(\"./templates/buy-now\")\n submit: (e) ->\n e.preventDefault()\n purchase()\n\n persistenceElement = require(\"./templates/persistence\")(self)\n\n animate ->\n self.performDraw()\n\n # Rerender on length changes\n Observable ->\n song.length()\n self.rerenderNotes()\n\n return self\n\nrand = (a) ->\n a[Math.floor Math.random() * a.length]\n\ngenerateExportTitle = ->\n adjective = rand [\n \"cool\"\n \"rad\"\n \"kickin'\"\n \"bumpin'\"\n \"sweet\"\n \"tasty\"\n ]\n\n noun = rand [\n \"banger\"\n \"track\"\n \"song\"\n \"tune\"\n \"jam\"\n ]\n\n \"Export your #{adjective} #{noun}\"\n\nanimate = (fn) ->\n step = ->\n requestAnimationFrame(step)\n fn()\n\n step()\n"
},
"sample.coffee": {
- "content": "context = require \"./lib/audio-context\"\n\nbufferLoader = (url) ->\n new Promise (resolve, reject) ->\n fetch(url)\n .then (response) ->\n throw response unless response.ok\n response.arrayBuffer()\n .then (buffer) ->\n context.decodeAudioData buffer, resolve, reject\n\nblobLoader = (url) ->\n fetch(url)\n .then (response) ->\n throw response unless response.ok\n response.blob()\n\nBASE_PATH = \"https://danielx.net/composer/\"\nurlFor = (path) ->\n \"#{BASE_PATH}#{path}\"\n\nimgLoader = (url) ->\n new Promise (resolve, reject) ->\n image = new Image\n image.crossOrigin = true\n image.src = url\n image.onload = ->\n resolve image\n image.onerror = reject\n\nmodule.exports = Sample =\n load: (data) ->\n {sample} = data\n\n # Load audio buffer\n bufferLoader(urlFor(sample))\n .then (buffer) ->\n Object.assign data,\n buffer: buffer\n\n loadPack: ->\n imgLoader urlFor \"images/default.png\"\n .then (img) ->\n # TODO: a cleaner way to pass this to applySampleCSS\n Sample.image = img\n\n Promise.all defaultPack.map (sample, i) ->\n # Get blob url\n canvas = document.createElement 'canvas'\n canvas.width = 48\n canvas.height = 48\n ctx = canvas.getContext('2d')\n\n ctx.drawImage(img, i * 48, 0, 48, 48, 0, 0, 48, 48)\n\n new Promise (resolve, reject) ->\n canvas.toBlob (blob) ->\n url = URL.createObjectURL blob\n sample.cursor = \"url(#{url}) #{24} #{24}, default\"\n sample.url = url\n resolve()\n\n .then ->\n Promise.all(defaultPack.map(Sample.load))\n\n # Export all image/activeImage sprites to a single png\n # This is going to be part of the future format\n exportPNG: (samples) ->\n canvas = document.createElement 'canvas'\n canvas.width = 48 * samples.length\n canvas.height = 96\n ctx = canvas.getContext('2d')\n\n # All images are 48x48\n samples.forEach ({image, activeImage}, i) ->\n x = 48 * i\n ctx.drawImage(image, x, 0) #normal\n ctx.drawImage(activeImage, x, 48) # active y=48\n\n canvas.toBlob (blob) ->\n url = URL.createObjectURL(blob)\n window.open url, \"_blank\"\n\n# Load Editor sound effects, currently just the eraser\nbufferLoader(\"#{BASE_PATH}erase2.wav\")\n.then (buffer) ->\n Sample.fx =\n eraser: buffer\n\nsamples =\n synth:\n sample: \"synth.wav\"\n piano:\n sample: \"piano.wav\"\n pan: -0.5\n guitar:\n sample: \"guitar.wav\"\n pan: 0.25\n bass:\n sample: \"16.wav\"\n horn:\n sample: \"horn.wav\"\n pan: -0.333\n orch_hit:\n sample: \"orch_hit.wav\"\n pan: 0.25\n chime:\n sample: \"5.wav\"\n pan: 0.5\n organ:\n sample: \"organ.wav\"\n pan: -0.25\n drum:\n sample: \"drum.wav\"\n snare:\n sample: \"snare.wav\"\n woodblock:\n sample: \"14.wav\"\n pan: -0.333\n clap:\n sample: \"clap.wav\"\n hat:\n sample: \"hat.wav\"\n pan: 0.333\n baby:\n sample: \"baby.wav\"\n pan: -0.5\n yoshi:\n sample: \"yoshi.wav\"\n pan: 0.5\n pig:\n sample: \"oink.wav\"\n cat:\n sample: \"cat.wav\"\n pan: 0.25\n dog:\n sample: \"dog.wav\"\n pan: -0.25\n french:\n sample: \"french-horn.wav\"\n pan: 0.333\n pitchShift: 0\n nylon:\n sample: \"nylon-guitar2.wav\"\n pan: -0.25\n pitchShift: -12\n snare2:\n sample: \"snare2.wav\"\n pitchShift: 0\n\ndefaultPack = Object.keys(samples).map (name, i) ->\n sample = samples[name]\n sample.name = name\n sample.index = i\n sample.pitchShift ?= -5 # These samples are pitched to F, the -5 pithces them to C\n sample.pan ?= 0\n sample.volume ?= 1\n\n sample\n"
+ "content": "context = require \"./lib/audio-context\"\n\nbufferLoader = (url) ->\n new Promise (resolve, reject) ->\n fetch(url)\n .then (response) ->\n throw response unless response.ok\n response.arrayBuffer()\n .then (buffer) ->\n context.decodeAudioData buffer, resolve, reject\n\nblobLoader = (url) ->\n fetch(url)\n .then (response) ->\n throw response unless response.ok\n response.blob()\n\nBASE_PATH = window.location\nurlFor = (path) ->\n \"#{BASE_PATH}assets/#{path}\"\n\nimgLoader = (url) ->\n new Promise (resolve, reject) ->\n image = new Image\n image.crossOrigin = true\n image.src = url\n image.onload = ->\n resolve image\n image.onerror = reject\n\nmodule.exports = Sample =\n load: (data) ->\n {sample} = data\n\n # Load audio buffer\n bufferLoader(urlFor(sample))\n .then (buffer) ->\n Object.assign data,\n buffer: buffer\n\n loadPack: ->\n imgLoader urlFor \"images/default.png\"\n .then (img) ->\n # TODO: a cleaner way to pass this to applySampleCSS\n Sample.image = img\n\n Promise.all defaultPack.map (sample, i) ->\n # Get blob url\n canvas = document.createElement 'canvas'\n canvas.width = 48\n canvas.height = 48\n ctx = canvas.getContext('2d')\n\n ctx.drawImage(img, i * 48, 0, 48, 48, 0, 0, 48, 48)\n\n new Promise (resolve, reject) ->\n canvas.toBlob (blob) ->\n url = URL.createObjectURL blob\n sample.cursor = \"url(#{url}) #{24} #{24}, default\"\n sample.url = url\n resolve()\n\n .then ->\n Promise.all(defaultPack.map(Sample.load))\n\n # Export all image/activeImage sprites to a single png\n # This is going to be part of the future format\n exportPNG: (samples) ->\n canvas = document.createElement 'canvas'\n canvas.width = 48 * samples.length\n canvas.height = 96\n ctx = canvas.getContext('2d')\n\n # All images are 48x48\n samples.forEach ({image, activeImage}, i) ->\n x = 48 * i\n ctx.drawImage(image, x, 0) #normal\n ctx.drawImage(activeImage, x, 48) # active y=48\n\n canvas.toBlob (blob) ->\n url = URL.createObjectURL(blob)\n window.open url, \"_blank\"\n\n# Load Editor sound effects, currently just the eraser\nbufferLoader(\"#{BASE_PATH}erase2.wav\")\n.then (buffer) ->\n Sample.fx =\n eraser: buffer\n\nsamples =\n synth:\n sample: \"synth.wav\"\n piano:\n sample: \"piano.wav\"\n pan: -0.5\n guitar:\n sample: \"guitar.wav\"\n pan: 0.25\n bass:\n sample: \"16.wav\"\n horn:\n sample: \"horn.wav\"\n pan: -0.333\n orch_hit:\n sample: \"orch_hit.wav\"\n pan: 0.25\n chime:\n sample: \"5.wav\"\n pan: 0.5\n organ:\n sample: \"organ.wav\"\n pan: -0.25\n drum:\n sample: \"drum.wav\"\n snare:\n sample: \"snare.wav\"\n woodblock:\n sample: \"14.wav\"\n pan: -0.333\n clap:\n sample: \"clap.wav\"\n hat:\n sample: \"hat.wav\"\n pan: 0.333\n baby:\n sample: \"baby.wav\"\n pan: -0.5\n yoshi:\n sample: \"yoshi.wav\"\n pan: 0.5\n pig:\n sample: \"oink.wav\"\n cat:\n sample: \"cat.wav\"\n pan: 0.25\n dog:\n sample: \"dog.wav\"\n pan: -0.25\n french:\n sample: \"french-horn.wav\"\n pan: 0.333\n pitchShift: 0\n nylon:\n sample: \"nylon-guitar2.wav\"\n pan: -0.25\n pitchShift: -12\n snare2:\n sample: \"snare2.wav\"\n pitchShift: 0\n\ndefaultPack = Object.keys(samples).map (name, i) ->\n sample = samples[name]\n sample.name = name\n sample.index = i\n sample.pitchShift ?= -5 # These samples are pitched to F, the -5 pithces them to C\n sample.pan ?= 0\n sample.volume ?= 1\n\n sample\n"
},
"style.styl": {
"content": "primary-color-dark = #4a148c\nprimary-color = #673ab7\nhighlight-color = #FFE0B2\nactive-color = #FFC107\nbg-color = #ede7f6\n\npixelated()\n -ms-interpolation-mode: nearest-neighbor\n image-rendering: crisp-edges\n image-rendering: pixelated\n\n@font-face\n font-display: auto\n font-family: 'Chicago'\n src: url('https://danielx.net/fonts/chicago.woff2') format('woff2'),\n url('https://danielx.net/fonts/chicago.woff') format('woff')\n font-weight: normal\n font-style: normal\n\n*\n box-sizing: border-box\n\nimg\n max-width: 100%\n\n.hidden\n display: none !important\n\n#modal \n > *\n border: 1px solid primary-color\n border-radius: 4px\n box-shadow: 1px 2px 0px primary-color\n color: inherit\n padding: 1rem\n\n > h1, > h2\n margin-top: 0\n\n > section.purchase\n background-color: transparent\n border: none\n border-radius: 0\n box-shadow: none\n padding: 0\n\n > .publish\n > p.status:empty\n margin: 0\n \n > pre\n user-select: all\n \n > actions\n display: flex\n > button:last-child\n margin-left: auto\n\n:focus\n color: white\n background-color: primary-color\n outline: none\n\nhtml, body\n height: 100%\n\nbody\n color: #241440\n display: flex\n font-family: Chicago, sans-serif\n font-size: 16px\n line-height: 1rem\n margin: 0\n overflow: hidden\n user-select: none\n\np\n font-family: sans-serif\n\n// Input and form things\ninput, textarea, select, button\n font-family: inherit\n\ninput\n background-color: bg-color\n border: 1px solid primary-color\n border-radius: 4px\n box-shadow: 1px 2px 0px primary-color inset\n color: primary-color\n font-size: inherit\n padding: 2px 0.25em\n\n // Firefox number spinners are a crime to my eyes ;_;\n // TODO Custom style for number spinners on all browsers\n &[type=\"number\"]\n -moz-appearance: textfield\n \n &:focus\n background-color: active-color\n color: rgba(0, 0, 0, 0.69)\n\n@keyframes note-active\n from\n background-color: rgba(0, 0, 0, 0)\n\n to\n background-color: rgba(255, 0, 255, 0)\n\n// Background has to be on ::after so it is above the ledger lines on ::before\n// Accidentals are in ::after content, vertically centered and left of the bg\nnote\n font-size: 48px\n height: 48px\n pixelated()\n position: absolute\n width: 48px\n\n &.active\n animation-name: note-active\n animation-duration: 0.25s\n\n &::after\n align-items: center\n background-repeat: no-repeat\n background-position: 100% 50%\n content: \"\"\n display: flex\n position: absolute\n left: 0\n top: 0\n width: 100%\n height: 100%\n text-indent: -12px\n\n &.â™\n &::after\n content: \"â™\"\n\n &.♯\n &::after\n content: \"♯\"\n\n // Extra ledger lines\n &.C4, &.A5, &.C6, &.C2, &.E2, &.B5, &.D2\n &::before\n content: \"\"\n width: 48px\n left: 0px\n top: 23px\n position: absolute\n height: 0\n border-bottom: 2px solid black\n\n &.B5\n &::before\n top: 40px\n\n &.D2\n &::before\n top: -8px\n\nsong-section\n height: 483px\n position: absolute\n top: -241px\n\n &::after\n content: \"\"\n border-right: 2px solid black\n height: 100%\n position: absolute\n right: 10px\n\n > span.measure-number\n background-color: rgba(255, 255, 255, 0.9375)\n border: 1px solid black\n box-shadow: 1px 1px 0 0 rgba(0,0,0,0.5)\n font-style: italic\n left: 0px\n padding: 2px 6px 2px 4px\n position: absolute\n top: -132px\n\n > div.key-signature\n &.s > ::after\n content: \"♯\"\n\n &.f > ::after\n content: \"â™\"\n\n > *\n display: none\n font-size: 96px\n height: 48px\n position: absolute\n width: 48px\n\n &::after\n align-items: center\n display: flex\n height: 100%\n width: 100%\n\n &.s1, &.s2, &.s3, &.s4, &.s5, &.s6, &.s7\n > :nth-child(1)\n display: initial\n top: -24px\n left: -96px\n &.s2, &.s3, &.s4, &.s5, &.s6, &.s7\n > :nth-child(2)\n display: initial\n top: 48px\n left: -72px\n &.s3, &.s4, &.s5, &.s6, &.s7\n > :nth-child(3)\n display: initial\n top: -48px\n left: -48px\n &.s4, &.s5, &.s6, &.s7\n > :nth-child(4)\n display: initial\n top: 24px\n left: -24px\n &.s5, &.s6, &.s7\n > :nth-child(5)\n display: initial\n top: 96px\n left: 0\n &.s6, &.s7\n > :nth-child(6)\n display: initial\n top: 0px\n left: 24px\n &.s7\n > :nth-child(7)\n display: initial\n top: 72px\n left: 48px\n \n &.f1, &.f2, &.f3, &.f4, &.f5, &.f6, &.f7\n > :nth-child(1)\n display: initial\n top: 72px\n left: -96px\n &.f2, &.f3, &.f4, &.f5, &.f6, &.f7\n > :nth-child(2)\n display: initial\n top: 0px\n left: -72px\n &.f3, &.f4, &.f5, &.f6, &.f7\n > :nth-child(3)\n display: initial\n top: 96px\n left: -48px\n &.f4, &.f5, &.f6, &.f7\n > :nth-child(4)\n display: initial\n top: 24px\n left: -24px\n &.f5, &.f6, &.f7\n > :nth-child(5)\n display: initial\n top: -48px\n left: 0\n &.f6, &.f7\n > :nth-child(6)\n display: initial\n top: 48px\n left: 24px\n &.f7\n > :nth-child(7)\n display: initial\n top: -24px\n left: 48px\n\ntd\n > select\n width: 100%\n > input\n border-radius: 0\n box-shadow: none\n > aside.fx-picker\n > label\n display: none\n\n &.sprite\n text-align: center\n vertical-align: middle\n\n > img\n margin-right: 1rem\n vertical-align: middle\n \n &.input\n > input[type=number]\n display: block\n margin: auto\n width: 60px\n\nsection.settings\n overflow: auto\n padding: 1rem\n position: relative\n\n > h2\n margin: 0 0 1rem\n > button.close\n position: absolute\n top: 1rem\n right: 1rem\n\n > table\n margin: 0 -8px\n width: calc(100% + 16px)\n\nsection.demo-picker\n overflow: auto\n padding: 1rem\n position: relative\n\n > h2\n margin: 0 0 1rem\n > button.close\n position: absolute\n top: 1rem\n right: 1rem\n\n > table\n font-size: 18px\n > tbody\n > tr\n cursor: pointer\n line-height: 2rem\n\n &:hover\n background-color: rgba(103, 58, 183, 0.19)\n\nviewport\n background-attachment: local\n background-color: bg-color\n display: flex\n height: 100%\n align-items: center\n overflow-x: scroll\n overflow-y: hidden\n\n > staff\n background-color: rgba(255, 255, 255, 0.9375)\n border: 1px solid rgba(0, 0, 0, 0.5)\n box-sizing: content-box\n box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.5)\n display: block\n flex: 0 0 auto\n padding: 96px 0\n position: relative\n margin: 0 48px\n z-index: 0\n\n // Hack for right margin inside scrollable viewport\n // Also serving as song end bar\n &::after\n border-left: 8px solid black\n content: \"\"\n position: absolute\n right: -48px\n top: 96px\n width: 48px\n height: calc(100% - 192px)\n z-index: -1\n\n > z-meter\n display: block\n position: absolute\n left: 256px\n top: 96px\n height: calc(100% - 192px)\n width: calc(100% - 256px)\n\n &::before\n content: \"\"\n border-left: 2px solid black\n position: absolute\n height: 100%\n left: -257px\n\n > div.repeat\n position: absolute\n right: 0\n height: 100%\n\n &::before, &::after\n content: \"\"\n display: block\n background-color: black\n border-radius: 100%\n position: absolute\n width: 16px\n height: 16px\n right: 16px\n top: 66px\n\n &::after\n top: 114px\n \n &.bass\n position: relative\n top: 288px\n\n > notes\n display: block\n position: absolute\n left: 256px\n top: 337px\n\n > pattern-preview\n opacity: 0.5\n position: absolute\n\n > selection\n background-color: rgba(103, 58, 183, 0.25)\n border: 2px dashed primary-color\n position: absolute\n left: -150px\n z-index: 20\n\n > button\n display: none\n position: absolute\n top: 0\n left: 0\n right: 0\n bottom: 0\n margin: auto\n width: 48px\n height: 48px\n\n &.up\n bottom: calc(100%+4px)\n top: auto\n &.down\n top: calc(100%+4px)\n bottom: auto\n &.left\n right: calc(100%+4px)\n left: auto\n &.right\n left: calc(100%+4px)\n right: auto\n\n > actions\n display: none\n position: absolute\n\n > button\n margin-left: 4px\n\n &.t > actions\n top: 0\n &.b > actions\n bottom: 0\n &.l > actions\n left: 0\n &.r > actions\n right: 0\n\n &.set \n > actions\n display: flex\n > button\n display: block\n\n > lines\n display: block\n margin-bottom: 48px\n \n &:nth-child(2)\n margin-bottom: 0\n\n > line:last-child\n height: 0\n\n > line\n border-top: 3px solid black\n display: block\n height: 48px\n\n > playhead\n border-left: 1px solid rgba(103, 58, 183, 0.5)\n border-right: 1px solid rgba(103, 58, 183, 0.5)\n position: absolute\n top: 0\n height: 674px\n width: 2px\n z-index: 10\n\n &.buffer-start\n display: none\n border-color: rgba(255, 0, 0, 0.5)\n\n &.buffer-end\n display: none\n border-color: rgba(0, 0, 255, 0.5)\n\n > img\n position: absolute\n height: 600px\n top: -94px\n left: -120px\n\n &:last-child\n left: -100px\n top: 181px\n\nform\n > label\n display: block\n\n > h3\n font-size: 1rem\n\n &.inline\n display: inline-block\n margin-right: 0.5em\n\n > input\n padding-left: 2px\n width: 100%\n\n > actions\n display: flex\n justify-content: space-between\n margin-top: 1em\n\n &.purchase\n width: 672px\n\napp\n display: flex\n flex: 1 0\n height: 100%\n flex-direction: column\n\nbutton.loop\n font-size: 32px\n line-height: 1rem\n\naside.meter-picker\n margin-left: 8px\n > button\n border-radius: 0\n\n &:first-child\n border-top-left-radius: 4px\n border-bottom-left-radius: 4px\n\n &:last-child\n border-top-right-radius: 4px\n border-bottom-right-radius: 4px\n\naside.note-control\n > section\n display: flex\n margin-left: 1rem\n \n > label\n display: flex\n align-items: center\n margin-right: 4px\n\n > button\n flex: 0 0 auto\n font-size: 20px\n line-height: 1rem\n width: 36px\n\n border-radius: 0\n\n &.triplet\n font-size: 16px\n\n &:nth-child(n + 3)\n border-left: 0\n\n &:nth-child(2)\n border-top-left-radius: 4px\n border-bottom-left-radius: 4px\n\n &.snap\n > button\n &:nth-child(5)\n border-top-right-radius: 4px\n border-bottom-right-radius: 4px\n \n > input\n margin-left: 5px\n width: 60px\n\n &.accidental\n > button:nth-child(4)\n border-top-right-radius: 4px\n border-bottom-right-radius: 4px\n\nsection.persistence\n display: flex\n flex-direction: column\n padding: 1rem\n width: 480px\n\n > button\n margin-top: 8px\n width: 100%\n\n &:first-child\n margin-top: 0\n\nabout > actions\n > button, a.button\n width: 100%\n margin-top: 8px\n\naside\n display: flex\n > label\n display: flex\n align-items: center\n margin-right: 4px\n margin-left: 1rem\n\naside.stereo-analyser\n > canvas\n border: 1px solid #673ab7\n border-radius: 4px\n\naside.fx-picker\n display: flex\n\n > button\n background-size: 100%\n border: 1px solid #673ab7\n border-radius: 4px\n height: 36px\n padding: 0\n margin-left: 2px\n width: 36px\n\naside.actions\n background-color: white\n border-top: 2px solid primary-color\n display: flex\n flex: 0 0 auto\n flex-direction: column\n padding: 4px 4px 0px 4px\n width: 100%\n\n > section:nth-child(2)\n > button, > a.button\n padding: 15px 8px\n\n > section\n display: flex\n &:last-child\n margin-top: 8px\n\n > section.buttons\n > *\n margin-bottom: 6px\n \n &:nth-child(n + 2)\n margin-left: 4px\n \n > form\n display: flex\n \n > button, > a.button\n flex: 0 0 auto\n\n > label\n align-items: center\n border: 1px solid #673ab7\n border-radius: 4px\n box-shadow: 1px 2px 0px #673ab7\n color: #673ab7\n display: flex\n flex-direction: column\n padding: 5px 8px 4px\n white-space: nowrap\n \n > input\n padding: 2px 0 0\n text-align: center\n width: 60px\n \n > h2\n display: block\n font-size: 1em\n font-weight: normal\n margin: 0 0 4px\n \n > .right\n margin-left: auto\n\nactions > label\n background-color: white\n border 1px solid primary-color\n border-radius: 4px\n box-shadow: 1px 2px 0px primary-color\n color: #673ab7\n cursor: pointer\n font: inherit\n line-height: 1em\n padding: 9px 16px\n &:nth-child(n + 2)\n margin-left: 4px\n\na.button\n align-items: center\n display: inline-flex\n justify-content: center\n text-align: center\n text-decoration: none\n\n // Hack for merch icon spacing\n > :nth-child(2)\n margin-left: 5px\n\nbutton, a.button\n background-color: white\n border 1px solid primary-color\n border-radius: 4px\n box-shadow: 1px 2px 0px primary-color\n color: #673ab7\n cursor: pointer\n font-size: inherit\n line-height: 1em\n padding: 9px 8px\n white-space: nowrap\n\n &.full\n width: 100%\n\n &:focus\n background-color: active-color\n color: rgba(0, 0, 0, 0.69)\n outline-offset: 4px\n\n &:active, &.active\n background-color: primary-color\n border 1px solid primary-color\n color: white\n box-shadow: 1px 2px 0px #241440 inset\n\n &:disabled\n background-color: #eee\n border-color: #4e4e4e\n box-shadow: 1px 2px 0px #4e4e4e\n color: #4e4e4e\n cursor: default\n\ntools, patterns\n background-color: white\n border-bottom: 2px solid primary-color\n display: flex\n width: 100%\n\n > *\n background-repeat: no-repeat\n background-position: 50% 50%\n border-right: 1px solid primary-color\n cursor: pointer\n display: block\n width: 49px\n height: 48px\n &:hover\n background-color: highlight-color\n &.active\n background-color: active-color\n\n > tool.eraser\n order: 2\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAAIdJREFUeJzNUsERwCAIw15n031wDt0Hl0s/9VoF9NnmZzRBCERfI2zusdOtDABmopRGVoRCrdviADNMiADM6L873Mql2NYiw3E2WItzVi2dSuw8JBHNvQyegcU4vmjNFesWZrHFTSlYQ/RhRDgatKZFnXPy7zMIoVaYa3fH5i3PTHira4r/gQv1W1E4p9FksQAAAABJRU5ErkJggg==\")\n\n > .selection\n order: 2\n background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAiUlEQVRYR+1XQQrAIAyr/3/0RLAyqoUdzNJDvFnEpkkM2MzssX21Qw1SGo0igHfNgfiZ6/sI4HqDSVs6wGlaCNXZpQ7gN80jkIxyNAtLkjIA0BOn99O0d0RlJIjvFC3JZkJ0Q3kgMqAcWIwoB8oEkXKAHsV0APIAXQI6AHkA9vf78jUbZ+gAaB7opmM7IWnG3nUAAAAASUVORK5CYII=\")\n\ntools\n > *\n pixelated()\n\npatterns\n margin-top: -1px\n\n &:empty\n display: none\n\n > pattern\n display: flex\n padding: 0 8px\n align-items: center\n width: 64px\n overflow: hidden\n\n > preview\n position: relative\n transform: scale(0.0625, 0.0625)\n top: -14px\n\n > *\n transform: scale(4)\n\npre.position\n background-color: rgba(255, 255, 255, 0.9375)\n border: 1px solid black\n box-shadow: 1px 1px 0 0 rgba(0,0,0,0.5)\n padding: 2px 6px 2px 4px\n font-family: inherit\n pointer-events: none\n padding: 4px\n position: absolute\n left: 1rem\n top: calc(45px + 1rem)\n\n &:empty\n display: none\n\npre.debug\n &:empty\n display: none\n\n@media only screen and (max-width: 768px)\n aside.actions\n border-top: none\n padding-top: 0\n > section\n &:first-child\n display: none\n &:nth-child(2)\n margin-top: 0\n\n button\n > span.description\n display: none\n\n tools, patterns\n order: 2\n border: none\n width: 49px\n height: 48px\n\n > *\n border-top: 1px solid primary-color\n display: none\n\n tool.active\n display: block\n\n pattern.active\n display: flex\n\n &.open\n background-color: primary-color\n display: grid\n grid-gap: 1px\n position: absolute\n width: 100%\n height: 100%\n grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr\n z-index: 1\n\n > *\n background-color: white\n background-size: 100%\n border: none\n display: block\n image-rendering: pixelated\n height: 100%\n width: 100%\n\n &:hover\n background-color: #FFE0B2\n"
@@ -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\")();\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"
+ "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 = \"assets/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"
@@ -727,7 +727,7 @@
"content": "var ExportTemplate, Modal, OptionTemplate, Pattern, Progress, Sample, Song, StaffView, Undo, animate, generateExportTitle, purchase, rand, _ref;\n\nrequire(\"./lib/extensions\");\n\n_ref = system.ui, Progress = _ref.Progress, Modal = _ref.Modal;\n\nExportTemplate = require(\"./templates/export\");\n\nOptionTemplate = require(\"./templates/option\");\n\nSample = require(\"./sample\");\n\nSong = require(\"./song-v2\");\n\nStaffView = require(\"./staff-view\");\n\nPattern = require(\"./lib/pattern\");\n\nUndo = require(\"./lib/undo\");\n\npurchase = require(\"./lib/stripe-checkout\").purchase;\n\nmodule.exports = function(I, self) {\n var FXPicker, MeterPicker, Template, aboutTemplate, buyNowTemplate, helpTemplate, persistenceElement, song;\n if (I == null) {\n I = {};\n }\n if (self == null) {\n self = Model(I);\n }\n defaults(I, {\n patterns: []\n });\n self.include(require(\"./lib/hotkeys\"));\n self.include(require(\"./lib/midi-input\"));\n self.attrObservable(\"activeSection\", \"purchased\");\n self.attrModels(\"patterns\", Pattern);\n self.attrModel(\"song\", Song);\n song = self.song();\n Sample.loadPack().then(song.loadSettings).then(function() {\n return self.applySampleCSS(Sample.image);\n });\n self.extend({\n notePosition: Observable(\"\"),\n samples: song.settings,\n addNote: function(note) {\n self.unsaved(true);\n self.song().addNote(note);\n return self.pushState(self.getState());\n },\n removeNote: function(note, nearby) {\n var removed;\n self.unsaved(true);\n removed = self.song().removeNote(note, nearby);\n self.pushState(self.getState());\n return removed;\n },\n addPattern: function(beatNote, pattern) {\n self.unsaved(true);\n self.song().addPattern(beatNote, pattern);\n self.pushState(self.getState());\n return self.triggerRerender();\n },\n deleteRange: function(range) {\n self.unsaved(true);\n self.song().deleteRange(range);\n self.pushState(self.getState());\n return self.triggerRerender();\n },\n copyRange: function(range) {\n var pattern;\n pattern = self.song().copyRange(range);\n if (pattern.notes().length) {\n self.activeToolIndex(3);\n return self.activePatternIndex(self.patterns.push(pattern) - 1);\n }\n },\n moveNotes: function(notes, beatDelta, staffDelta) {\n self.unsaved(true);\n notes.forEach(function(note) {\n note[0] += beatDelta;\n return note[1] += staffDelta;\n });\n if (beatDelta !== 0) {\n self.song().resection(notes);\n }\n self.pushState(self.getState());\n return self.triggerRerender(true);\n },\n exportSprites: function() {\n return Sample.exportPNG(self.samples());\n },\n applySampleCSS: function(image) {\n var n, style, _i, _results;\n style = document.querySelector(\"style.samples\");\n if (style == null) {\n style = document.createElement(\"style\");\n }\n style.classList.add(\"samples\");\n n = image.width / 48;\n style.innerHTML = (function() {\n _results = [];\n for (var _i = 0; 0 <= n ? _i < n : _i > n; 0 <= n ? _i++ : _i--){ _results.push(_i); }\n return _results;\n }).apply(this).map(function(i) {\n var x;\n x = -i * 48;\n return \"note.i\" + i + \":after {background: url(\" + image.src + \") \" + x + \"px 0;}\\nnote.i\" + i + \".active:after {background: url(\" + image.src + \") \" + x + \"px 48px;}\\ntools > .i\" + i + \" {background: url(\" + image.src + \") \" + x + \"px 0px;}\\ntools > .i\" + i + \".active {background: #FFC107 url(\" + image.src + \") \" + x + \"px 48px;}\";\n }).join(\"\\n\");\n return document.head.appendChild(style);\n },\n loop: song.loop,\n toggleLoop: function() {\n return self.loop.toggle();\n },\n loopButtonClass: function() {\n if (self.loop()) {\n return \"active\";\n }\n },\n playButtonClass: function() {\n if (self.playing()) {\n return \"active\";\n }\n },\n length: song.length,\n sections: song.sections,\n sectionAt: song.sectionAt,\n tempo: function() {\n return self.activeSection().tempo.apply(null, arguments);\n },\n presetName: function() {\n return self.activeSection().presetName.apply(null, arguments);\n },\n keySignature: function() {\n return self.activeSection().keySignature.apply(null, arguments);\n },\n timeSignature: function() {\n return self.activeSection().timeSignature.apply(null, arguments);\n },\n clearDisabled: function() {\n return !self.activeSection().notes().length;\n },\n clear: function() {\n return Modal.confirm(\"Clear entire song?\").then(function(confirmed) {\n if (confirmed) {\n song.clear();\n self.unsaved(true);\n self.pushState(self.getState());\n return self.triggerRerender();\n }\n });\n },\n about: function() {\n return Modal.show(aboutTemplate, {\n cancellable: true\n });\n },\n showHelp: function() {\n return Modal.show(helpTemplate, {\n cancellable: true\n });\n },\n showPersistenceModal: function() {\n return Modal.show(persistenceElement);\n },\n hiddenIfPurchased: function() {\n if (self.purchased()) {\n return \"hidden\";\n }\n },\n hiddenUnlessPurchased: function() {\n if (!self.purchased()) {\n return \"hidden\";\n }\n },\n purchase: function() {\n return Modal.show(buyNowTemplate, {\n cancellable: true\n });\n },\n oldVersion: function() {\n return window.location = \"https://danielx.net/composer/0.2.0-pre.0/\";\n },\n exportAudio: function() {\n var formatTypes, name, selectedType;\n name = Observable(\"song\");\n selectedType = Observable(\"mp3\");\n formatTypes = [\"mp3\", \"wav\"];\n return Modal.show(ExportTemplate({\n name: name,\n title: generateExportTitle(),\n selectedType: selectedType,\n formatOptionElements: function() {\n return formatTypes.map(function(type) {\n return OptionTemplate({\n text: type,\n value: type\n });\n });\n },\n cancel: function(e) {\n e.preventDefault();\n return Modal.hide();\n },\n submit: function(e) {\n e.preventDefault();\n return self.exportSong(self.song(), {\n name: name(),\n type: selectedType()\n });\n }\n }));\n },\n upcomingNotes: song.upcomingNotes,\n fullscreen: Observable(document.fullscreenElement),\n loadFromURL: function(url) {\n var progressView;\n progressView = Progress({\n value: 0,\n message: \"Loading...\"\n });\n Modal.show(progressView.element, {\n cancellable: false\n });\n return ajax.ajax({\n url: url,\n responseType: \"json\"\n }).progress(function(_arg) {\n var lengthComputable, loaded, total;\n lengthComputable = _arg.lengthComputable, loaded = _arg.loaded, total = _arg.total;\n if (lengthComputable) {\n return progressView.value(loaded / total);\n }\n }).then(self.fromJSON).then(function() {\n return Modal.hide();\n })[\"catch\"](function(e) {\n if (e.statusText) {\n return Modal.alert(\"An error has occurred: \" + e.status + \" - \" + e.statusText);\n } else {\n return Modal.alert(\"An error has occurred: \" + e.message);\n }\n });\n },\n hotkeysTableElement: function() {\n var div;\n div = document.createElement('div');\n div.innerHTML = \"\\n\\n\\n| Action | \\nKey | \\n
\\n\\n\\n| Select Instrument | \\n0-9 | \\n
\\n\\n| Select Instrument | \\n<backtick> 1-7 | \\n
\\n\\n| Select Pattern | \\nShift+0-9 | \\n
\\n\\n| Eraser Tool | \\ne | \\n
\\n\\n| Selection Tool | \\ns | \\n
\\n\\n| Undo | \\nCtrl+z, ⌘+z | \\n
\\n\\n| Redo | \\nCtrl+y, ⌘+y | \\n
\\n\\n| Play/Pause | \\nSpace | \\n
\\n\\n| Play from Beginning | \\nEnter | \\n
\\n\\n| | \\n | \\n
\\n\\n| Selection | \\n | \\n
\\n\\n| Copy | \\nc | \\n
\\n\\n| Cut | \\nx | \\n
\\n\\n| Delete | \\nDelete | \\n
\\n\\n| Reset | \\nEsc | \\n
\\n\\n| | \\n | \\n
\\n\\n| Data | \\n | \\n
\\n\\n| Save | \\nCtrl+s, ⌘+s | \\n
\\n\\n| Open | \\nCtrl+o, ⌘+o | \\n
\\n\\n| Export | \\nCtrl+r, ⌘+r | \\n
\\n\\n| | \\n | \\n
\\n\\n| Meta | \\n | \\n
\\n\\n| About | \\nF1 | \\n
\\n\\n| Toggle Full screen | \\nF11 | \\n
\\n\\n| Help | \\n? | \\n
\\n
\";\n return div.children[0];\n }\n });\n self.include(require(\"./player-audio\"));\n self.include(require(\"./persistence\"));\n self.include(require(\"./tools\"));\n self.include(StaffView);\n Undo(self);\n self.pushState(self.getState());\n self.activeSection(self.sections()[0]);\n self.noteControlElement = require(\"./views/note-control\")(self).element;\n MeterPicker = require(\"./views/meter-picker\");\n self.meterPickerElement = MeterPicker(self).element;\n FXPicker = require(\"./views/fx-picker\");\n self.fxPickerElement = FXPicker({\n presetName: self.presetName\n }).element;\n Observable(function() {\n var name;\n name = self.presetName();\n return self.fxStyle(FXPicker.styleFor(name));\n });\n require(\"./lib/meter-background\").then(function(timeSignatureMeterBackgroundUrls) {\n var style;\n style = document.createElement(\"style\");\n style.classList.add(\"meter\");\n style.innerHTML = Object.keys(timeSignatureMeterBackgroundUrls).map(function(n) {\n var url;\n url = timeSignatureMeterBackgroundUrls[n];\n return \"song-section.n\" + n + \" {background-image: url(\" + url + \");}\";\n }).join(\"\\n\");\n return document.head.appendChild(style);\n });\n require(\"./views/demo-picker\")(self);\n require(\"./views/settings\")(self);\n require(\"./views/arranger\")(self);\n Template = require(\"./templates/app\");\n self.element = Template({\n actionsElement: require(\"./views/actions\")(self).element,\n toolsElement: require(\"./views/tools\")(self).element,\n patternsElement: require(\"./views/patterns\")(self).element,\n staffElement: self.staffElement,\n notePosition: self.notePosition\n });\n aboutTemplate = require(\"./templates/about\")(self);\n helpTemplate = require(\"./templates/help\")(self);\n buyNowTemplate = require(\"./templates/buy-now\")({\n submit: function(e) {\n e.preventDefault();\n return purchase();\n }\n });\n persistenceElement = require(\"./templates/persistence\")(self);\n animate(function() {\n return self.performDraw();\n });\n Observable(function() {\n song.length();\n return self.rerenderNotes();\n });\n return self;\n};\n\nrand = function(a) {\n return a[Math.floor(Math.random() * a.length)];\n};\n\ngenerateExportTitle = function() {\n var adjective, noun;\n adjective = rand([\"cool\", \"rad\", \"kickin'\", \"bumpin'\", \"sweet\", \"tasty\"]);\n noun = rand([\"banger\", \"track\", \"song\", \"tune\", \"jam\"]);\n return \"Export your \" + adjective + \" \" + noun;\n};\n\nanimate = function(fn) {\n var step;\n step = function() {\n requestAnimationFrame(step);\n return fn();\n };\n return step();\n};\n"
},
"sample": {
- "content": "var BASE_PATH, Sample, blobLoader, bufferLoader, context, defaultPack, imgLoader, samples, urlFor;\n\ncontext = require(\"./lib/audio-context\");\n\nbufferLoader = function(url) {\n return new Promise(function(resolve, reject) {\n return fetch(url).then(function(response) {\n if (!response.ok) {\n throw response;\n }\n return response.arrayBuffer();\n }).then(function(buffer) {\n return context.decodeAudioData(buffer, resolve, reject);\n });\n });\n};\n\nblobLoader = function(url) {\n return fetch(url).then(function(response) {\n if (!response.ok) {\n throw response;\n }\n return response.blob();\n });\n};\n\nBASE_PATH = \"https://danielx.net/composer/\";\n\nurlFor = function(path) {\n return \"\" + BASE_PATH + path;\n};\n\nimgLoader = function(url) {\n return new Promise(function(resolve, reject) {\n var image;\n image = new Image;\n image.crossOrigin = true;\n image.src = url;\n image.onload = function() {\n return resolve(image);\n };\n return image.onerror = reject;\n });\n};\n\nmodule.exports = Sample = {\n load: function(data) {\n var sample;\n sample = data.sample;\n return bufferLoader(urlFor(sample)).then(function(buffer) {\n return Object.assign(data, {\n buffer: buffer\n });\n });\n },\n loadPack: function() {\n return imgLoader(urlFor(\"images/default.png\")).then(function(img) {\n Sample.image = img;\n return Promise.all(defaultPack.map(function(sample, i) {\n var canvas, ctx;\n canvas = document.createElement('canvas');\n canvas.width = 48;\n canvas.height = 48;\n ctx = canvas.getContext('2d');\n ctx.drawImage(img, i * 48, 0, 48, 48, 0, 0, 48, 48);\n return new Promise(function(resolve, reject) {\n return canvas.toBlob(function(blob) {\n var url;\n url = URL.createObjectURL(blob);\n sample.cursor = \"url(\" + url + \") \" + 24 + \" \" + 24 + \", default\";\n sample.url = url;\n return resolve();\n });\n });\n }));\n }).then(function() {\n return Promise.all(defaultPack.map(Sample.load));\n });\n },\n exportPNG: function(samples) {\n var canvas, ctx;\n canvas = document.createElement('canvas');\n canvas.width = 48 * samples.length;\n canvas.height = 96;\n ctx = canvas.getContext('2d');\n samples.forEach(function(_arg, i) {\n var activeImage, image, x;\n image = _arg.image, activeImage = _arg.activeImage;\n x = 48 * i;\n ctx.drawImage(image, x, 0);\n return ctx.drawImage(activeImage, x, 48);\n });\n return canvas.toBlob(function(blob) {\n var url;\n url = URL.createObjectURL(blob);\n return window.open(url, \"_blank\");\n });\n }\n};\n\nbufferLoader(\"\" + BASE_PATH + \"erase2.wav\").then(function(buffer) {\n return Sample.fx = {\n eraser: buffer\n };\n});\n\nsamples = {\n synth: {\n sample: \"synth.wav\"\n },\n piano: {\n sample: \"piano.wav\",\n pan: -0.5\n },\n guitar: {\n sample: \"guitar.wav\",\n pan: 0.25\n },\n bass: {\n sample: \"16.wav\"\n },\n horn: {\n sample: \"horn.wav\",\n pan: -0.333\n },\n orch_hit: {\n sample: \"orch_hit.wav\",\n pan: 0.25\n },\n chime: {\n sample: \"5.wav\",\n pan: 0.5\n },\n organ: {\n sample: \"organ.wav\",\n pan: -0.25\n },\n drum: {\n sample: \"drum.wav\"\n },\n snare: {\n sample: \"snare.wav\"\n },\n woodblock: {\n sample: \"14.wav\",\n pan: -0.333\n },\n clap: {\n sample: \"clap.wav\"\n },\n hat: {\n sample: \"hat.wav\",\n pan: 0.333\n },\n baby: {\n sample: \"baby.wav\",\n pan: -0.5\n },\n yoshi: {\n sample: \"yoshi.wav\",\n pan: 0.5\n },\n pig: {\n sample: \"oink.wav\"\n },\n cat: {\n sample: \"cat.wav\",\n pan: 0.25\n },\n dog: {\n sample: \"dog.wav\",\n pan: -0.25\n },\n french: {\n sample: \"french-horn.wav\",\n pan: 0.333,\n pitchShift: 0\n },\n nylon: {\n sample: \"nylon-guitar2.wav\",\n pan: -0.25,\n pitchShift: -12\n },\n snare2: {\n sample: \"snare2.wav\",\n pitchShift: 0\n }\n};\n\ndefaultPack = Object.keys(samples).map(function(name, i) {\n var sample;\n sample = samples[name];\n sample.name = name;\n sample.index = i;\n if (sample.pitchShift == null) {\n sample.pitchShift = -5;\n }\n if (sample.pan == null) {\n sample.pan = 0;\n }\n if (sample.volume == null) {\n sample.volume = 1;\n }\n return sample;\n});\n"
+ "content": "var BASE_PATH, Sample, blobLoader, bufferLoader, context, defaultPack, imgLoader, samples, urlFor;\n\ncontext = require(\"./lib/audio-context\");\n\nbufferLoader = function(url) {\n return new Promise(function(resolve, reject) {\n return fetch(url).then(function(response) {\n if (!response.ok) {\n throw response;\n }\n return response.arrayBuffer();\n }).then(function(buffer) {\n return context.decodeAudioData(buffer, resolve, reject);\n });\n });\n};\n\nblobLoader = function(url) {\n return fetch(url).then(function(response) {\n if (!response.ok) {\n throw response;\n }\n return response.blob();\n });\n};\n\nBASE_PATH = window.location;\n\nurlFor = function(path) {\n return \"\" + BASE_PATH + \"assets/\" + path;\n};\n\nimgLoader = function(url) {\n return new Promise(function(resolve, reject) {\n var image;\n image = new Image;\n image.crossOrigin = true;\n image.src = url;\n image.onload = function() {\n return resolve(image);\n };\n return image.onerror = reject;\n });\n};\n\nmodule.exports = Sample = {\n load: function(data) {\n var sample;\n sample = data.sample;\n return bufferLoader(urlFor(sample)).then(function(buffer) {\n return Object.assign(data, {\n buffer: buffer\n });\n });\n },\n loadPack: function() {\n return imgLoader(urlFor(\"images/default.png\")).then(function(img) {\n Sample.image = img;\n return Promise.all(defaultPack.map(function(sample, i) {\n var canvas, ctx;\n canvas = document.createElement('canvas');\n canvas.width = 48;\n canvas.height = 48;\n ctx = canvas.getContext('2d');\n ctx.drawImage(img, i * 48, 0, 48, 48, 0, 0, 48, 48);\n return new Promise(function(resolve, reject) {\n return canvas.toBlob(function(blob) {\n var url;\n url = URL.createObjectURL(blob);\n sample.cursor = \"url(\" + url + \") \" + 24 + \" \" + 24 + \", default\";\n sample.url = url;\n return resolve();\n });\n });\n }));\n }).then(function() {\n return Promise.all(defaultPack.map(Sample.load));\n });\n },\n exportPNG: function(samples) {\n var canvas, ctx;\n canvas = document.createElement('canvas');\n canvas.width = 48 * samples.length;\n canvas.height = 96;\n ctx = canvas.getContext('2d');\n samples.forEach(function(_arg, i) {\n var activeImage, image, x;\n image = _arg.image, activeImage = _arg.activeImage;\n x = 48 * i;\n ctx.drawImage(image, x, 0);\n return ctx.drawImage(activeImage, x, 48);\n });\n return canvas.toBlob(function(blob) {\n var url;\n url = URL.createObjectURL(blob);\n return window.open(url, \"_blank\");\n });\n }\n};\n\nbufferLoader(\"\" + BASE_PATH + \"erase2.wav\").then(function(buffer) {\n return Sample.fx = {\n eraser: buffer\n };\n});\n\nsamples = {\n synth: {\n sample: \"synth.wav\"\n },\n piano: {\n sample: \"piano.wav\",\n pan: -0.5\n },\n guitar: {\n sample: \"guitar.wav\",\n pan: 0.25\n },\n bass: {\n sample: \"16.wav\"\n },\n horn: {\n sample: \"horn.wav\",\n pan: -0.333\n },\n orch_hit: {\n sample: \"orch_hit.wav\",\n pan: 0.25\n },\n chime: {\n sample: \"5.wav\",\n pan: 0.5\n },\n organ: {\n sample: \"organ.wav\",\n pan: -0.25\n },\n drum: {\n sample: \"drum.wav\"\n },\n snare: {\n sample: \"snare.wav\"\n },\n woodblock: {\n sample: \"14.wav\",\n pan: -0.333\n },\n clap: {\n sample: \"clap.wav\"\n },\n hat: {\n sample: \"hat.wav\",\n pan: 0.333\n },\n baby: {\n sample: \"baby.wav\",\n pan: -0.5\n },\n yoshi: {\n sample: \"yoshi.wav\",\n pan: 0.5\n },\n pig: {\n sample: \"oink.wav\"\n },\n cat: {\n sample: \"cat.wav\",\n pan: 0.25\n },\n dog: {\n sample: \"dog.wav\",\n pan: -0.25\n },\n french: {\n sample: \"french-horn.wav\",\n pan: 0.333,\n pitchShift: 0\n },\n nylon: {\n sample: \"nylon-guitar2.wav\",\n pan: -0.25,\n pitchShift: -12\n },\n snare2: {\n sample: \"snare2.wav\",\n pitchShift: 0\n }\n};\n\ndefaultPack = Object.keys(samples).map(function(name, i) {\n var sample;\n sample = samples[name];\n sample.name = name;\n sample.index = i;\n if (sample.pitchShift == null) {\n sample.pitchShift = -5;\n }\n if (sample.pan == null) {\n sample.pan = 0;\n }\n if (sample.volume == null) {\n sample.volume = 1;\n }\n return sample;\n});\n"
},
"song-v2": {
"content": "\n/*\nSong V2\n=======\n\nA song has a list of sections.\n\nEach section has:\n\nname: string\nlength: beats\nnotes: array\nsettings: [{\n pan: float [-1, 1]\n pitchShift: float (semitones)\n volume: float\n}, ...]\n */\nvar OldSong, Pattern, Section, Setting, noteCompare, oldInstrumentMapping, pitchToStaffNote, quantize, staffNoteToPitch, _ref,\n __slice = [].slice;\n\n_ref = require(\"./lib/util\"), pitchToStaffNote = _ref.pitchToStaffNote, staffNoteToPitch = _ref.staffNoteToPitch, noteCompare = _ref.noteCompare, quantize = _ref.quantize;\n\nSetting = function(I, self) {\n if (I == null) {\n I = {};\n }\n if (self == null) {\n self = Model(I);\n }\n self.attrObservable(\"url\", \"name\");\n [\"pan\", \"pitchShift\", \"volume\"].forEach(function(name) {\n self[name] = Observable(I[name]);\n return self[name].observe(function(newValue) {\n return I[name] = Number(newValue);\n });\n });\n self.toJSON = function() {\n return {\n pan: I.pan,\n pitchShift: I.pitchShift,\n volume: I.volume\n };\n };\n return self;\n};\n\nPattern = require(\"./lib/pattern\");\n\nSection = require(\"./lib/section\");\n\nOldSong = require(\"/lib/legacy/song\");\n\noldInstrumentMapping = {\n 0: 0,\n 1: 1,\n 2: 2,\n 3: 4,\n 4: 5,\n 5: 8,\n 6: 9,\n 7: 11,\n 8: 16,\n 9: 17\n};\n\nmodule.exports = function(I, self) {\n var sectionFor;\n if (I == null) {\n I = {};\n }\n if (self == null) {\n self = Model(I);\n }\n defaults(I, {\n title: \"Untitled\",\n description: \"\",\n sections: [\n {\n name: \"Section 1\",\n length: 64,\n tempo: 120,\n notes: []\n }\n ],\n settings: [],\n loop: true,\n version: 2\n });\n self.attrObservable(\"loop\");\n self.attrModels(\"sections\", Section);\n self.attrModels(\"settings\", Setting);\n sectionFor = null;\n self.extend({\n clear: function() {\n var section;\n section = self.sections()[0];\n section.notes([]);\n return self.sections([section]);\n },\n loadSettings: function(samples) {\n return self.settings(samples.map(function(sample) {\n return Setting(sample);\n }));\n },\n addPattern: function(_arg, pattern) {\n var note, time;\n time = _arg[0], note = _arg[1];\n return pattern.notes().forEach(function(_arg1) {\n var n, rest, t;\n t = _arg1[0], n = _arg1[1], rest = 3 <= _arg1.length ? __slice.call(_arg1, 2) : [];\n return self.addNote([time + t, note + n].concat(__slice.call(rest)));\n });\n },\n copyRange: function(range) {\n var dt, first, n0, notes, t, t0;\n t = range[0][0];\n dt = range[0][1] - t;\n notes = self.notesWithinRange(range);\n first = notes[0];\n if (first) {\n t0 = first[0], n0 = first[1];\n notes = notes.map(function(_arg) {\n var n, rest, t;\n t = _arg[0], n = _arg[1], rest = 3 <= _arg.length ? __slice.call(_arg, 2) : [];\n return [t - t0, n - n0].concat(__slice.call(rest));\n });\n }\n return Pattern({\n beats: dt,\n notes: notes\n });\n },\n deleteRange: function(range) {\n var time;\n time = 0;\n return self.sections.forEach(function(section) {\n var l;\n l = section.length();\n section.deleteRange([[range[0][0] - time, range[0][1] - time], range[1]]);\n return time += l;\n });\n },\n\n /*\n Pass all the notes that are upcoming to a handler fn\n The handler fn receives:\n \n note - a stable reference to a note that can be used as a key to an element\n section - a reference to the section object the note appears in\n s - seconds after beat t, accounts for different section bpm\n \n i.e. notes that occur within the interval [t, t+dt)\n \n t and dt are in beats\n \n doesn't return notes outside of [0, length)\n \n current is expected to be <= t\n \n if current is < 0 then it is considered to wrap from the end\n \n current is an optional reference time to receive the accumulated seconds\n relative to. This is used in the audio playback.\n */\n upcomingNotes: function(t, dt, fn, current) {\n var accumulatedBeats, accumulatedTime, songDuration, songLength;\n if (current == null) {\n current = t;\n }\n accumulatedBeats = 0;\n accumulatedTime = 0;\n songLength = self.length();\n songDuration = self.duration();\n if (songLength <= 0 || isNaN(songLength)) {\n return;\n }\n if (t >= songLength) {\n return;\n }\n while (current < 0) {\n accumulatedTime += songDuration;\n current += songLength;\n }\n if (current > 0) {\n self.sections().forEach(function(section) {\n var sectionDuration, sectionLength;\n sectionLength = section.length();\n sectionDuration = section.duration();\n if (sectionLength < current) {\n current -= sectionLength;\n return accumulatedTime -= sectionDuration;\n } else if (current > 0) {\n accumulatedTime -= (current / sectionLength) * sectionDuration;\n return current = 0;\n }\n });\n }\n return self.sections().forEach(function(section) {\n var sectionDuration, sectionLength;\n sectionLength = section.length();\n sectionDuration = section.duration();\n section.upcomingNotes(t - accumulatedBeats, dt, function(note) {\n var beat, s;\n beat = note[0];\n s = (beat / sectionLength) * sectionDuration + accumulatedTime;\n return fn(note, section, s);\n });\n accumulatedBeats += sectionLength;\n return accumulatedTime += sectionDuration;\n });\n },\n notesWithin: function(t, dt) {\n var notes;\n notes = [];\n self.upcomingNotes(t, dt, function(note) {\n return notes.push(note);\n });\n return notes;\n },\n notesWithinRange: function(range) {\n var dt, n0, n1, notes, t, _ref1;\n t = range[0][0];\n dt = range[0][1] - t;\n _ref1 = range[1], n0 = _ref1[0], n1 = _ref1[1];\n return notes = self.notesWithin(t, dt + 0.0001).filter(function(_arg) {\n var n, _;\n _ = _arg[0], n = _arg[1];\n return (n0 <= n && n <= n1);\n }).sort(noteCompare);\n },\n sectionAt: function(t) {\n var i, section, sections, time;\n time = 0;\n i = 0;\n sections = self.sections();\n while (section = sections[i]) {\n time += section.length();\n if (t < time) {\n return [section, time - t];\n }\n i++;\n }\n return [];\n },\n addNote: function(note) {\n var t, time;\n time = 0;\n t = note[0];\n return self.sections.forEach(function(section) {\n var l, _ref1;\n l = section.length();\n if ((0 <= (_ref1 = t - time) && _ref1 < l)) {\n note[0] -= time;\n section.addNote(note);\n sectionFor.set(note, section);\n }\n return time += l;\n });\n },\n removeNote: function(note, nearby) {\n var removed, t, time;\n time = 0;\n t = note[0];\n removed = false;\n self.sections.forEach(function(section) {\n var l, _ref1;\n l = section.length();\n if ((0 <= (_ref1 = t - time) && _ref1 < l)) {\n note[0] -= time;\n removed = section.removeNote(note, nearby);\n }\n return time += l;\n });\n return removed;\n },\n sectionBeginsAt: function(s) {\n var ret, t;\n t = 0;\n ret = void 0;\n self.sections.forEach(function(section) {\n if (section === s) {\n ret = t;\n }\n return t += s.length();\n });\n return ret;\n },\n duration: function() {\n return self.sections.reduce(function(total, section) {\n return total + section.duration();\n }, 0);\n },\n exportDuration: function() {\n var last;\n last = self.sections().length - 1;\n return self.sections.reduce(function(total, section, i) {\n if (i === last) {\n return total + section.nonEmptyDuration();\n } else {\n return total + section.duration();\n }\n }, 0);\n },\n length: function() {\n return self.sections().reduce(function(count, section) {\n return count + section.length();\n }, 0);\n },\n reset: function() {\n sectionFor = new Map;\n return self.upcomingNotes(0, self.length(), function(note, section) {\n return sectionFor.set(note, section);\n });\n },\n resection: function(notes) {\n return notes.forEach(function(note) {\n var section, sectionStart, t;\n section = sectionFor.get(note);\n t = note[0];\n if ((t < 0) || (t >= section.length())) {\n sectionStart = self.sectionBeginsAt(section);\n if (sectionStart === 0 && t < 0) {\n return;\n }\n if (section.removeNoteByReference(note)) {\n note[0] += sectionStart;\n return self.addNote(note);\n }\n }\n });\n },\n toJSON: function() {\n return Object.assign({}, I, {\n settings: self.settings.map(function(s) {\n return s.toJSON();\n })\n });\n },\n fromJSON: function(data) {\n var lastTime, newNotes, notes, section, song;\n if (data.patterns || data.notes) {\n song = OldSong().fromJSON(data);\n notes = song.upcomingNotes(0, song.size());\n newNotes = notes.map(function(_arg) {\n var beat, instrument, note;\n beat = _arg[0], note = _arg[1], instrument = _arg[2];\n return [beat].concat(__slice.call(pitchToStaffNote(note)), [oldInstrumentMapping[instrument]]);\n });\n section = self.sections()[0];\n section.notes(newNotes);\n lastTime = newNotes.reduce(function(max, _arg) {\n var t;\n t = _arg[0];\n return Math.max(t, max);\n }, 0);\n section.length(quantize(lastTime + 1.99999, 4));\n section.tempo(song.tempo());\n } else {\n self.sections(data.sections.map(function(s) {\n return Section(s);\n }));\n if (data.settings) {\n debugger;\n data.settings.forEach(function(s, i) {\n var setting;\n setting = self.settings()[i];\n return Object.keys(s).forEach(function(key) {\n return setting[key](s[key]);\n });\n });\n }\n if (data.version === void 0) {\n self.sections.forEach(function(section) {\n notes = section.I.notes;\n return notes.forEach(function(n, i) {\n var accidental, beat, instrument, pitch, staffNote, _ref1, _ref2;\n _ref1 = notes[i], beat = _ref1[0], staffNote = _ref1[1], accidental = _ref1[2], instrument = _ref1[3];\n pitch = staffNoteToPitch(staffNote, accidental) + 5;\n _ref2 = pitchToStaffNote(pitch), staffNote = _ref2[0], accidental = _ref2[1];\n notes[i][1] = staffNote;\n return notes[i][2] = accidental;\n });\n });\n }\n }\n self.reset();\n return self;\n }\n });\n self.reset();\n return self;\n};\n"
@@ -784,7 +784,7 @@
"content": "module.exports = system.ui.Jadelet.exec([\"tool\",{\"class\":[{\"bind\":\"index\"},{\"bind\":\"active\"}],\"click\":{\"bind\":\"click\"}},[]]);"
},
"templates/staff": {
- "content": "module.exports = system.ui.Jadelet.exec([\"viewport\",{\"contextmenu\":{\"bind\":\"contextmenu\"},\"dragstart\":{\"bind\":\"dragstart\"},\"scroll\":{\"bind\":\"scroll\"},\"mousedown\":{\"bind\":\"mousedown\"},\"mousemove\":{\"bind\":\"mousemove\"},\"mouseup\":{\"bind\":\"mouseup\"},\"style\":[{\"bind\":\"fxStyle\"}]},[[\"staff\",{},[[\"lines\",{},[[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]]]],[\"lines\",{},[[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]]]],[\"z-meter\",{},[[\"div\",{\"class\":[\"repeat\",{\"bind\":\"repeatClass\"}]},[]],[\"div\",{\"class\":[\"repeat\",\"bass\",{\"bind\":\"repeatClass\"}]},[]]]],[\"notes\",{},[]],[\"selection\",{\"class\":[{\"bind\":\"selectionClass\"}]},[[\"button\",{\"class\":[\"up\"],\"click\":{\"bind\":\"selectionMoveUp\"}},[\"🡅\"]],[\"button\",{\"class\":[\"down\"],\"click\":{\"bind\":\"selectionMoveDown\"}},[\"🡇\"]],[\"button\",{\"class\":[\"left\"],\"click\":{\"bind\":\"selectionMoveLeft\"}},[\"🡄\"]],[\"button\",{\"class\":[\"right\"],\"click\":{\"bind\":\"selectionMoveRight\"}},[\"🡆\"]],[\"actions\",{},[[\"button\",{\"click\":{\"bind\":\"selectionCopy\"}},[\"Copy\"]],[\"button\",{\"click\":{\"bind\":\"selectionCut\"}},[\"Cut\"]],[\"button\",{\"click\":{\"bind\":\"selectionDelete\"}},[\"Delete\"]],[\"button\",{\"click\":{\"bind\":\"selectionReset\"}},[\"Cancel\"]]]]]],[\"pattern-preview\",{},[]],[\"playhead\",{},[]],[\"playhead\",{\"class\":[\"buffer-start\"]},[]],[\"playhead\",{\"class\":[\"buffer-end\"]},[]],[\"img\",{\"src\":\"https://danielx.net/composer/treble-clef.svg\"},[]],[\"img\",{\"src\":\"https://danielx.net/composer/bass-clef.svg\"},[]]]]]]);"
+ "content": "module.exports = system.ui.Jadelet.exec([\"viewport\",{\"contextmenu\":{\"bind\":\"contextmenu\"},\"dragstart\":{\"bind\":\"dragstart\"},\"scroll\":{\"bind\":\"scroll\"},\"mousedown\":{\"bind\":\"mousedown\"},\"mousemove\":{\"bind\":\"mousemove\"},\"mouseup\":{\"bind\":\"mouseup\"},\"style\":[{\"bind\":\"fxStyle\"}]},[[\"staff\",{},[[\"lines\",{},[[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]]]],[\"lines\",{},[[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]],[\"line\",{},[]]]],[\"z-meter\",{},[[\"div\",{\"class\":[\"repeat\",{\"bind\":\"repeatClass\"}]},[]],[\"div\",{\"class\":[\"repeat\",\"bass\",{\"bind\":\"repeatClass\"}]},[]]]],[\"notes\",{},[]],[\"selection\",{\"class\":[{\"bind\":\"selectionClass\"}]},[[\"button\",{\"class\":[\"up\"],\"click\":{\"bind\":\"selectionMoveUp\"}},[\"🡅\"]],[\"button\",{\"class\":[\"down\"],\"click\":{\"bind\":\"selectionMoveDown\"}},[\"🡇\"]],[\"button\",{\"class\":[\"left\"],\"click\":{\"bind\":\"selectionMoveLeft\"}},[\"🡄\"]],[\"button\",{\"class\":[\"right\"],\"click\":{\"bind\":\"selectionMoveRight\"}},[\"🡆\"]],[\"actions\",{},[[\"button\",{\"click\":{\"bind\":\"selectionCopy\"}},[\"Copy\"]],[\"button\",{\"click\":{\"bind\":\"selectionCut\"}},[\"Cut\"]],[\"button\",{\"click\":{\"bind\":\"selectionDelete\"}},[\"Delete\"]],[\"button\",{\"click\":{\"bind\":\"selectionReset\"}},[\"Cancel\"]]]]]],[\"pattern-preview\",{},[]],[\"playhead\",{},[]],[\"playhead\",{\"class\":[\"buffer-start\"]},[]],[\"playhead\",{\"class\":[\"buffer-end\"]},[]],[\"img\",{\"src\":\"assets/treble-clef.svg\"},[]],[\"img\",{\"src\":\"assets/bass-clef.svg\"},[]]]]]]);"
},
"templates/stripe/form": {
"content": "module.exports = system.ui.Jadelet.exec([\"form\",{\"action\":\"/charge\",\"method\":\"post\",\"submit\":{\"bind\":\"submit\"}},[[\"input\",{\"type\":\"hidden\",\"name\":\"stripeToken\",\"value\":{\"bind\":\"tokenId\"}},[]]]]);"
@@ -826,7 +826,7 @@
"content": "var destination;\n\ndestination = (new AudioContext).destination;\n\nmodule.exports = {\n destination: destination\n};\n"
},
"views/fx-picker": {
- "content": "\n/*\nStereo analyser node view\n\nStatus: WIP\n */\nvar FX, FXPicker, ItemTemplate, Jadelet, Template, chorus, delay, styleFor, _ref;\n\n_ref = FX = require(\"../lib/fx\"), chorus = _ref.chorus, delay = _ref.delay;\n\nTemplate = require(\"../templates/fx-picker\");\n\nJadelet = system.ui.Jadelet;\n\nItemTemplate = Jadelet.exec(\"button(@click @style @title class=@active)\");\n\nstyleFor = function(name) {\n if (name === \"purple\") {\n return {\n backgroundColor: \"#ede7f6\"\n };\n } else {\n return {\n backgroundImage: \"url(https://danielx.net/composer/fx/\" + name + \".jpg)\"\n };\n }\n};\n\nmodule.exports = FXPicker = function(_arg) {\n var presetName, self;\n presetName = _arg.presetName;\n self = {\n choices: function() {\n return FX.choices;\n },\n items: function() {\n return this.choices().map(function(name) {\n return ItemTemplate({\n title: name,\n click: function() {\n return presetName(name);\n },\n style: function() {\n return styleFor(name);\n },\n active: function() {\n if (presetName() === name) {\n return \"active\";\n }\n }\n });\n });\n },\n element: null\n };\n self.element = Template(self);\n return self;\n};\n\nFXPicker.styleFor = styleFor;\n"
+ "content": "\n/*\nStereo analyser node view\n\nStatus: WIP\n */\nvar FX, FXPicker, ItemTemplate, Jadelet, Template, chorus, delay, styleFor, _ref;\n\n_ref = FX = require(\"../lib/fx\"), chorus = _ref.chorus, delay = _ref.delay;\n\nTemplate = require(\"../templates/fx-picker\");\n\nJadelet = system.ui.Jadelet;\n\nItemTemplate = Jadelet.exec(\"button(@click @style @title class=@active)\");\n\nstyleFor = function(name) {\n if (name === \"purple\") {\n return {\n backgroundColor: \"#ede7f6\"\n };\n } else {\n return {\n backgroundImage: \"url(/assets/fx/\" + name + \".jpg)\"\n };\n }\n};\n\nmodule.exports = FXPicker = function(_arg) {\n var presetName, self;\n presetName = _arg.presetName;\n self = {\n choices: function() {\n return FX.choices;\n },\n items: function() {\n return this.choices().map(function(name) {\n return ItemTemplate({\n title: name,\n click: function() {\n return presetName(name);\n },\n style: function() {\n return styleFor(name);\n },\n active: function() {\n if (presetName() === name) {\n return \"active\";\n }\n }\n });\n });\n },\n element: null\n };\n self.element = Template(self);\n return self;\n};\n\nFXPicker.styleFor = styleFor;\n"
},
"templates/fx-picker": {
"content": "module.exports = system.ui.Jadelet.exec([\"aside\",{\"class\":[\"fx-picker\"]},[[\"label\",{},[\"FX\"]],{\"bind\":\"items\"}]]);"