diff --git a/README.md b/README.md
index 0d8d957..803b6f7 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,10 @@
This is a dumped copy of [DanielX's Mario Paint composer](https://www.danielx.net/composer/). I wanted to make it available for personal use with minor modifications. This code is dumped from the electron app purchased from Steam.
+
+
+To do a test run of this, you can serve it with python:
+```sh
+python -m http.server 8080
+```
+
+And open your browser to http://localhost:8080.
+
diff --git a/index.html b/index.html
index fe826bb..5b1910f 100755
--- a/index.html
+++ b/index.html
@@ -49,7 +49,7 @@
"content": "###\nPlayer Audio\n============\n\nMain audio loop\n\nNeeds tempo, playable, start beat, end beat, looping mode to play.\n\nProvides playTime and playing methods.\n###\n\n{Progress, Modal, Style} = system.ui\n\n# Safari uses webkit prefix.\nOfflineAudioContext = window.OfflineAudioContext or window.webkitOfflineAudioContext\n\nliveContext = require(\"./lib/audio-context\")\nEncoder = require \"./lib/encoder/index\"\n{quantize, staffNoteToPitch} = require \"./lib/util\"\n{limiter, stereoAnalyser} = FX = require(\"./lib/fx\")\n\nFXNetwork = (destination) ->\n {context} = destination\n gain = context.createGain()\n gain.connect destination\n\n fx = FX.choices.reduce (o, name) ->\n o[name] = FX[name] gain\n return o\n , {}\n\n default: gain\n fx: fx\n dispose: ->\n {currentTime} = context\n gain.gain.exponentialRampToValueAtTime 0.001, 0.1 + currentTime\n gain.gain.setValueAtTime 0, 0.1 + currentTime\n\n setTimeout ->\n gain.disconnect()\n , 0.25\n\nmodule.exports = (I, self) ->\n playTime = 0 # beats\n secondsPerMinute = 60 # unitless\n lastContextTime = 0\n lastQueuedBeat = 0\n lookahead = 0.1 # seconds\n toBeat = null\n wrapping = false\n\n self.attrObservable \"playing\"\n\n # The destination for 'live audio' goes to the analyser node then the speakers.\n # We add a hard limiter on right before the analyser and output.\n self.analysedDestination = analysedDestination = stereoAnalyser liveContext.destination\n liveDestination = limiter analysedDestination\n\n # FX Networks are disposable so we can fade out when stopping / restarting\n activeDestination = FXNetwork(liveDestination)\n\n # Need more lookahead to play when window loses focus and request animation frame\n # gets sparser\n document.addEventListener \"visibilitychange\", (e) ->\n if document.hidden\n lookahead = 1.25\n else\n lookahead = 0.1\n bufferAudio()\n\n initPlay = ->\n {context} = liveDestination\n\n context.resume()\n lastContextTime = context.currentTime\n lastQueuedBeat = playTime\n wrapping = false\n\n # playTime in beats\n # elapsedSeconds in seconds\n computeElapsedBeats = (playTime, elapsedSeconds) ->\n if playTime >= self.length()\n playTime -= self.length()\n\n [section, beatsLeft] = self.sectionAt(playTime)\n _bps = section.tempo() / 60\n\n beats = elapsedSeconds * _bps\n\n if beats > beatsLeft\n beatsLeft + computeElapsedBeats playTime + beatsLeft, elapsedSeconds - beatsLeft / _bps\n else\n beats\n\n # Schedules upcoming sounds to play\n playUpcomingSounds = (currentBeat, fromBeat, toBeat) ->\n dt = toBeat - fromBeat\n return if dt <=0\n\n self.upcomingNotes fromBeat, dt, ([beat, staffNote, accidental, instrument], section, s) ->\n pitch = staffNoteToPitch(staffNote, accidental, section.keySignature())\n\n # Play in FX destination for section\n self.playNote instrument, pitch, s, activeDestination.fx[section.presetName()]\n , currentBeat\n\n # beats per second\n bps = ->\n self.tempo() / secondsPerMinute\n\n # Buffer audio to the output device\n # The challenge is that the playhead needs to accurately track where we are in\n # beats, according to the context currentTime, but we need to buffer ahead so\n # we can properly time all the upcoming notes.\n #\n # There is one more tricky part where we wrap around. Even when not wrapping\n # the bufferd region shouldn't grow but the playhead should still advance\n # until done.\n #\n # Multi-section makes this even more tricky!\n bufferAudio = ->\n if self.playing()\n length = self.length()\n return if length < 1 or isNaN(length)\n\n {context} = liveDestination\n {currentTime} = context\n\n dt = currentTime - lastContextTime # seconds\n return if dt is 0\n lastContextTime = currentTime\n\n elapsedBeats = computeElapsedBeats playTime, dt\n self.animateNoteElements(playTime, elapsedBeats)\n playTime += elapsedBeats\n\n toBeat = playTime + computeElapsedBeats playTime, lookahead\n\n # We're continuing to wrap around until playTime loops\n if wrapping\n toBeat -= length\n playUpcomingSounds(playTime - length, lastQueuedBeat, toBeat)\n\n else\n # Enqueue sounds within the upcoming window since last queued beat\n playUpcomingSounds(playTime, lastQueuedBeat, toBeat)\n\n if toBeat >= length and self.loop()\n wrapping = true\n toBeat -= length\n lastQueuedBeat = 0\n playUpcomingSounds(playTime - length, lastQueuedBeat, toBeat)\n\n # Sometimes the toBeat could be less (we reduce the lookahead when focusing\n # the window, so only adjust it forward so we don't double buffer notes.\n lastQueuedBeat = Math.max lastQueuedBeat, toBeat\n\n if playTime >= length\n # Once playtime catches up to the end we're no longer wrapping\n wrapping = false\n if self.loop()\n playTime -= length\n self.animateNoteElements(0, playTime)\n else\n playTime = 0\n self.playing false\n\n # Set active section based on playTime\n self.activeSection self.sectionAt(playTime)[0]\n\n return\n\n setInterval ->\n bufferAudio()\n , 1000 / 120\n\n animBuffer = ->\n requestAnimationFrame animBuffer\n bufferAudio()\n animBuffer()\n\n self.extend\n exportSong: (song, opts={}) ->\n {name, type} = opts\n\n name ?= \"song\"\n type ?= \"mp3\"\n\n extension = \".#{type}\"\n\n progressView = Progress\n message: \"Rendering Audio...\"\n\n Modal.show progressView.element,\n cancellable: false\n\n cleanup = ->\n Modal.hide()\n\n err = (fn) ->\n (e) ->\n fn()\n throw e\n\n # Length total of all sections in beats\n songLength = song.length()\n\n audioChannels = 2\n samplesPerSecond = 44100\n # Export duration trims empty measures on last section\n lengthInSeconds = song.exportDuration()\n offlineContext = new OfflineAudioContext(audioChannels, samplesPerSecond * lengthInSeconds, samplesPerSecond)\n offlineFxNetwork = FXNetwork limiter offlineContext.destination\n\n new Promise (resolve, reject) ->\n t = 0 # beats\n dt = 1 # beats\n\n work = ->\n song.upcomingNotes t, dt, ([beat, staffNote, accidental, instrument], section, s) ->\n presetName = section.presetName()\n keySig = section.keySignature()\n note = staffNoteToPitch(staffNote, accidental, keySig)\n\n self.playNote instrument, note, s, offlineFxNetwork.fx[presetName]\n , 0\n\n t += dt\n\n if t <= songLength\n setTimeout work, 0\n else\n p = offlineContext.startRendering()\n\n if p\n p.then(resolve, reject)\n else\n # Safari doesn't support the Promise version yet\n offlineContext.oncomplete = ({renderedBuffer}) ->\n resolve renderedBuffer\n # There is no `onerror` event\n\n return\n\n work()\n return\n\n .then (audioBuffer) ->\n if type is \"mp3\"\n progressView.message \"Encoding mp3...\"\n Encoder.audioBufferToMP3(audioBuffer)\n else\n progressView.message \"Encoding wav...\"\n Encoder.audioBufferToWav(audioBuffer)\n .then (blob) ->\n blob.download name + extension\n .then cleanup, err cleanup\n\n # TODO: Think about how to connect these note events into audio networks fx, etc.\n # Schedule a note to be played, use the buffer at the given index, pitch shift by\n # `note` semitones, and play at `time` seconds in the future.\n playNote: (instrumentId, note=0, time=0, dest) ->\n dest ?= activeDestination.fx[self.presetName()] or liveDestination\n {context} = dest\n sample = self.samples()[instrumentId].I\n\n if sample\n {buffer, pitchShift, pan, volume} = sample\n note += pitchShift\n\n # Add panner node, this also forces mono-samples to stereo\n # FUTURE: iOS doesn't support createStereoPanner yet\n panner = context.createPanner()\n panner.panningModel = 'equalpower'\n panner.setPosition(pan, 0, 1 - Math.abs(pan))\n\n gain = context.createGain()\n gain.gain.value = volume\n\n rate = Math.pow 2, note / 12\n source = self.playBuffer(buffer, rate, time, panner)\n\n panner.connect gain\n gain.connect dest\n\n source.onended = ->\n gain.disconnect()\n\n # Plays a buffer directly into the live audio context destination or the\n # given destination. Rate and time are given to shift pitch and start time.\n # time is in seconds relative to the currentTime of the destination's\n # context.\n #\n # This is used for the 'eraser' sound effect.\n # time is in seconds, must be >= 0\n playBuffer: (buffer, rate=1, time=0, dest=liveDestination) ->\n {context} = dest\n\n # sometimes time comes in very slightly negative o_o, maybe a rounding error?\n # Audio context throws an error if start is called with a negative argument\n unless time >= 0\n time = 0\n\n source = context.createBufferSource()\n source.buffer = buffer\n source.connect(dest or context.destination)\n source.start(time + context.currentTime)\n source.playbackRate.value = rate\n\n return source\n\n # Adjust playhead by dt beats, centering the scroll on it quantized to `q`\n adjustPlayhead: (dt, q=0) ->\n self.setPlayHead quantize playTime + dt, q\n self.recenterPlayhead()\n\n # Sets playhead cursor to the `t` in beats, clamping to song length\n # t in beats\n setPlayHead: (t) ->\n return if self.playing()\n\n # Needs to be < length otherwise section is undefined\n playTime = Math.min self.length() - 0.00001, Math.max t, 0\n self.activeSection self.sectionAt(playTime)[0]\n\n return playTime\n\n bufferTo: ->\n toBeat\n \n lastQueuedBeat: ->\n lastQueuedBeat\n\n playTime: ->\n playTime\n\n playFromStart: ->\n if self.playing()\n self.stop()\n else\n playTime = 0\n self.playing true\n\n initPlay()\n\n pause: ->\n if self.playing()\n self.stop()\n else\n self.playing true\n initPlay()\n\n play: ->\n self.pause()\n\n stop: ->\n self.playing false\n # Quick fade\n activeDestination.dispose()\n activeDestination = FXNetwork(liveDestination)\n\n rewind: ->\n self.stop()\n playTime = 0\n self.scrollTo(0)\n\n reset: ->\n self.stop()\n playTime = 0\n self.activeSection self.sections()[0]\n"
},
"player.coffee": {
- "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 feedback: ->\n window.open \"https://docs.google.com/forms/d/e/1FAIpQLSeRz9rCsLJLacvpJNAtAPhj0AN0LM155INP01Y8Tt4k2pIlmA/viewform\", \"_blank\"\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"
+ "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"
@@ -724,7 +724,7 @@
"content": "\n/*\nPlayer Audio\n============\n\nMain audio loop\n\nNeeds tempo, playable, start beat, end beat, looping mode to play.\n\nProvides playTime and playing methods.\n */\nvar Encoder, FX, FXNetwork, Modal, OfflineAudioContext, Progress, Style, limiter, liveContext, quantize, staffNoteToPitch, stereoAnalyser, _ref, _ref1, _ref2;\n\n_ref = system.ui, Progress = _ref.Progress, Modal = _ref.Modal, Style = _ref.Style;\n\nOfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;\n\nliveContext = require(\"./lib/audio-context\");\n\nEncoder = require(\"./lib/encoder/index\");\n\n_ref1 = require(\"./lib/util\"), quantize = _ref1.quantize, staffNoteToPitch = _ref1.staffNoteToPitch;\n\n_ref2 = FX = require(\"./lib/fx\"), limiter = _ref2.limiter, stereoAnalyser = _ref2.stereoAnalyser;\n\nFXNetwork = function(destination) {\n var context, fx, gain;\n context = destination.context;\n gain = context.createGain();\n gain.connect(destination);\n fx = FX.choices.reduce(function(o, name) {\n o[name] = FX[name](gain);\n return o;\n }, {});\n return {\n \"default\": gain,\n fx: fx,\n dispose: function() {\n var currentTime;\n currentTime = context.currentTime;\n gain.gain.exponentialRampToValueAtTime(0.001, 0.1 + currentTime);\n gain.gain.setValueAtTime(0, 0.1 + currentTime);\n return setTimeout(function() {\n return gain.disconnect();\n }, 0.25);\n }\n };\n};\n\nmodule.exports = function(I, self) {\n var activeDestination, analysedDestination, animBuffer, bps, bufferAudio, computeElapsedBeats, initPlay, lastContextTime, lastQueuedBeat, liveDestination, lookahead, playTime, playUpcomingSounds, secondsPerMinute, toBeat, wrapping;\n playTime = 0;\n secondsPerMinute = 60;\n lastContextTime = 0;\n lastQueuedBeat = 0;\n lookahead = 0.1;\n toBeat = null;\n wrapping = false;\n self.attrObservable(\"playing\");\n self.analysedDestination = analysedDestination = stereoAnalyser(liveContext.destination);\n liveDestination = limiter(analysedDestination);\n activeDestination = FXNetwork(liveDestination);\n document.addEventListener(\"visibilitychange\", function(e) {\n if (document.hidden) {\n lookahead = 1.25;\n } else {\n lookahead = 0.1;\n }\n return bufferAudio();\n });\n initPlay = function() {\n var context;\n context = liveDestination.context;\n context.resume();\n lastContextTime = context.currentTime;\n lastQueuedBeat = playTime;\n return wrapping = false;\n };\n computeElapsedBeats = function(playTime, elapsedSeconds) {\n var beats, beatsLeft, section, _bps, _ref3;\n if (playTime >= self.length()) {\n playTime -= self.length();\n }\n _ref3 = self.sectionAt(playTime), section = _ref3[0], beatsLeft = _ref3[1];\n _bps = section.tempo() / 60;\n beats = elapsedSeconds * _bps;\n if (beats > beatsLeft) {\n return beatsLeft + computeElapsedBeats(playTime + beatsLeft, elapsedSeconds - beatsLeft / _bps);\n } else {\n return beats;\n }\n };\n playUpcomingSounds = function(currentBeat, fromBeat, toBeat) {\n var dt;\n dt = toBeat - fromBeat;\n if (dt <= 0) {\n return;\n }\n return self.upcomingNotes(fromBeat, dt, function(_arg, section, s) {\n var accidental, beat, instrument, pitch, staffNote;\n beat = _arg[0], staffNote = _arg[1], accidental = _arg[2], instrument = _arg[3];\n pitch = staffNoteToPitch(staffNote, accidental, section.keySignature());\n return self.playNote(instrument, pitch, s, activeDestination.fx[section.presetName()]);\n }, currentBeat);\n };\n bps = function() {\n return self.tempo() / secondsPerMinute;\n };\n bufferAudio = function() {\n var context, currentTime, dt, elapsedBeats, length;\n if (self.playing()) {\n length = self.length();\n if (length < 1 || isNaN(length)) {\n return;\n }\n context = liveDestination.context;\n currentTime = context.currentTime;\n dt = currentTime - lastContextTime;\n if (dt === 0) {\n return;\n }\n lastContextTime = currentTime;\n elapsedBeats = computeElapsedBeats(playTime, dt);\n self.animateNoteElements(playTime, elapsedBeats);\n playTime += elapsedBeats;\n toBeat = playTime + computeElapsedBeats(playTime, lookahead);\n if (wrapping) {\n toBeat -= length;\n playUpcomingSounds(playTime - length, lastQueuedBeat, toBeat);\n } else {\n playUpcomingSounds(playTime, lastQueuedBeat, toBeat);\n if (toBeat >= length && self.loop()) {\n wrapping = true;\n toBeat -= length;\n lastQueuedBeat = 0;\n playUpcomingSounds(playTime - length, lastQueuedBeat, toBeat);\n }\n }\n lastQueuedBeat = Math.max(lastQueuedBeat, toBeat);\n if (playTime >= length) {\n wrapping = false;\n if (self.loop()) {\n playTime -= length;\n self.animateNoteElements(0, playTime);\n } else {\n playTime = 0;\n self.playing(false);\n }\n }\n self.activeSection(self.sectionAt(playTime)[0]);\n }\n };\n setInterval(function() {\n return bufferAudio();\n }, 1000 / 120);\n animBuffer = function() {\n requestAnimationFrame(animBuffer);\n return bufferAudio();\n };\n animBuffer();\n return self.extend({\n exportSong: function(song, opts) {\n var audioChannels, cleanup, err, extension, lengthInSeconds, name, offlineContext, offlineFxNetwork, progressView, samplesPerSecond, songLength, type;\n if (opts == null) {\n opts = {};\n }\n name = opts.name, type = opts.type;\n if (name == null) {\n name = \"song\";\n }\n if (type == null) {\n type = \"mp3\";\n }\n extension = \".\" + type;\n progressView = Progress({\n message: \"Rendering Audio...\"\n });\n Modal.show(progressView.element, {\n cancellable: false\n });\n cleanup = function() {\n return Modal.hide();\n };\n err = function(fn) {\n return function(e) {\n fn();\n throw e;\n };\n };\n songLength = song.length();\n audioChannels = 2;\n samplesPerSecond = 44100;\n lengthInSeconds = song.exportDuration();\n offlineContext = new OfflineAudioContext(audioChannels, samplesPerSecond * lengthInSeconds, samplesPerSecond);\n offlineFxNetwork = FXNetwork(limiter(offlineContext.destination));\n return new Promise(function(resolve, reject) {\n var dt, t, work;\n t = 0;\n dt = 1;\n work = function() {\n var p;\n song.upcomingNotes(t, dt, function(_arg, section, s) {\n var accidental, beat, instrument, keySig, note, presetName, staffNote;\n beat = _arg[0], staffNote = _arg[1], accidental = _arg[2], instrument = _arg[3];\n presetName = section.presetName();\n keySig = section.keySignature();\n note = staffNoteToPitch(staffNote, accidental, keySig);\n return self.playNote(instrument, note, s, offlineFxNetwork.fx[presetName]);\n }, 0);\n t += dt;\n if (t <= songLength) {\n setTimeout(work, 0);\n } else {\n p = offlineContext.startRendering();\n if (p) {\n p.then(resolve, reject);\n } else {\n offlineContext.oncomplete = function(_arg) {\n var renderedBuffer;\n renderedBuffer = _arg.renderedBuffer;\n return resolve(renderedBuffer);\n };\n }\n }\n };\n work();\n }).then(function(audioBuffer) {\n if (type === \"mp3\") {\n progressView.message(\"Encoding mp3...\");\n return Encoder.audioBufferToMP3(audioBuffer);\n } else {\n progressView.message(\"Encoding wav...\");\n return Encoder.audioBufferToWav(audioBuffer);\n }\n }).then(function(blob) {\n return blob.download(name + extension);\n }).then(cleanup, err(cleanup));\n },\n playNote: function(instrumentId, note, time, dest) {\n var buffer, context, gain, pan, panner, pitchShift, rate, sample, source, volume;\n if (note == null) {\n note = 0;\n }\n if (time == null) {\n time = 0;\n }\n if (dest == null) {\n dest = activeDestination.fx[self.presetName()] || liveDestination;\n }\n context = dest.context;\n sample = self.samples()[instrumentId].I;\n if (sample) {\n buffer = sample.buffer, pitchShift = sample.pitchShift, pan = sample.pan, volume = sample.volume;\n note += pitchShift;\n panner = context.createPanner();\n panner.panningModel = 'equalpower';\n panner.setPosition(pan, 0, 1 - Math.abs(pan));\n gain = context.createGain();\n gain.gain.value = volume;\n rate = Math.pow(2, note / 12);\n source = self.playBuffer(buffer, rate, time, panner);\n panner.connect(gain);\n gain.connect(dest);\n return source.onended = function() {\n return gain.disconnect();\n };\n }\n },\n playBuffer: function(buffer, rate, time, dest) {\n var context, source;\n if (rate == null) {\n rate = 1;\n }\n if (time == null) {\n time = 0;\n }\n if (dest == null) {\n dest = liveDestination;\n }\n context = dest.context;\n if (!(time >= 0)) {\n time = 0;\n }\n source = context.createBufferSource();\n source.buffer = buffer;\n source.connect(dest || context.destination);\n source.start(time + context.currentTime);\n source.playbackRate.value = rate;\n return source;\n },\n adjustPlayhead: function(dt, q) {\n if (q == null) {\n q = 0;\n }\n self.setPlayHead(quantize(playTime + dt, q));\n return self.recenterPlayhead();\n },\n setPlayHead: function(t) {\n if (self.playing()) {\n return;\n }\n playTime = Math.min(self.length() - 0.00001, Math.max(t, 0));\n self.activeSection(self.sectionAt(playTime)[0]);\n return playTime;\n },\n bufferTo: function() {\n return toBeat;\n },\n lastQueuedBeat: function() {\n return lastQueuedBeat;\n },\n playTime: function() {\n return playTime;\n },\n playFromStart: function() {\n if (self.playing()) {\n self.stop();\n } else {\n playTime = 0;\n self.playing(true);\n }\n return initPlay();\n },\n pause: function() {\n if (self.playing()) {\n return self.stop();\n } else {\n self.playing(true);\n return initPlay();\n }\n },\n play: function() {\n return self.pause();\n },\n stop: function() {\n self.playing(false);\n activeDestination.dispose();\n return activeDestination = FXNetwork(liveDestination);\n },\n rewind: function() {\n self.stop();\n playTime = 0;\n return self.scrollTo(0);\n },\n reset: function() {\n self.stop();\n playTime = 0;\n return self.activeSection(self.sections()[0]);\n }\n });\n};\n"
},
"player": {
- "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 feedback: function() {\n return window.open(\"https://docs.google.com/forms/d/e/1FAIpQLSeRz9rCsLJLacvpJNAtAPhj0AN0LM155INP01Y8Tt4k2pIlmA/viewform\", \"_blank\");\n },\n discord: function() {\n return window.open(\"https://discord.gg/wcpWDNk\", \"_blank\");\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"
+ "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"
@@ -742,7 +742,7 @@
"content": "module.exports = system.ui.Jadelet.exec([\"about\",{},[[\"h1\",{},[\"About\"]],[\"p\",{},[\"ProTip™ hold Shift to sharp, Ctrl to flat\"]],[\"p\",{},[\"By\\n\",[\"a\",{\"href\":\"https://danielx.net\"},[\"Daniel X. Moore\"]],\" creator of\\n\",[\"a\",{\"href\":\"https://whimsy.space\"},[\"Whimsy.Space\"]]]],[\"p\",{},[\"Email me at: \\n\",[\"a\",{\"href\":\"mailto:daniel+composer@danielx.net\",\"title\":\"Email me!\"},[\"daniel+composer@danielx.net\"]]]],[\"actions\",{},[[\"a\",{\"class\":[\"button\"],\"href\":\"https://www.youtube.com/channel/UChCCc_j4Q2n1fiNjhlomkqA/videos\",\"target\":\"_blank\"},[\"📺 YouTube\"]],[\"button\",{\"click\":{\"bind\":\"discord\"}},[\"☎️ Discord\"]],[\"button\",{\"click\":{\"bind\":\"showHelp\"}},[\"⌨️ Hotkeys\"]],[\"button\",{\"click\":{\"bind\":\"oldVersion\"}},[\"👵 Old Version\"]]]]]]);"
},
"templates/actions": {
- "content": "module.exports = system.ui.Jadelet.exec([\"aside\",{\"class\":[\"actions\"]},[[\"section\",{},[[\"button\",{\"class\":[\"loop\",{\"bind\":\"loopButtonClass\"}],\"click\":{\"bind\":\"toggleLoop\"}},[\"𝄇\"]],{\"bind\":\"meterPickerElement\"},{\"bind\":\"noteControlElement\"},{\"bind\":\"fxPickerElement\"},{\"bind\":\"stereoAnalyserElement\"}]],[\"section\",{\"class\":[\"buttons\"]},[[\"button\",{\"class\":[\"play\",{\"bind\":\"playButtonClass\"}],\"click\":{\"bind\":\"play\"},\"title\":\"Play\"},[[\"span\",{\"class\":[\"icon\"]},[\"▶️\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Play\"]]]],[\"button\",{\"click\":{\"bind\":\"rewind\"},\"title\":\"Rewind\"},[[\"span\",{\"class\":[\"icon\"]},[\"⏪\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Go to Start\"]]]],[\"label\",{},[[\"h2\",{},[\"Tempo\"]],[\"input\",{\"value\":{\"bind\":\"tempo\"},\"keydown\":{\"bind\":\"inputHotkeys\"},\"type\":\"number\",\"min\":\"1\",\"step\":\"1\"},[]]]],[\"label\",{},[[\"h2\",{},[\"Length\"]],[\"input\",{\"value\":{\"bind\":\"length\"},\"keydown\":{\"bind\":\"inputHotkeys\"},\"type\":\"number\",\"min\":\"4\",\"step\":\"4\"},[]]]],[\"button\",{\"class\":[{\"bind\":\"hiddenUnlessPurchased\"}],\"click\":{\"bind\":\"showArranger\"}},[[\"span\",{\"class\":[\"icon\"]},[\"📑\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Sections\"]]]],[\"button\",{\"click\":{\"bind\":\"undo\"},\"disabled\":{\"bind\":\"undoDisabled\"}},[[\"span\",{\"class\":[\"icon\"]},[\"↩️\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Undo\"]]]],[\"button\",{\"click\":{\"bind\":\"redo\"},\"disabled\":{\"bind\":\"redoDisabled\"}},[[\"span\",{\"class\":[\"icon\"]},[\"↪️\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Redo\"]]]],[\"button\",{\"click\":{\"bind\":\"clear\"},\"disabled\":{\"bind\":\"clearDisabled\"}},[[\"span\",{\"class\":[\"icon\"]},[\"💣\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Clear\"]]]],[\"button\",{\"click\":{\"bind\":\"showSpriteConfig\"}},[[\"span\",{\"class\":[\"icon\"]},[\"⚙️\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Settings\"]]]],[\"button\",{\"click\":{\"bind\":\"showPersistenceModal\"}},[[\"span\",{\"class\":[\"icon\"]},[\"💾\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Save/Load\"]]]],[\"button\",{\"class\":[\"right\",{\"bind\":\"hiddenIfPurchased\"}],\"click\":{\"bind\":\"purchase\"}},[\"💸 Buy Now! 💸\"]],[\"a\",{\"class\":[\"button\"],\"href\":\"https://www.redbubble.com/shop/ap/56719884\",\"target\":\"_blank\"},[[\"span\",{\"class\":[\"icon\"]},[\"👕\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Merch\"]]]],[\"button\",{\"click\":{\"bind\":\"about\"}},[[\"span\",{\"class\":[\"icon\"]},[\"📚\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"About\"]]]],[\"button\",{\"click\":{\"bind\":\"feedback\"}},[[\"span\",{\"class\":[\"icon\"]},[\"💬\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Feedback\"]]]]]]]]);"
+ "content": "module.exports = system.ui.Jadelet.exec([\"aside\",{\"class\":[\"actions\"]},[[\"section\",{},[[\"button\",{\"class\":[\"loop\",{\"bind\":\"loopButtonClass\"}],\"click\":{\"bind\":\"toggleLoop\"}},[\"𝄇\"]],{\"bind\":\"meterPickerElement\"},{\"bind\":\"noteControlElement\"},{\"bind\":\"fxPickerElement\"},{\"bind\":\"stereoAnalyserElement\"}]],[\"section\",{\"class\":[\"buttons\"]},[[\"button\",{\"class\":[\"play\",{\"bind\":\"playButtonClass\"}],\"click\":{\"bind\":\"play\"},\"title\":\"Play\"},[[\"span\",{\"class\":[\"icon\"]},[\"▶️\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Play\"]]]],[\"button\",{\"click\":{\"bind\":\"rewind\"},\"title\":\"Rewind\"},[[\"span\",{\"class\":[\"icon\"]},[\"⏪\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Go to Start\"]]]],[\"label\",{},[[\"h2\",{},[\"Tempo\"]],[\"input\",{\"value\":{\"bind\":\"tempo\"},\"keydown\":{\"bind\":\"inputHotkeys\"},\"type\":\"number\",\"min\":\"1\",\"step\":\"1\"},[]]]],[\"label\",{},[[\"h2\",{},[\"Length\"]],[\"input\",{\"value\":{\"bind\":\"length\"},\"keydown\":{\"bind\":\"inputHotkeys\"},\"type\":\"number\",\"min\":\"4\",\"step\":\"4\"},[]]]],[\"button\",{\"class\":[{\"bind\":\"hiddenUnlessPurchased\"}],\"click\":{\"bind\":\"showArranger\"}},[[\"span\",{\"class\":[\"icon\"]},[\"📑\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Sections\"]]]],[\"button\",{\"click\":{\"bind\":\"undo\"},\"disabled\":{\"bind\":\"undoDisabled\"}},[[\"span\",{\"class\":[\"icon\"]},[\"↩️\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Undo\"]]]],[\"button\",{\"click\":{\"bind\":\"redo\"},\"disabled\":{\"bind\":\"redoDisabled\"}},[[\"span\",{\"class\":[\"icon\"]},[\"↪️\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Redo\"]]]],[\"button\",{\"click\":{\"bind\":\"clear\"},\"disabled\":{\"bind\":\"clearDisabled\"}},[[\"span\",{\"class\":[\"icon\"]},[\"💣\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Clear\"]]]],[\"button\",{\"click\":{\"bind\":\"showSpriteConfig\"}},[[\"span\",{\"class\":[\"icon\"]},[\"⚙️\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Settings\"]]]],[\"button\",{\"click\":{\"bind\":\"showPersistenceModal\"}},[[\"span\",{\"class\":[\"icon\"]},[\"💾\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Save/Load\"]]]],[\"button\",{\"class\":[\"right\",{\"bind\":\"hiddenIfPurchased\"}],\"click\":{\"bind\":\"purchase\"}},[\"💸 Buy Now! 💸\"]],[\"a\",{\"class\":[\"button\"],\"href\":\"https://www.redbubble.com/shop/ap/56719884\",\"target\":\"_blank\"},[[\"span\",{\"class\":[\"icon\"]},[\"👕\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"Merch\"]]]],[\"button\",{\"click\":{\"bind\":\"about\"}},[[\"span\",{\"class\":[\"icon\"]},[\"📚\"]],\"\\n\",[\"span\",{\"class\":[\"description\"]},[\"About\"]]]]]]]]);"
},
"templates/app": {
"content": "module.exports = system.ui.Jadelet.exec([\"app\",{},[{\"bind\":\"toolsElement\"},{\"bind\":\"patternsElement\"},{\"bind\":\"staffElement\"},{\"bind\":\"actionsElement\"},[\"pre\",{\"class\":[\"position\"]},[{\"bind\":\"notePosition\"}]]]]);"