From baf68794ba888bb5b2620551ab2b22ae18a1473e Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Mon, 10 May 2021 11:10:20 -0500 Subject: [PATCH] Forgot to add index.html --- index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 35e5209..5f6b409 100755 --- a/index.html +++ b/index.html @@ -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 \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
ActionKey
Select Instrument0-9
Select Instrument<backtick> 1-7
Select PatternShift+0-9
Eraser Toole
Selection Tools
UndoCtrl+z, ⌘+z
RedoCtrl+y, ⌘+y
Play/PauseSpace
Play from BeginningEnter
 
Selection
Copyc
Cutx
DeleteDelete
ResetEsc
 
Data
SaveCtrl+s, ⌘+s
OpenCtrl+o, ⌘+o
ExportCtrl+r, ⌘+r
 
Meta
AboutF1
Toggle Full screenF11
Help?
\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 = 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" + "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}assets/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" @@ -343,7 +343,7 @@ "content": "// https://stackoverflow.com/a/42632646/68210\r\n\r\nself.onmessage = function( e ){\r\n var wavPCM = new WavePCM( e['data']['config'] );\r\n wavPCM.record( e['data']['pcmArrays'] );\r\n wavPCM.requestData();\r\n};\r\n\r\nvar WavePCM = function( config ){\r\n this.sampleRate = config['sampleRate'] || 48000;\r\n this.bitDepth = config['bitDepth'] || 16;\r\n this.recordedBuffers = [];\r\n this.bytesPerSample = this.bitDepth / 8;\r\n};\r\n\r\nWavePCM.prototype.record = function( buffers ){\r\n this.numberOfChannels = this.numberOfChannels || buffers.length;\r\n var bufferLength = buffers[0].length;\r\n var reducedData = new Uint8Array( bufferLength * this.numberOfChannels * this.bytesPerSample );\r\n\r\n // Interleave\r\n for ( var i = 0; i < bufferLength; i++ ) {\r\n for ( var channel = 0; channel < this.numberOfChannels; channel++ ) {\r\n\r\n var outputIndex = ( i * this.numberOfChannels + channel ) * this.bytesPerSample;\r\n var sample = buffers[ channel ][ i ];\r\n\r\n // Check for clipping\r\n if ( sample > 1 ) {\r\n sample = 1;\r\n }\r\n\r\n else if ( sample < -1 ) {\r\n sample = -1;\r\n }\r\n\r\n // bit reduce and convert to uInt\r\n switch ( this.bytesPerSample ) {\r\n case 4:\r\n sample = sample * 2147483648;\r\n reducedData[ outputIndex ] = sample;\r\n reducedData[ outputIndex + 1 ] = sample >> 8;\r\n reducedData[ outputIndex + 2 ] = sample >> 16;\r\n reducedData[ outputIndex + 3 ] = sample >> 24;\r\n break;\r\n\r\n case 3:\r\n sample = sample * 8388608;\r\n reducedData[ outputIndex ] = sample;\r\n reducedData[ outputIndex + 1 ] = sample >> 8;\r\n reducedData[ outputIndex + 2 ] = sample >> 16;\r\n break;\r\n\r\n case 2:\r\n sample = sample * 32768;\r\n reducedData[ outputIndex ] = sample;\r\n reducedData[ outputIndex + 1 ] = sample >> 8;\r\n break;\r\n\r\n case 1:\r\n reducedData[ outputIndex ] = ( sample + 1 ) * 128;\r\n break;\r\n\r\n default:\r\n throw \"Only 8, 16, 24 and 32 bits per sample are supported\";\r\n }\r\n }\r\n }\r\n\r\n this.recordedBuffers.push( reducedData );\r\n};\r\n\r\nWavePCM.prototype.requestData = function(){\r\n var bufferLength = this.recordedBuffers[0].length;\r\n var dataLength = this.recordedBuffers.length * bufferLength;\r\n var headerLength = 44;\r\n var wav = new Uint8Array( headerLength + dataLength );\r\n var view = new DataView( wav.buffer );\r\n\r\n view.setUint32( 0, 1380533830, false ); // RIFF identifier 'RIFF'\r\n view.setUint32( 4, 36 + dataLength, true ); // file length minus RIFF identifier length and file description length\r\n view.setUint32( 8, 1463899717, false ); // RIFF type 'WAVE'\r\n view.setUint32( 12, 1718449184, false ); // format chunk identifier 'fmt '\r\n view.setUint32( 16, 16, true ); // format chunk length\r\n view.setUint16( 20, 1, true ); // sample format (raw)\r\n view.setUint16( 22, this.numberOfChannels, true ); // channel count\r\n view.setUint32( 24, this.sampleRate, true ); // sample rate\r\n view.setUint32( 28, this.sampleRate * this.bytesPerSample * this.numberOfChannels, true ); // byte rate (sample rate * block align)\r\n view.setUint16( 32, this.bytesPerSample * this.numberOfChannels, true ); // block align (channel count * bytes per sample)\r\n view.setUint16( 34, this.bitDepth, true ); // bits per sample\r\n view.setUint32( 36, 1684108385, false); // data chunk identifier 'data'\r\n view.setUint32( 40, dataLength, true ); // data chunk length\r\n\r\n for (var i = 0; i < this.recordedBuffers.length; i++ ) {\r\n wav.set( this.recordedBuffers[i], i * bufferLength + headerLength );\r\n }\r\n\r\n self.postMessage( wav, [wav.buffer] );\r\n self.close();\r\n};\r\n" }, "lib/encoder/index.coffee": { - "content": "###\nEncode web audio buffer data in a variety of formats. Handles both stereo and\nmono.\n\nMP3, WAV\n\nUse web workers where appropriate.\n\nStatus: Acceptable\n###\n\n# \n# channelData: [Int16Array]\n# returns a Promise containing a Blob with type audio/mp3\nmp3Encode = (data) ->\n remoteWorker(\"https://danielx.net/composer/lame-worker2.js\")\n .then (worker) ->\n new Promise (resolve, reject) ->\n worker.postMessage data\n\n worker.onmessage = (e) ->\n resolve e.data\n\nremoteWorker = (url) ->\n fetch(url)\n .then (response) ->\n response.arrayBuffer()\n .then (buffer) ->\n blob = new Blob [buffer], type: \"application/javascript\"\n URL.createObjectURL(blob)\n .then (url) ->\n # TODO: revokeObjectURL\n new Worker(url)\n\n# Stereo or Mono\naudioBufferToChannelData = (audioBuffer) ->\n channels = audioBuffer.numberOfChannels\n if channels is 2\n [\n audioBuffer.getChannelData(0)\n audioBuffer.getChannelData(1)\n ]\n else\n [audioBuffer.getChannelData(0)]\n\naudioChannelDataToInt16 = (buffer) ->\n {min, max} = Math\n\n bufferLength = buffer.length\n data = new Int16Array bufferLength\n\n i = 0\n while i < bufferLength\n sample = max -1, min 1, buffer[i]\n\n if sample < 0\n sample *= 0x8000\n else\n sample *= 0x7FFF\n\n data[i] = sample\n i++\n\n return data\n\naudioBufferToMP3 = (audioBuffer) ->\n throw new Error \"Must pass an AudioBuffer to encode .mp3\" unless audioBuffer\n\n bufferWorker = fnToWorker(audioChannelDataToInt16)\n \n Promise.all audioBufferToChannelData(audioBuffer).map (channelBuffer) ->\n bufferWorker(channelBuffer)\n .then (channelData) ->\n mp3Encode\n channelData: channelData\n\naudioBufferToWav = (audioBuffer) ->\n throw new Error \"Must pass an AudioBuffer to encode .wav\" unless audioBuffer\n\n new Promise (resolve, reject) ->\n workerSource = new Blob [PACKAGE.distribution[\"lib/encoder/wave-worker\"].content],\n type: \"application/javascript\"\n\n url = URL.createObjectURL(workerSource)\n worker = new Worker(url)\n\n worker.onmessage = (e) ->\n resolve new Blob [e.data.buffer], type: \"audio/wav\"\n URL.revokeObjectURL(url)\n\n worker.postMessage\n pcmArrays: audioBufferToChannelData(audioBuffer)\n config:\n sampleRate: audioBuffer.sampleRate\n\n# Convert a function without any closure variables into a worker source\n# it receives a single parameter from the onmessage event data, passes it to\n# the function, returns the value to the parent, then closes the worker.\nfnToWorker = (fn) ->\n (data) ->\n new Promise (resolve, reject) ->\n workerSource = new Blob [\"\"\"\n self.onmessage = function (e) {\n self.postMessage((#{fn.toString()})(e.data));\n self.close();\n };\n \"\"\"], type: \"application/javascript\"\n \n url = URL.createObjectURL(workerSource)\n worker = new Worker(url)\n\n # Resolev and cleanup when we receive the message from the worker.\n worker.onmessage = (e) ->\n resolve(e.data)\n URL.revokeObjectURL(url)\n\n worker.postMessage data\n\nmodule.exports =\n mp3Encode: mp3Encode\n audioBufferToMP3: audioBufferToMP3\n audioBufferToWav: audioBufferToWav\n audioChannelDataToInt16: audioChannelDataToInt16\n fnToWorker: fnToWorker\n" + "content": "###\nEncode web audio buffer data in a variety of formats. Handles both stereo and\nmono.\n\nMP3, WAV\n\nUse web workers where appropriate.\n\nStatus: Acceptable\n###\n\n# \n# channelData: [Int16Array]\n# returns a Promise containing a Blob with type audio/mp3\nmp3Encode = (data) ->\n remoteWorker(\"lame-worker2.js\")\n .then (worker) ->\n new Promise (resolve, reject) ->\n worker.postMessage data\n\n worker.onmessage = (e) ->\n resolve e.data\n\nremoteWorker = (url) ->\n fetch(url)\n .then (response) ->\n response.arrayBuffer()\n .then (buffer) ->\n blob = new Blob [buffer], type: \"application/javascript\"\n URL.createObjectURL(blob)\n .then (url) ->\n # TODO: revokeObjectURL\n new Worker(url)\n\n# Stereo or Mono\naudioBufferToChannelData = (audioBuffer) ->\n channels = audioBuffer.numberOfChannels\n if channels is 2\n [\n audioBuffer.getChannelData(0)\n audioBuffer.getChannelData(1)\n ]\n else\n [audioBuffer.getChannelData(0)]\n\naudioChannelDataToInt16 = (buffer) ->\n {min, max} = Math\n\n bufferLength = buffer.length\n data = new Int16Array bufferLength\n\n i = 0\n while i < bufferLength\n sample = max -1, min 1, buffer[i]\n\n if sample < 0\n sample *= 0x8000\n else\n sample *= 0x7FFF\n\n data[i] = sample\n i++\n\n return data\n\naudioBufferToMP3 = (audioBuffer) ->\n throw new Error \"Must pass an AudioBuffer to encode .mp3\" unless audioBuffer\n\n bufferWorker = fnToWorker(audioChannelDataToInt16)\n \n Promise.all audioBufferToChannelData(audioBuffer).map (channelBuffer) ->\n bufferWorker(channelBuffer)\n .then (channelData) ->\n mp3Encode\n channelData: channelData\n\naudioBufferToWav = (audioBuffer) ->\n throw new Error \"Must pass an AudioBuffer to encode .wav\" unless audioBuffer\n\n new Promise (resolve, reject) ->\n workerSource = new Blob [PACKAGE.distribution[\"lib/encoder/wave-worker\"].content],\n type: \"application/javascript\"\n\n url = URL.createObjectURL(workerSource)\n worker = new Worker(url)\n\n worker.onmessage = (e) ->\n resolve new Blob [e.data.buffer], type: \"audio/wav\"\n URL.revokeObjectURL(url)\n\n worker.postMessage\n pcmArrays: audioBufferToChannelData(audioBuffer)\n config:\n sampleRate: audioBuffer.sampleRate\n\n# Convert a function without any closure variables into a worker source\n# it receives a single parameter from the onmessage event data, passes it to\n# the function, returns the value to the parent, then closes the worker.\nfnToWorker = (fn) ->\n (data) ->\n new Promise (resolve, reject) ->\n workerSource = new Blob [\"\"\"\n self.onmessage = function (e) {\n self.postMessage((#{fn.toString()})(e.data));\n self.close();\n };\n \"\"\"], type: \"application/javascript\"\n \n url = URL.createObjectURL(workerSource)\n worker = new Worker(url)\n\n # Resolev and cleanup when we receive the message from the worker.\n worker.onmessage = (e) ->\n resolve(e.data)\n URL.revokeObjectURL(url)\n\n worker.postMessage data\n\nmodule.exports =\n mp3Encode: mp3Encode\n audioBufferToMP3: audioBufferToMP3\n audioBufferToWav: audioBufferToWav\n audioChannelDataToInt16: audioChannelDataToInt16\n fnToWorker: fnToWorker\n" }, "lib/test/encoder/index.coffee": { "content": "{\n audioBufferToMP3\n audioBufferToWav\n audioChannelDataToInt16\n fnToWorker\n} = Encoder = require \"/lib/encoder/index\"\n\n{abs} = Math\n\n# Noise buffer\ncontext = new AudioContext()\nstereoBuffer = context.createBuffer(2, 65536, 44100)\nmonoBuffer = context.createBuffer(1, 65536, 44100)\n\nnumberOfChannels = stereoBuffer.numberOfChannels\nc = 0\nwhile c < numberOfChannels\n buffer = stereoBuffer.getChannelData(c)\n i = 0\n while i < buffer.length\n # white noise in [-1.0, 1.0]\n buffer[i] = Math.random() * 2 - 1\n i++\n c++\n\nbuffer = monoBuffer.getChannelData(0)\ni = 0\nwhile i < buffer.length\n # white noise in [-1.0, 1.0]\n buffer[i] = Math.random() * 2 - 1\n i++\n\ndownloadLink = (blob, name) ->\n link = document.createElement 'a'\n link.textContent = link.download = name\n link.href = URL.createObjectURL(blob)\n document.body.appendChild link\n return\n\ndescribe \"Audio Encoder\", ->\n it \"should encode MP3s in stereo\", ->\n audioBufferToMP3(stereoBuffer)\n\n it \"should encode MP3s in mono\", ->\n audioBufferToMP3(monoBuffer)\n\n it \"should encode wav in stereo\", ->\n audioBufferToWav(stereoBuffer)\n\n it \"should encode wav in mono\", ->\n audioBufferToWav(monoBuffer)\n\n it \"should convert audioBuffer to Int16Array\", ->\n assert audioChannelDataToInt16 monoBuffer.getChannelData(0)\n\n assert audioChannelDataToInt16 stereoBuffer.getChannelData(0)\n assert audioChannelDataToInt16 stereoBuffer.getChannelData(1)\n\n it \"should convert to Int16Array a web worker\", ->\n workerFn = fnToWorker(audioChannelDataToInt16)\n\n workerFn monoBuffer.getChannelData(0)\n\n it \"should have a proper distribution of values when converting to int16\"\n ->\n b = audioChannelDataToInt16(monoBuffer.getChannelData(0))\n\n bins = new Map\n l = b.length\n\n i = 0\n while i < l\n v = b[i] + 0x8000\n c = (bins.get(v)|0)\n bins.set(v, c+1)\n i++\n\n console.log(\"i\", i)\n expected = 0\n actual = 0\n maxDiscrepancy = 0\n maxDiscrepancyIndex = null\n i = 0\n while i < 65536\n c = (bins.get(i)|0)\n expected += 10\n actual += c\n\n d = abs(expected - actual)\n if d > maxDiscrepancy\n maxDiscrepancy = d\n maxDiscrepancyIndex = i\n\n i++\n\n sum = Array.from(bins.values()).reduce (a, b) -> a+b\n \n console.log \"SUM\", sum, actual, expected\n console.log b, bins\n console.log \"Disc\", maxDiscrepancy, maxDiscrepancyIndex\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\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
ActionKey
Select Instrument0-9
Select Instrument<backtick> 1-7
Select PatternShift+0-9
Eraser Toole
Selection Tools
UndoCtrl+z, ⌘+z
RedoCtrl+y, ⌘+y
Play/PauseSpace
Play from BeginningEnter
 
Selection
Copyc
Cutx
DeleteDelete
ResetEsc
 
Data
SaveCtrl+s, ⌘+s
OpenCtrl+o, ⌘+o
ExportCtrl+r, ⌘+r
 
Meta
AboutF1
Toggle Full screenF11
Help?
\";\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 = 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" + "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 + \"assets/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"