Repository: stufreen/web-drum-sequencer Branch: master Commit: 84446c7aa128 Files: 199 Total size: 381.2 KB Directory structure: gitextract_4_fwl4lt/ ├── .circleci/ │ └── config.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── CNAME ├── README.md ├── __mocks__/ │ ├── fileMock.js │ └── styleMock.js ├── babel.config.js ├── index.html ├── manifest.json ├── package.json ├── public/ │ ├── assets/ │ │ └── icons/ │ │ └── browserconfig.xml │ └── sw.js ├── src/ │ ├── __mocks__/ │ │ └── samples.config.js │ ├── assets/ │ │ └── js/ │ │ ├── webaudio-controls.js │ │ └── webcomponents-lite.js │ ├── common/ │ │ ├── channels/ │ │ │ ├── channels.actions.js │ │ │ ├── channels.constants.js │ │ │ ├── channels.reducer.js │ │ │ ├── channels.reducer.test.js │ │ │ ├── channels.selectors.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── master/ │ │ │ ├── index.js │ │ │ ├── master.actions.js │ │ │ ├── master.constants.js │ │ │ ├── master.reducer.js │ │ │ ├── master.reducer.test.js │ │ │ └── master.selectors.js │ │ ├── notes/ │ │ │ ├── index.js │ │ │ ├── notes.actions.js │ │ │ ├── notes.constants.js │ │ │ ├── notes.reducer.js │ │ │ ├── notes.reducer.test.js │ │ │ └── notes.selectors.js │ │ ├── playbackSession/ │ │ │ ├── index.js │ │ │ ├── playbackSession.actions.js │ │ │ ├── playbackSession.constants.js │ │ │ ├── playbackSession.reducer.js │ │ │ ├── playbackSession.reducer.test.js │ │ │ └── playbackSession.selectors.js │ │ ├── presets/ │ │ │ ├── index.js │ │ │ ├── presets.actions.js │ │ │ ├── presets.constants.js │ │ │ ├── presets.reducer.js │ │ │ ├── presets.reducer.test.js │ │ │ └── presets.selectors.js │ │ ├── tempo/ │ │ │ ├── index.js │ │ │ ├── tempo.actions.js │ │ │ ├── tempo.constants.js │ │ │ ├── tempo.reducer.js │ │ │ ├── tempo.reducer.test.js │ │ │ └── tempo.selectors.js │ │ ├── userSamples/ │ │ │ ├── index.js │ │ │ ├── userSamples.actions.js │ │ │ ├── userSamples.constants.js │ │ │ ├── userSamples.reducer.js │ │ │ └── userSamples.selectors.js │ │ └── window/ │ │ ├── index.js │ │ ├── window.actions.js │ │ ├── window.constants.js │ │ ├── window.reducer.js │ │ ├── window.reducer.test.js │ │ └── window.selectors.js │ ├── components/ │ │ ├── AddChannelButton/ │ │ │ ├── AddChannelButton.component.jsx │ │ │ ├── AddChannelButton.container.js │ │ │ └── index.js │ │ ├── App.jsx │ │ ├── BPMInput/ │ │ │ ├── BPMInput.component.jsx │ │ │ ├── BPMInput.container.js │ │ │ ├── BPMInput.selectors.js │ │ │ └── index.js │ │ ├── Branding.jsx │ │ ├── Channel/ │ │ │ ├── Channel.component.jsx │ │ │ ├── Channel.container.js │ │ │ ├── Channel.selectors.js │ │ │ ├── HitButton.component.jsx │ │ │ ├── RemoveButton.component.jsx │ │ │ └── index.js │ │ ├── ChannelControls/ │ │ │ ├── ChannelControls.component.jsx │ │ │ ├── ChannelControls.container.js │ │ │ ├── ChannelControls.selectors.js │ │ │ └── index.js │ │ ├── ChannelHeader/ │ │ │ ├── ChannelHeader.component.jsx │ │ │ ├── ChannelHeaderLabel.component.jsx │ │ │ └── index.js │ │ ├── ChannelList/ │ │ │ ├── ChannelList.component.jsx │ │ │ ├── ChannelList.container.js │ │ │ ├── ChannelList.selectors.js │ │ │ └── index.js │ │ ├── FancyButton.component.jsx │ │ ├── FlashMessage/ │ │ │ ├── FlashMessage.component.jsx │ │ │ ├── FlashMessage.container.js │ │ │ ├── FlashMessage.selectors.js │ │ │ └── index.js │ │ ├── GithubLink.component.jsx │ │ ├── InfoKnob.component.jsx │ │ ├── InstallButton.jsx │ │ ├── Knob.component.jsx │ │ ├── LabelBox.jsx │ │ ├── Logo.component.jsx │ │ ├── Marker/ │ │ │ ├── Marker.component.jsx │ │ │ ├── Marker.container.js │ │ │ ├── Marker.selectors.js │ │ │ └── index.js │ │ ├── MasterControls/ │ │ │ ├── MasterControls.component.jsx │ │ │ └── index.js │ │ ├── Modal.component.jsx │ │ ├── MuteSolo/ │ │ │ ├── MuteSolo.component.jsx │ │ │ ├── MuteSolo.container.js │ │ │ └── index.js │ │ ├── PatternSelector/ │ │ │ ├── PatternSelector.component.jsx │ │ │ ├── PatternSelector.container.js │ │ │ ├── PatternSelector.selectors.js │ │ │ └── index.js │ │ ├── PlayButton/ │ │ │ ├── PlayButton.component.jsx │ │ │ ├── PlayButton.container.js │ │ │ ├── PlayButton.selectors.js │ │ │ └── index.js │ │ ├── PresetDeleted.component.jsx │ │ ├── PresetSaved.component.jsx │ │ ├── PresetSelector/ │ │ │ ├── PresetSelector.component.jsx │ │ │ ├── PresetSelector.container.js │ │ │ ├── PresetSelector.selectors.js │ │ │ └── index.js │ │ ├── SampleLoadError.component.jsx │ │ ├── SampleSelect/ │ │ │ ├── SampleSelect.component.jsx │ │ │ ├── SampleSelect.container.js │ │ │ ├── SampleSelect.selectors.js │ │ │ └── index.js │ │ ├── SavePresetModal/ │ │ │ ├── SavePresetModal.component.jsx │ │ │ ├── SavePresetModal.container.js │ │ │ ├── SavePresetModal.selectors.js │ │ │ └── index.js │ │ ├── SwingControl/ │ │ │ ├── SwingControl.component.jsx │ │ │ ├── SwingControl.container.js │ │ │ ├── SwingControl.selectors.js │ │ │ └── index.js │ │ ├── Toggles/ │ │ │ ├── Toggle.component.jsx │ │ │ ├── ToggleGroup.component.jsx │ │ │ ├── Toggles.component.jsx │ │ │ ├── Toggles.container.js │ │ │ ├── Toggles.selectors.js │ │ │ └── index.js │ │ ├── VolumeMeter.component.jsx │ │ ├── design-system/ │ │ │ ├── Box.js │ │ │ ├── Button.js │ │ │ ├── ControlLabel.js │ │ │ ├── Form.js │ │ │ ├── Heading.js │ │ │ ├── HoverButton.js │ │ │ ├── HoverLink.js │ │ │ ├── Image.js │ │ │ ├── Label.js │ │ │ ├── Line.js │ │ │ ├── Text.js │ │ │ ├── TextInput.js │ │ │ └── index.js │ │ ├── index.js │ │ └── timedCallback.hoc.jsx │ ├── index.html │ ├── index.jsx │ ├── presets/ │ │ ├── 707.js │ │ ├── 808.js │ │ ├── __mocks__/ │ │ │ └── index.js │ │ ├── ace.js │ │ ├── empty.js │ │ ├── hip-hop.js │ │ ├── index.js │ │ └── ldrum.js │ ├── reducer.js │ ├── samples.config.js │ ├── services/ │ │ ├── __mocks__/ │ │ │ ├── audioContext.js │ │ │ ├── audioRouter.js │ │ │ └── featureChecks.js │ │ ├── animations.js │ │ ├── audioAnalyzer.js │ │ ├── audioContext.js │ │ ├── audioContext.test.js │ │ ├── audioEngine.config.js │ │ ├── audioLoop.js │ │ ├── audioRouter.js │ │ ├── audioScheduler.js │ │ ├── audioScheduler.test.js │ │ ├── database.js │ │ ├── featureChecks.js │ │ ├── fileUtils.js │ │ ├── pwaInstall.js │ │ ├── reverb.js │ │ ├── sampleStore.js │ │ ├── swing.js │ │ ├── unmute.js │ │ └── uuid.js │ ├── store.js │ └── styles/ │ ├── globalStyles.js │ └── theme.js └── vite.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ # Javascript Node CircleCI 2.0 configuration file # # Check https://circleci.com/docs/2.0/language-javascript/ for more details # version: 2 jobs: build: docker: # specify the version you desire here - image: cimg/node:16.13.2 # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ # - image: circleci/mongo:3.4.4 working_directory: ~/repo steps: - checkout # Download and cache dependencies - restore_cache: keys: - v1-dependencies-{{ checksum "package.json" }} # fallback to using the latest cache if no exact match is found - v1-dependencies- - run: npm install - run: npm run build - persist_to_workspace: root: ~/repo paths: - dist - save_cache: paths: - node_modules key: v1-dependencies-{{ checksum "package.json" }} test: docker: - image: cimg/node:16.13.2 working_directory: ~/repo steps: - checkout # Download and cache dependencies - restore_cache: keys: - v1-dependencies-{{ checksum "package.json" }} # fallback to using the latest cache if no exact match is found - v1-dependencies- - run: npm install - run: name: Test command: npm run test - run: name: Lint command: npm run lint # run tests! #- run: yarn test deploy: docker: - image: circleci/python:2.7-jessie working_directory: ~/repo steps: - attach_workspace: # Must be absolute path or relative path from working_directory at: ~/repo - run: name: Install awscli command: sudo pip install awscli - run: name: Deploy to S3 command: aws s3 sync --acl public-read ~/repo/dist s3://wds-1.com/ --delete workflows: version: 2 build-deploy: jobs: - build - test: requires: - build - deploy: requires: - test filters: branches: only: master ================================================ FILE: .eslintignore ================================================ src/assets/**/*.js ================================================ FILE: .eslintrc ================================================ { "extends": ["eslint:recommended", "plugin:react/recommended"], "env": { "browser": true, "jest": true, "es6": true }, "rules": { "import/prefer-default-export": 0, "global-require": 0, "jsx-a11y/label-has-for": [0], "jsx-a11y/label-has-associated-control": [0], "default-param-last": 0, "implicit-arrow-linebreak": 0 }, "parser": "@babel/eslint-parser" } ================================================ FILE: .gitignore ================================================ node_modules .vscode .DS_Store dist/* ================================================ FILE: .prettierrc ================================================ { "trailingComma": "all", "singleQuote": true } ================================================ FILE: CNAME ================================================ wds-1.com ================================================ FILE: README.md ================================================ # Web Drum Sequencer A browser-based drum machine and sequencer built with the Web Audio API, React, and Redux. ## Demo https://wds-1.com ## Features * Swap drum samples * Choose drum samples from file * Pattern selector to save up to 8 patterns per drum kit * BPM and swing control * Sample hit buttons * Gain and pan * Reverb * Mute and solo * Pitch shift * Preset system for saving and loading drum kits * Works offline with service worker and caching * Installable as PWA * Drag to reorder channels ## Circle CI status [![CircleCI](https://circleci.com/gh/stufreen/web-drum-sequencer.svg?style=svg)](https://circleci.com/gh/stufreen/web-drum-sequencer) ## Installation To run a local development server: ``` npm install npm run start ``` To build a production version: `npm run build` ## Tests ``` npm run test ``` ## Thank You * [React-Select](https://github.com/JedWatson/react-select) * [Webaudio-Controls](https://github.com/g200kg/webaudio-controls) * Chris Wilson's article [here](https://www.html5rocks.com/en/tutorials/audio/scheduling/) * [Voxengo impluse response](https://www.voxengo.com/impulses/) * [Jost* typeface](https://github.com/indestructible-type/Jost) * [Draggable](https://shopify.github.io/draggable/) ================================================ FILE: __mocks__/fileMock.js ================================================ module.exports = 'test-file-stub'; ================================================ FILE: __mocks__/styleMock.js ================================================ module.exports = {}; ================================================ FILE: babel.config.js ================================================ const presets = [ [ "@babel/env", { targets: "> 0.25%, not dead", useBuiltIns: "usage", }, ], "@babel/preset-react", ]; module.exports = { presets }; ================================================ FILE: index.html ================================================ WDS-1: Web Drum Sequencer
================================================ FILE: manifest.json ================================================ { "icons": [ { "src": "/assets/icons/icon-48x48.png", "sizes": "48x48", "type": "image/png", "purpose": "maskable any" }, { "src": "/assets/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png", "purpose": "maskable any" }, { "src": "/assets/icons/icon-96x96.png", "sizes": "96x96", "type": "image/png", "purpose": "maskable any" }, { "src": "/assets/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png", "purpose": "maskable any" }, { "src": "/assets/icons/icon-144x144.png", "sizes": "144x144", "type": "image/png", "purpose": "maskable any" }, { "src": "/assets/icons/icon-152x152.png", "sizes": "152x152", "type": "image/png", "purpose": "maskable any" }, { "src": "/assets/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable any" }, { "src": "/assets/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png", "purpose": "maskable any" }, { "src": "/assets/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable any" } ], "name": "WDS-1: Wed Drum Sequencer", "short_name": "WDS-1", "orientation": "portrait", "display": "standalone", "start_url": "/", "background_color": "#202429", "theme_color": "#000000" } ================================================ FILE: package.json ================================================ { "name": "web-drum-sequencer", "version": "0.2.4", "description": "A drum machine and sequencer built with the Web Audio API and React.", "main": "dist/index.html", "scripts": { "start": "vite --host", "build": "rm -rf dist && vite build", "lint": "eslint src", "test": "jest", "gh-pages": "git subtree push --prefix dist origin gh-pages" }, "author": "Stu Freen (http://www.stufreen.com)", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/stufreen/web-drum-sequencer.git" }, "keywords": [ "Web Audio", "Web Audio API", "Drum Machine", "Playback", "Effect", "Instrument", "React", "Redux", "Interactive Music" ], "dependencies": { "@shopify/draggable": "^1.0.0-beta.8", "animol": "1.0.9", "prop-types": "^15.6.2", "ramda": "^0.25.0", "react": "^16.4.1", "react-dom": "^16.4.1", "react-redux": "^5.0.7", "react-select": "^2.0.0", "recompose": "^0.27.1", "redux": "^4.0.0", "redux-persist": "^5.10.0", "redux-thunk": "^2.3.0", "reselect": "^3.0.1", "serviceworker-webpack-plugin": "^1.0.1", "styled-components": "^3.3.3", "styled-system": "^3.0.2", "uuid": "^3.3.2" }, "jest": { "testURL": "http://localhost", "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", "\\.(css|less)$": "/__mocks__/styleMock.js" } }, "devDependencies": { "@babel/core": "^7.22.9", "@babel/eslint-parser": "^7.22.9", "@babel/preset-env": "^7.22.9", "@babel/preset-react": "^7.22.5", "@vitejs/plugin-react-refresh": "^1.3.6", "eslint": "^8.46.0", "eslint-config-airbnb": "^19.0.4", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.1", "eslint-plugin-react-hooks": "^4.6.0", "jest": "^29.6.2", "vite": "^4.4.7" } } ================================================ FILE: public/assets/icons/browserconfig.xml ================================================ #ffffff ================================================ FILE: public/sw.js ================================================ /* eslint no-restricted-globals: 1 */ const addToCache = (event, fetchResponse) => { // Check if we received a valid response if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') { return fetchResponse; } // IMPORTANT: Clone the response. A response is a stream // and because we want the browser to consume the response // as well as the cache consuming the response, we need // to clone it so we have two streams. const responseToCache = fetchResponse.clone(); caches.open('wdsCache').then((cache) => { cache.put(event.request, responseToCache); }); return fetchResponse; }; const fetchAndCache = (event, fetchRequest) => fetch(fetchRequest) .then(fetchResponse => addToCache(event, fetchResponse)); self.addEventListener('fetch', (event) => { // IMPORTANT: Clone the request. A request is a stream and // can only be consumed once. Since we are consuming this // once by cache and once by the browser for fetch, we need // to clone the response. const fetchRequest = event.request.clone(); // Try to get files from the "assets" directory from cache first if (fetchRequest.url.indexOf('/assets/') >= 0) { event.respondWith( caches.match(fetchRequest) .then(response => response || fetchAndCache(event, fetchRequest)), ); } else { event.respondWith( fetchAndCache(event, fetchRequest), ); } }); ================================================ FILE: src/__mocks__/samples.config.js ================================================ export default [ { name: 'Fake sample A', url: '/fake/sample/a/url.wav', }, { name: 'Fake sample B', url: '/fake/sample/b/url.wav', }, ]; ================================================ FILE: src/assets/js/webaudio-controls.js ================================================ /* * * * WebAudio-Controls is based on * webaudio-knob by Eiji Kitamura http://google.com/+agektmr * webaudio-slider by RYoya Kawai https://plus.google.com/108242669191458983485/posts * webaudio-switch by Keisuke Ai http://d.hatena.ne.jp/aike/ * Integrated and enhanced by g200kg http://www.g200kg.com/ * * Copyright 2013 Eiji Kitamura / Ryoya KAWAI / Keisuke Ai / g200kg(Tatsuya Shinyagaito) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * */ if(window.customElements){ let styles=document.createElement("style"); styles.innerHTML= `#webaudioctrl-context-menu { display: none; position: absolute; z-index: 10; padding: 0; width: 100px; color:#eee; background-color: #268; border: solid 1px #888; box-shadow: 1px 1px 2px #888; font-family: sans-serif; font-size: 11px; line-height:1.7em; text-align:center; cursor:pointer; color:#fff; list-style: none; } #webaudioctrl-context-menu.active { display: block; } .webaudioctrl-context-menu__item { display: block; margin: 0; padding: 0; color: #000; background-color:#eee; text-decoration: none; } .webaudioctrl-context-menu__title{ font-weight:bold; } .webaudioctrl-context-menu__item:last-child { margin-bottom: 0; } .webaudioctrl-context-menu__item:hover { background-color: #b8b8b8; } `; document.head.appendChild(styles); let midimenu=document.createElement("ul"); midimenu.id="webaudioctrl-context-menu"; midimenu.innerHTML= `
  • MIDI Learn
  • Learn
  • Clear
  • Close
  • `; let opt={ useMidi:0, midilearn:0, mididump:0, outline:0, knobSrc:null, knobSprites:0, knobWidth:0, knobHeight:0, knobDiameter:64, knobColors:"#e00;#000;#000", sliderSrc:null, sliderKnobsrc:null, sliderWidth:0, sliderHeight:0, sliderKnobwidth:0, sliderKnobheight:0, sliderDitchlength:0, sliderColors:"#e00;#000;#fcc", switchWidth:0, switchHeight:0, switchDiameter:24, switchColors:"#e00;#000;#fcc", paramWidth:32, paramHeight:16, paramColors:"#fff;#000", xypadColors:"#e00;#000;#fcc", }; if(window.WebAudioControlsOptions) Object.assign(opt,window.WebAudioControlsOptions); class WebAudioControlsWidget extends HTMLElement{ constructor(){ super(); this.addEventListener("keydown",this.keydown); this.addEventListener("mousedown",this.pointerdown,{passive:false}); this.addEventListener("touchstart",this.pointerdown,{passive:false}); this.addEventListener("wheel",this.wheel,{passive:false}); this.addEventListener("mouseover",this.pointerover); this.addEventListener("mouseout",this.pointerout); this.addEventListener("contextmenu",this.contextMenu); this.hover=this.drag=0; document.body.appendChild(midimenu); this.basestyle=` .webaudioctrl-tooltip{ display:inline-block; position:absolute; margin:0 -1000px; z-index: 999; background:#eee; color:#000; border:1px solid #666; border-radius:4px; padding:5px 10px; text-align:center; left:0; top:0; font-size:11px; opacity:0; visibility:hidden; } .webaudioctrl-tooltip:before{ content: ""; position: absolute; top: 100%; left: 50%; margin-left: -8px; border: 8px solid transparent; border-top: 8px solid #666; } .webaudioctrl-tooltip:after{ content: ""; position: absolute; top: 100%; left: 50%; margin-left: -6px; border: 6px solid transparent; border-top: 6px solid #eee; } `; } sendEvent(ev){ let event; event=document.createEvent("HTMLEvents"); event.initEvent(ev,false,true); this.dispatchEvent(event); } getAttr(n,def){ let v=this.getAttribute(n); if(v==""||v==null) return def; switch(typeof(def)){ case "number": if(v=="true") return 1; v=+v; if(isNaN(v)) return 0; return v; } return v; } showtip(d){ function valstr(x,c,type){ switch(type){ case "x": return (x|0).toString(16); case "X": return (x|0).toString(16).toUpperCase(); case "d": return (x|0).toString(); case "f": return x.toFixed(c); case "s": return x.toString(); } return ""; } function numformat(s,x){ if(typeof(x)=="undefined") return; let i=s.indexOf("%"); let c=[0,0],type=0,m=0,r="",j=i+1; for(;j=0){ type=s[j]; break; } if(s[j]==".") m=1; else c[m]=c[m]*10+parseInt(s[j]); } if(typeof(x)=="number") r=valstr(x,c[1],type); else r=valstr(x.x,c[1],type)+","+valstr(x.y,c[1],type); if(c[0]>0) r=(" "+r).slice(-c[0]); r=s.replace(/%.*[xXdfs]/,r); return r; } let s=this.tooltip; if(this.drag||this.hover){ if(this.valuetip){ if(s==null) s=`%.${this.digits}f`; else if(s.indexOf("%")<0) s+=` : %.${this.digits}f`; } if(s){ this.ttframe.innerHTML=numformat(s,this.convValue); this.ttframe.style.display="inline-block"; this.ttframe.style.width="auto"; this.ttframe.style.height="auto"; this.ttframe.style.transition="opacity 0.5s "+d+"s,visibility 0.5s "+d+"s"; this.ttframe.style.opacity=0.9; this.ttframe.style.visibility="visible"; let rc=this.getBoundingClientRect(),rc2=this.ttframe.getBoundingClientRect(),rc3=document.documentElement.getBoundingClientRect(); this.ttframe.style.left=((rc.width-rc2.width)*0.5+1000)+"px"; this.ttframe.style.top=(-rc2.height-8)+"px"; return; } } this.ttframe.style.transition="opacity 0.1s "+d+"s,visibility 0.1s "+d+"s"; this.ttframe.style.opacity=0; this.ttframe.style.visibility="hidden"; } pointerover(e) { this.hover=1; this.showtip(0.6); } pointerout(e) { this.hover=0; this.showtip(0); } contextMenu(e){ if(window.webAudioControlsMidiManager && this.midilearn) webAudioControlsMidiManager.contextMenuOpen(e,this); e.preventDefault(); e.stopPropagation(); } setMidiController(channel, cc) { if (this.listeningToThisMidiController(channel, cc)) return; this.midiController={ 'channel': channel, 'cc': cc}; console.log("Added mapping for channel=" + channel + " cc=" + cc + " tooltip=" + this.tooltip); } listeningToThisMidiController(channel, cc) { const c = this.midiController; if((c.channel === channel || c.channel < 0) && c.cc === cc) return true; return false; } processMidiEvent(event){ const channel = event.data[0] & 0xf; const controlNumber = event.data[1]; if(this.midiMode == 'learn') { this.setMidiController(channel, controlNumber); webAudioControlsMidiManager.contextMenuClose(); this.midiMode = 'normal'; } if(this.listeningToThisMidiController(channel, controlNumber)) { if(this.tagName=="WEBAUDIO-SWITCH"){ switch(this.type){ case "toggle": if(event.data[2]>=64) this.setValue(1-this.value,true); break; case "kick": this.setValue(event.data[2]>=64?1:0); break; case "radio": let els=document.querySelectorAll("webaudio-switch[type='radio'][group='"+this.group+"']"); for(let i=0;i ${this.basestyle} webaudio-knob{ display:inline-block; position:relative; margin:0; padding:0; cursor:pointer; font-family: sans-serif; font-size: 11px; } .webaudio-knob-body{ display:inline-block; position:relative; margin:0; padding:0; vertical-align:bottom; }
    `; this.elem=root.childNodes[2]; this.ttframe=root.childNodes[3]; this.enable=this.getAttr("enable",1); this._src=this.getAttr("src",opt.knobSrc); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); this.defvalue=this.getAttr("defvalue",0); this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=+v;this.redraw()}}); this._max=this.getAttr("max",100); Object.defineProperty(this,"max",{get:()=>{return this._max},set:(v)=>{this._max=+v;this.redraw()}}); this._step=this.getAttr("step",1); Object.defineProperty(this,"step",{get:()=>{return this._step},set:(v)=>{this._step=+v;this.redraw()}}); this._sprites=this.getAttr("sprites",opt.knobSprites); Object.defineProperty(this,"sprites",{get:()=>{return this._sprites},set:(v)=>{this._sprites=v;this.setupImage()}}); this._width=this.getAttr("width",opt.knobWidth); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); this._height=this.getAttr("height",opt.knobHeight); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); this._diameter=this.getAttr("diameter",opt.knobDiameter); Object.defineProperty(this,"diameter",{get:()=>{return this._diameter},set:(v)=>{this._diameter=v;this.setupImage()}}); this._colors=this.getAttr("colors",opt.knobColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); this.outline=this.getAttr("outline",opt.outline); this.sensitivity=this.getAttr("sensitivity",1); this.valuetip=this.getAttr("valuetip",1); this.tooltip=this.getAttr("tooltip",null); this.conv=this.getAttr("conv",null); if(this.conv) this.convValue=eval(this.conv)(this._value); else this.convValue=this._value; this.midilearn=this.getAttr("midilearn",opt.midilearn); this.midicc=this.getAttr("midicc",null); this.midiController={}; this.midiMode="normal"; if(this.midicc) { let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); this.setMidiController(ch, cc); } this.setupImage(); this.digits=0; this.coltab=["#e00","#000","#000"]; if(window.webAudioControlsMidiManager) // window.webAudioControlsMidiManager.updateWidgets(); window.webAudioControlsMidiManager.addWidget(this); } disconnectedCallback(){} setupImage(){ this.kw=this.width||this.diameter; this.kh=this.height||this.diameter; if(!this.src){ if(this.colors) this.coltab = this.colors.split(";"); if(!this.coltab) this.coltab=["#e00","#000","#000"]; let svg= ` `; for(let i=0;i<101;++i){ svg += ``; svg += ``; } svg += ""; this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svg)+")"; // this.elem.style.backgroundSize = "100% 10100%"; this.elem.style.backgroundSize = `${this.kw}px ${this.kh*101}px`; } else{ this.elem.style.backgroundImage = "url("+(this.src)+")"; if(!this.sprites) this.elem.style.backgroundSize = "100% 100%"; else{ // this.elem.style.backgroundSize = `100% ${(this.sprites+1)*100}%`; this.elem.style.backgroundSize = `${this.kw}px ${this.kh*(this.sprites+1)}px`; } } this.elem.style.outline=this.outline?"":"none"; this.elem.style.width=this.kw+"px"; this.elem.style.height=this.kh+"px"; this.style.height=this.kh+"px"; this.redraw(); } redraw() { this.digits=0; if(this.step && this.step < 1) { for(let n = this.step ; n < 1; n *= 10) ++this.digits; } if(this.valuethis.max){ this.value=this.max; return; } let range = this.max - this.min; let style = this.elem.style; let sp = this.src?this.sprites:100; if(sp>=1){ let offset = ((sp * (this.value - this.min) / range) | 0); style.backgroundPosition = "0px " + (-offset*this.kh) + "px"; style.transform = 'rotate(0deg)'; } else { let deg = 270 * ((this.value - this.min) / range - 0.5); style.backgroundPosition="0px 0px"; style.transform = 'rotate(' + deg + 'deg)'; } } _setValue(v){ if(this.step) v=(Math.round((v-this.min)/this.step))*this.step+this.min; this._value=Math.min(this.max,Math.max(this.min,v)); if(this._value!=this.oldvalue){ this.oldvalue=this._value; if(this.conv) this.convValue=eval(this.conv)(this._value); else this.convValue=this._value; this.redraw(); this.showtip(0); return 1; } return 0; } setValue(v,f){ if(this._setValue(v) && f) this.sendEvent("input"),this.sendEvent("change"); } wheel(e) { if (!this.enable) return; let delta=(this.max-this.min)*0.01; delta=e.deltaY>0?-delta:delta; if(!e.shiftKey) delta*=5; if(Math.abs(delta) < this.step) delta = (delta > 0) ? +this.step : -this.step; this.setValue(+this.value+delta,true); e.preventDefault(); e.stopPropagation(); } pointerdown(ev){ if(!this.enable) return; let e=ev; if(ev.touches){ e = ev.changedTouches[0]; this.identifier=e.identifier; } else { if(e.buttons!=1 && e.button!=0) return; } this.elem.focus(); this.drag=1; this.showtip(0); let pointermove=(ev)=>{ let e=ev; if(ev.touches){ for(let i=0;i{ let e=ev; if(ev.touches){ for(let i=0;;){ if(ev.changedTouches[i].identifier==this.identifier){ break; } if(++i>=ev.changedTouches.length) return; } } this.drag=0; this.showtip(0); this.startPosX = this.startPosY = null; window.removeEventListener('mousemove', pointermove); window.removeEventListener('touchmove', pointermove, {passive:false}); window.removeEventListener('mouseup', pointerup); window.removeEventListener('touchend', pointerup); window.removeEventListener('touchcancel', pointerup); document.body.removeEventListener('touchstart', preventScroll,{passive:false}); this.sendEvent("change"); } let preventScroll=(e)=>{ e.preventDefault(); } if(e.ctrlKey || e.metaKey) this.setValue(this.defvalue,true); else { this.startPosX = e.pageX; this.startPosY = e.pageY; this.startVal = this.value; window.addEventListener('mousemove', pointermove); window.addEventListener('touchmove', pointermove, {passive:false}); } window.addEventListener('mouseup', pointerup); window.addEventListener('touchend', pointerup); window.addEventListener('touchcancel', pointerup); document.body.addEventListener('touchstart', preventScroll,{passive:false}); ev.preventDefault(); ev.stopPropagation(); return false; } }); } catch(error){ console.log("webaudio-knob already defined"); } try{ customElements.define("webaudio-slider", class WebAudioSlider extends WebAudioControlsWidget { constructor(){ super(); } connectedCallback(){ let root; // if(this.attachShadow) // root=this.attachShadow({mode: 'open'}); // else root=this; root.innerHTML= `
    `; this.elem=root.childNodes[2]; this.knob=this.elem.childNodes[0]; this.ttframe=root.childNodes[3]; this.enable=this.getAttr("enable",1); this._src=this.getAttr("src",opt.sliderSrc); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); this._knobsrc=this.getAttr("knobsrc",opt.sliderKnobsrc); Object.defineProperty(this,"knobsrc",{get:()=>{return this._knobsrc},set:(v)=>{this._knobsrc=v;this.setupImage()}}); this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); this.defvalue=this.getAttr("defvalue",0); this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=v;this.redraw()}}); this._max=this.getAttr("max",100); Object.defineProperty(this,"max",{get:()=>{return this._max},set:(v)=>{this._max=v;this.redraw()}}); this._step=this.getAttr("step",1); Object.defineProperty(this,"step",{get:()=>{return this._step},set:(v)=>{this._step=v;this.redraw()}}); this._sprites=this.getAttr("sprites",0); Object.defineProperty(this,"sprites",{get:()=>{return this._sprites},set:(v)=>{this._sprites=v;this.setupImage()}}); this._direction=this.getAttr("direction",null); Object.defineProperty(this,"direction",{get:()=>{return this._direction},set:(v)=>{this._direction=v;this.setupImage()}}); this._width=this.getAttr("width",opt.sliderWidth); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); this._height=this.getAttr("height",opt.sliderHeight); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); if(this._direction=="horz"){ if(this._width==0) this._width=128; if(this._height==0) this._height=24; } else{ if(this._width==0) this._width=24; if(this._height==0) this._height=128; } this._knobwidth=this.getAttr("knobwidth",opt.sliderKnobwidth); Object.defineProperty(this,"knobwidth",{get:()=>{return this._knobwidth},set:(v)=>{this._knobwidth=v;this.setupImage()}}); this._knobheight=this.getAttr("knbheight",opt.sliderKnobheight); Object.defineProperty(this,"knobheight",{get:()=>{return this._knobheight},set:(v)=>{this._knobheight=v;this.setupImage()}}); this._ditchlength=this.getAttr("ditchlength",opt.sliderDitchlength); Object.defineProperty(this,"ditchlength",{get:()=>{return this._ditchlength},set:(v)=>{this._ditchlength=v;this.setupImage()}}); this._colors=this.getAttr("colors",opt.sliderColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); this.outline=this.getAttr("outline",opt.outline); this.sensitivity=this.getAttr("sensitivity",1); this.valuetip=this.getAttr("valuetip",1); this.tooltip=this.getAttr("tooltip",null); this.conv=this.getAttr("conv",null); if(this.conv) this.convValue=eval(this.conv)(this._value); else this.convValue=this._value; this.midilearn=this.getAttr("midilearn",opt.midilearn); this.midicc=this.getAttr("midicc",null); this.midiController={}; this.midiMode="normal"; if(this.midicc) { let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); this.setMidiController(ch, cc); } this.setupImage(); this.digits=0; if(window.webAudioControlsMidiManager) // window.webAudioControlsMidiManager.updateWidgets(); window.webAudioControlsMidiManager.addWidget(this); this.elem.onclick=(e)=>{e.stopPropagation()}; } disconnectedCallback(){} setupImage(){ this.coltab = this.colors.split(";"); this.dr=this.direction; this.dlen=this.ditchlength; if(!this.width){ if(this.dr=="horz") this.width=128; else this.width=24; } if(!this.height){ if(this.dr=="horz") this.height=24; else this.height=128; } if(!this.dr) this.dr=(this.width<=this.height)?"vert":"horz"; if(this.dr=="vert"){ if(!this.dlen) this.dlen=this.height-this.width; } else{ if(!this.dlen) this.dlen=this.width-this.height; } this.knob.style.backgroundSize = "100% 100%"; this.elem.style.backgroundSize = "100% 100%"; this.elem.style.width=this.width+"px"; this.elem.style.height=this.height+"px"; this.style.height=this.height+"px"; this.kwidth=this.knobwidth||(this.dr=="horz"?this.height:this.width); this.kheight=this.knobheight||(this.dr=="horz"?this.height:this.width); this.knob.style.width = this.kwidth+"px"; this.knob.style.height = this.kheight+"px"; if(!this.src){ let r=Math.min(this.width,this.height)*0.5; let svgbody= ` `; this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgbody)+")"; } else{ this.elem.style.backgroundImage = "url("+(this.src)+")"; } if(!this.knobsrc){ let svgthumb= ` `; this.knob.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgthumb)+")"; } else{ this.knob.style.backgroundImage = "url("+(this.knobsrc)+")"; } this.elem.style.outline=this.outline?"":"none"; this.redraw(); } redraw() { this.digits=0; if(this.step && this.step < 1) { for(let n = this.step ; n < 1; n *= 10) ++this.digits; } if(this.valuethis.max){ this.value=this.max; return; } let range = this.max - this.min; let style = this.knob.style; if(this.dr=="vert"){ style.left=(this.width-this.kwidth)*0.5+"px"; style.top=(1-(this.value-this.min)/range)*this.dlen+"px"; this.sensex=0; this.sensey=1; } else{ style.top=(this.height-this.kheight)*0.5+"px"; style.left=(this.value-this.min)/range*this.dlen+"px"; this.sensex=1; this.sensey=0; } } _setValue(v){ v=(Math.round((v-this.min)/this.step))*this.step+this.min; this._value=Math.min(this.max,Math.max(this.min,v)); if(this._value!=this.oldvalue){ this.oldvalue=this._value; if(this.conv) this.convValue=eval(this.conv)(this._value); else this.convValue=this._value; this.redraw(); this.showtip(0); return 1; } return 0; } setValue(v,f){ if(this._setValue(v)&&f) this.sendEvent("input"),this.sendEvent("change"); } wheel(e) { let delta=(this.max-this.min)*0.01; delta=e.deltaY>0?-delta:delta; if(!e.shiftKey) delta*=5; if(Math.abs(delta) < this.step) delta = (delta > 0) ? +this.step : -this.step; this.setValue(+this.value+delta,true); e.preventDefault(); e.stopPropagation(); this.redraw(); } pointerdown(ev){ if(!this.enable) return; let e=ev; if(ev.touches){ e = ev.changedTouches[0]; this.identifier=e.identifier; } else { if(e.buttons!=1 && e.button!=0) return; } this.elem.focus(); this.drag=1; this.showtip(0); let pointermove=(ev)=>{ let e=ev; if(ev.touches){ for(let i=0;i{ let e=ev; if(ev.touches){ for(let i=0;;){ if(ev.changedTouches[i].identifier==this.identifier){ break; } if(++i>=ev.changedTouches.length) return; } } this.drag=0; this.showtip(0); this.startPosX = this.startPosY = null; window.removeEventListener('mousemove', pointermove); window.removeEventListener('touchmove', pointermove, {passive:false}); window.removeEventListener('mouseup', pointerup); window.removeEventListener('touchend', pointerup); window.removeEventListener('touchcancel', pointerup); document.body.removeEventListener('touchstart', preventScroll,{passive:false}); this.sendEvent("change"); } let preventScroll=(e)=>{ e.preventDefault(); } if(e.touches) e = e.touches[0]; if(e.ctrlKey || e.metaKey) this.setValue(this.defvalue,true); else { this.startPosX = e.pageX; this.startPosY = e.pageY; this.startVal = this.value; window.addEventListener('mousemove', pointermove); window.addEventListener('touchmove', pointermove, {passive:false}); } window.addEventListener('mouseup', pointerup); window.addEventListener('touchend', pointerup); window.addEventListener('touchcancel', pointerup); document.body.addEventListener('touchstart', preventScroll,{passive:false}); e.preventDefault(); e.stopPropagation(); return false; } }); } catch(error){ console.log("webaudio-slider already defined"); } try{ customElements.define("webaudio-switch", class WebAudioSwitch extends WebAudioControlsWidget { constructor(){ super(); } connectedCallback(){ let root; // if(this.attachShadow) // root=this.attachShadow({mode: 'open'}); // else root=this; root.innerHTML= `
    `; this.elem=root.childNodes[2]; this.ttframe=this.elem.childNodes[0]; this.enable=this.getAttr("enable",1); this._src=this.getAttr("src",null); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); this.defvalue=this.getAttr("defvalue",0); this.type=this.getAttr("type","toggle"); this.group=this.getAttr("group",""); this._width=this.getAttr("width",0); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); this._height=this.getAttr("height",0); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); this._diameter=this.getAttr("diameter",0); Object.defineProperty(this,"diameter",{get:()=>{return this._diameter},set:(v)=>{this._diameter=v;this.setupImage()}}); this.invert=this.getAttr("invert",0); this._colors=this.getAttr("colors",opt.switchColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); this.outline=this.getAttr("outline",opt.outline); this.valuetip=0; this.tooltip=this.getAttr("tooltip",null); this.midilearn=this.getAttr("midilearn",opt.midilearn); this.midicc=this.getAttr("midicc",null); this.midiController={}; this.midiMode="normal"; if(this.midicc) { let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); this.setMidiController(ch, cc); } this.setupImage(); this.digits=0; if(window.webAudioControlsMidiManager) // window.webAudioControlsMidiManager.updateWidgets(); window.webAudioControlsMidiManager.addWidget(this); this.elem.onclick=(e)=>{e.stopPropagation()}; } disconnectedCallback(){} setupImage(){ let w=this.width||this.diameter||opt.switchWidth||opt.switchDiameter; let h=this.height||this.diameter||opt.switchHeight||opt.switchDiameter; if(!this.src){ this.coltab = this.colors.split(";"); let mm=Math.min(w,h); let svg= ` `; this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svg)+")"; this.elem.style.backgroundSize = "100% 200%"; } else{ this.elem.style.backgroundImage = "url("+(this.src)+")"; if(!this.sprites) this.elem.style.backgroundSize = "100% 200%"; else this.elem.style.backgroundSize = `100% ${(this.sprites+1)*100}%`; } this.elem.style.width=w+"px"; this.elem.style.height=h+"px"; this.style.height=h+"px"; this.elem.style.outline=this.outline?"":"none"; this.redraw(); } redraw() { let style = this.elem.style; if(this.value^this.invert) style.backgroundPosition = "0px -100%"; else style.backgroundPosition = "0px 0px"; } setValue(v,f){ this.value=v; this.checked=(!!v); if(this.value!=this.oldvalue){ this.redraw(); this.showtip(0); if(f){ this.sendEvent("input"); this.sendEvent("change"); } this.oldvalue=this.value; } } pointerdown(ev){ if(!this.enable) return; let e=ev; if(ev.touches){ e = ev.changedTouches[0]; this.identifier=e.identifier; } else { if(e.buttons!=1 && e.button!=0) return; } this.elem.focus(); this.drag=1; this.showtip(0); let pointermove=(e)=>{ e.preventDefault(); e.stopPropagation(); return false; } let pointerup=(e)=>{ this.drag=0; this.showtip(0); window.removeEventListener('mousemove', pointermove); window.removeEventListener('touchmove', pointermove, {passive:false}); window.removeEventListener('mouseup', pointerup); window.removeEventListener('touchend', pointerup); window.removeEventListener('touchcancel', pointerup); document.body.removeEventListener('touchstart', preventScroll,{passive:false}); if(this.type=="kick"){ this.value=0; this.checked=false; this.redraw(); this.sendEvent("change"); } this.sendEvent("click"); e.preventDefault(); e.stopPropagation(); } let preventScroll=(e)=>{ e.preventDefault(); } switch(this.type){ case "kick": this.setValue(1); this.sendEvent("change"); break; case "toggle": if(e.ctrlKey || e.metaKey) this.value=defvalue; else this.value=1-this.value; this.checked=!!this.value; this.sendEvent("change"); break; case "radio": let els=document.querySelectorAll("webaudio-switch[type='radio'][group='"+this.group+"']"); for(let i=0;i ${this.basestyle} webaudio-param{ display:inline-block; user-select:none; margin:0; padding:0; font-family: sans-serif; font-size: 8px; cursor:pointer; position:relative; vertical-align:baseline; } .webaudio-param-body{ display:inline-block; position:relative; text-align:center; border:1px solid #888; background:none; border-radius:4px; margin:0; padding:0; font-family:sans-serif; font-size:11px; vertical-align:bottom; }
    `; this.elem=root.childNodes[2]; this.ttframe=root.childNodes[3]; this.enable=this.getAttr("enable",1); this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); this.defvalue=this.getAttr("defvalue",0); this._fontsize=this.getAttr("fontsize",9); Object.defineProperty(this,"fontsize",{get:()=>{return this._fontsize},set:(v)=>{this._fontsize=v;this.setupImage()}}); this._src=this.getAttr("src",null); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); this.link=this.getAttr("link",""); this._width=this.getAttr("width",32); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); this._height=this.getAttr("height",20); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); this._colors=this.getAttr("colors","#fff;#000"); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); this.outline=this.getAttr("outline",opt.outline); this.midiController={}; this.midiMode="normal"; if(this.midicc) { let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); this.setMidiController(ch, cc); } this.setupImage(); if(window.webAudioControlsMidiManager) // window.webAudioControlsMidiManager.updateWidgets(); window.webAudioControlsMidiManager.addWidget(this); this.fromLink=((e)=>{ this.setValue(e.target.convValue.toFixed(e.target.digits)); }).bind(this); this.elem.onchange=()=>{ this.value=this.elem.value; let le=document.getElementById(this.link); if(le) le.setValue(+this.elem.value); } } disconnectedCallback(){} setupImage(){ this.coltab = this.colors.split(";"); this.elem.style.color=this.coltab[0]; if(!this.src){ this.elem.style.backgroundColor=this.coltab[1]; } else{ this.elem.style.backgroundImage = "url("+(this.src)+")"; this.elem.style.backgroundSize = "100% 100%"; } this.elem.style.width=this.width+"px"; this.elem.style.height=this.height+"px"; this.elem.style.fontSize=this.fontsize+"px"; this.elem.style.outline=this.outline?"":"none"; let l=document.getElementById(this.link); if(l&&typeof(l.value)!="undefined"){ this.setValue(l.value.toFixed(l.digits)); l.addEventListener("input",(e)=>{this.setValue(l.value.toFixed(l.digits))}); } this.redraw(); } redraw() { this.elem.value=this.value; } setValue(v,f){ this.value=v; if(this.value!=this.oldvalue){ this.redraw(); this.showtip(0); if(f){ let event=document.createEvent("HTMLEvents"); event.initEvent("change",false,true); this.dispatchEvent(event); } this.oldvalue=this.value; } } pointerdown(ev){ if(!this.enable) return; let e=ev; if(ev.touches) e = ev.touches[0]; else { if(e.buttons!=1 && e.button!=0) return; } this.elem.focus(); this.redraw(); } }); } catch(error){ console.log("webaudio-param already defined"); } try{ customElements.define("webaudio-keyboard", class WebAudioKeyboard extends WebAudioControlsWidget { constructor(){ super(); } connectedCallback(){ let root; // if(this.attachShadow) // root=this.attachShadow({mode: 'open'}); // else root=this; root.innerHTML= `
    `; this.cv=root.childNodes[2]; this.ttframe=root.childNodes[3]; this.ctx=this.cv.getContext("2d"); this._values=[]; this.enable=this.getAttr("enable",1); this._width=this.getAttr("width",480); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); this._height=this.getAttr("height",128); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=+v;this.redraw()}}); this._keys=this.getAttr("keys",25); Object.defineProperty(this,"keys",{get:()=>{return this._keys},set:(v)=>{this._keys=+v;this.setupImage()}}); this._colors=this.getAttr("colors","#222;#eee;#ccc;#333;#000;#e88;#c44;#c33;#800"); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); this.outline=this.getAttr("outline",opt.outline); this.midilearn=this.getAttr("midilearn",0); this.midicc=this.getAttr("midicc",null); this.press=0; this.keycodes1=[90,83,88,68,67,86,71,66,72,78,74,77,188,76,190,187,191,226]; this.keycodes2=[81,50,87,51,69,82,53,84,54,89,55,85,73,57,79,48,80,192,222,219]; this.addEventListener("keyup",this.keyup); this.midiController={}; this.midiMode="normal"; if(this.midicc) { let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); this.setMidiController(ch, cc); } this.setupImage(); this.digits=0; if(window.webAudioControlsMidiManager) window.webAudioControlsMidiManager.addWidget(this); } disconnectedCallback(){} setupImage(){ this.cv.style.width=this.width+"px"; this.cv.style.height=this.height+"px"; this.bheight = this.height * 0.55; this.kp=[0,7/12,1,3*7/12,2,3,6*7/12,4,8*7/12,5,10*7/12,6]; this.kf=[0,1,0,1,0,0,1,0,1,0,1,0]; this.ko=[0,0,(7*2)/12-1,0,(7*4)/12-2,(7*5)/12-3,0,(7*7)/12-4,0,(7*9)/12-5,0,(7*11)/12-6]; this.kn=[0,2,4,5,7,9,11]; this.coltab=this.colors.split(";"); this.cv.width = this.width; this.cv.height = this.height; this.cv.style.width = this.width+'px'; this.cv.style.height = this.height+'px'; this.style.height = this.height+'px'; this.cv.style.outline=this.outline?"":"none"; this.bheight = this.height * 0.55; this.max=this.min+this.keys-1; this.dispvalues=[]; this.valuesold=[]; if(this.kf[this.min%12]) --this.min; if(this.kf[this.max%12]) ++this.max; this.redraw(); } redraw(){ function rrect(ctx, x, y, w, h, r, c1, c2) { if(c2) { let g=ctx.createLinearGradient(x,y,x+w,y); g.addColorStop(0,c1); g.addColorStop(1,c2); ctx.fillStyle=g; } else ctx.fillStyle=c1; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x+w, y); ctx.lineTo(x+w, y+h-r); ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h); ctx.lineTo(x+r, y+h); ctx.quadraticCurveTo(x, y+h, x, y+h-r); ctx.lineTo(x, y); ctx.fill(); } this.ctx.fillStyle = this.coltab[0]; this.ctx.fillRect(0,0,this.width,this.height); let x0=7*((this.min/12)|0)+this.kp[this.min%12]; let x1=7*((this.max/12)|0)+this.kp[this.max%12]; let n=x1-x0; this.wwidth=(this.width-1)/(n+1); this.bwidth=this.wwidth*7/12; let h2=this.bheight; let r=Math.min(8,this.wwidth*0.2); for(let i=this.min,j=0;i<=this.max;++i) { if(this.kf[i%12]==0) { let x=this.wwidth*(j++)+1; if(this.dispvalues.indexOf(i)>=0) rrect(this.ctx,x,1,this.wwidth-1,this.height-2,r,this.coltab[5],this.coltab[6]); else rrect(this.ctx,x,1,this.wwidth-1,this.height-2,r,this.coltab[1],this.coltab[2]); } } r=Math.min(8,this.bwidth*0.3); for(let i=this.min;i=0) rrect(this.ctx,x,1,this.bwidth,h2,r,this.coltab[7],this.coltab[8]); else rrect(this.ctx,x,1,this.bwidth,h2,r,this.coltab[3],this.coltab[4]); this.ctx.strokeStyle=this.coltab[0]; this.ctx.stroke(); } } } _setValue(v){ if(this.step) v=(Math.round((v-this.min)/this.step))*this.step+this.min; this._value=Math.min(this.max,Math.max(this.min,v)); if(this._value!=this.oldvalue){ this.oldvalue=this._value; this.redraw(); this.showtip(0); return 1; } return 0; } setValue(v,f){ if(this._setValue(v) && f) this.sendEvent("input"),this.sendEvent("change"); } wheel(e){} keydown(e){ let m=Math.floor((this.min+11)/12)*12; let k=this.keycodes1.indexOf(e.keyCode); if(k<0) { k=this.keycodes2.indexOf(e.keyCode); if(k>=0) k+=12; } if(k>=0){ k+=m; if(this.currentKey!=k){ this.currentKey=k; this.sendEventFromKey(1,k); this.setNote(1,k); } } } keyup(e){ let m=Math.floor((this.min+11)/12)*12; let k=this.keycodes1.indexOf(e.keyCode); if(k<0) { k=this.keycodes2.indexOf(e.keyCode); if(k>=0) k+=12; } if(k>=0){ k+=m; this.currentKey=-1; this.sendEventFromKey(0,k); this.setNote(0,k); } } pointerdown(ev){ this.cv.focus(); if(this.enable) { ++this.press; } let pointermove=(ev)=>{ if(!this.enable) return; let r=this.getBoundingClientRect(); let v=[],p; if(ev.touches) p=ev.targetTouches; else if(this.press) p=[ev]; else p=[]; if(p.length>0) this.drag=1; for(let i=0;i=0&&py=this.min&&k<=this.max) v.push(k); } } v.sort(); this.values=v; this.sendevent(); this.redraw(); } let pointerup=(ev)=>{ if(this.enable) { if(ev.touches) this.press=ev.touches.length; else this.press=0; pointermove(ev); this.sendevent(); if(this.press==0){ window.removeEventListener('mousemove', pointermove); window.removeEventListener('touchmove', pointermove, {passive:false}); window.removeEventListener('mouseup', pointerup); window.removeEventListener('touchend', pointerup); window.removeEventListener('touchcancel', pointerup); document.body.removeEventListener('touchstart', preventScroll,{passive:false}); } this.redraw(); } this.drag=0; ev.preventDefault(); } let preventScroll=(ev)=>{ ev.preventDefault(); } window.addEventListener('mousemove', pointermove); window.addEventListener('touchmove', pointermove, {passive:false}); window.addEventListener('mouseup', pointerup); window.addEventListener('touchend', pointerup); window.addEventListener('touchcancel', pointerup); document.body.addEventListener('touchstart', preventScroll,{passive:false}); pointermove(ev); ev.preventDefault(); ev.stopPropagation(); } sendEventFromKey(s,k){ let ev=document.createEvent('HTMLEvents'); ev.initEvent('change',true,true); ev.note=[s,k]; this.dispatchEvent(ev); } sendevent(){ let notes=[]; for(let i=0,j=this.valuesold.length;i=0) this.dispvalues.splice(n,1); } } setNote(state,note) { this.setdispvalues(state,note); this.redraw(); } }); } catch(error){ console.log("webaudio-keyboard already defined"); } try{ customElements.define("webaudio-xypad", class WebAudioXYPad extends WebAudioControlsWidget { constructor(){ super(); } connectedCallback(){ let root; // if(this.attachShadow) // root=this.attachShadow({mode: 'open'}); // else root=this; root.innerHTML= `
    `; this.elem=root.childNodes[2]; this.knob=this.elem.childNodes[0]; this.ttframe=root.childNodes[3]; this.enable=this.getAttr("enable",1); this._src=this.getAttr("src",opt.sliderSrc); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); this._knobsrc=this.getAttr("knobsrc",opt.sliderKnobsrc); Object.defineProperty(this,"knobsrc",{get:()=>{return this._knobsrc},set:(v)=>{this._knobsrc=v;this.setupImage()}}); this._x=this.getAttr("x",50); Object.defineProperty(this,"x",{get:()=>{return this._x},set:(v)=>{this._x=v;this.redraw()}}); this._y=this.getAttr("y",50); Object.defineProperty(this,"y",{get:()=>{return this._y},set:(v)=>{this._y=v;this.redraw()}}); this.defx=this.getAttr("defx",50); this.defy=this.getAttr("defy",50); this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=v;this.redraw()}}); this._max=this.getAttr("max",100); Object.defineProperty(this,"max",{get:()=>{return this._max},set:(v)=>{this._max=v;this.redraw()}}); this._step=this.getAttr("step",1); Object.defineProperty(this,"step",{get:()=>{return this._step},set:(v)=>{this._step=v;this.redraw()}}); this._sprites=this.getAttr("sprites",0); Object.defineProperty(this,"sprites",{get:()=>{return this._sprites},set:(v)=>{this._sprites=v;this.setupImage()}}); this._width=this.getAttr("width",128); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); this._height=this.getAttr("height",128); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); this._knobwidth=this.getAttr("knobwidth",28); Object.defineProperty(this,"knobwidth",{get:()=>{return this._knobwidth},set:(v)=>{this._knobwidth=v;this.setupImage()}}); this._knobheight=this.getAttr("knbheight",28); Object.defineProperty(this,"knobheight",{get:()=>{return this._knobheight},set:(v)=>{this._knobheight=v;this.setupImage()}}); this._colors=this.getAttr("colors",opt.sliderColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); this.outline=this.getAttr("outline",opt.outline); this.valuetip=this.getAttr("valuetip",1); this.tooltip=this.getAttr("tooltip",null); this.conv=this.getAttr("conv",null); if(this.conv){ this.convValue={x:eval(this.conv)(this._x),y:eval(this.conv)(this._y)}; } else this.convValue={x:this._x,y:this._y}; this.midilearn=this.getAttr("midilearn",opt.midilearn); this.midicc=this.getAttr("midicc",null); this.midiController={}; this.midiMode="normal"; if(this.midicc) { let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); this.setMidiController(ch, cc); } this.setupImage(); this.digits=0; if(window.webAudioControlsMidiManager) // window.webAudioControlsMidiManager.updateWidgets(); window.webAudioControlsMidiManager.addWidget(this); this.elem.onclick=(e)=>{e.stopPropagation()}; } disconnectedCallback(){} setupImage(){ this.coltab = this.colors.split(";"); this.dr=this.direction; this.dlen=this.ditchlength; if(!this.width) this.width=256; if(!this.height) this.height=256; this.knob.style.backgroundSize = "100% 100%"; this.elem.style.backgroundSize = "100% 100%"; this.elem.style.width=this.width+"px"; this.elem.style.height=this.height+"px"; this.kwidth=this.knobwidth||(this.width*0.15|0); this.kheight=this.knobheight||(this.height*0.15|0); this.knob.style.width = this.kwidth+"px"; this.knob.style.height = this.kheight+"px"; if(!this.src){ let r=Math.min(this.width,this.height)*0.02; let svgbody= ` `; this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgbody)+")"; } else{ this.elem.style.backgroundImage = "url("+(this.src)+")"; } if(!this.knobsrc){ let svgthumb= ` `; this.knob.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgthumb)+")"; } else{ this.knob.style.backgroundImage = "url("+(this.knobsrc)+")"; } this.elem.style.outline=this.outline?"":"none"; this.redraw(); } redraw() { this.digits=0; if(this.step && this.step < 1) { for(let n = this.step ; n < 1; n *= 10) ++this.digits; } if(this.valuethis.max){ this.value=this.max; return; } let range = this.max - this.min; let style = this.knob.style; style.left=(this.width-this.kwidth)*(this._x-this.min)/(this.max-this.min)+"px"; style.top=(this.height-this.kheight)*(1-(this._y-this.min)/(this.max-this.min))+"px"; this.sensex=0; this.sensey=1; } _setX(v){ v=(Math.round((v-this.min)/this.step))*this.step+this.min; this._x=Math.min(this.max,Math.max(this.min,v)); if(this._x!=this.oldx){ this.oldx=this._x; if(this.conv){ this.convValue={x:eval(this.conv)(this._x),y:eval(this.conv)(this._y)}; } else this.convValue={x:this._x,y:this._y}; this.redraw(); this.showtip(0); return 1; } return 0; } _setY(v){ v=(Math.round((v-this.min)/this.step))*this.step+this.min; this._y=Math.min(this.max,Math.max(this.min,v)); if(this._y!=this.oldy){ this.oldy=this._y; if(this.conv){ this.convValue={x:eval(this.conv)(this._x),y:eval(this.conv)(this._y)}; } else this.convValue={x:this._x,y:this._y}; this.redraw(); this.showtip(0); return 1; } return 0; } setX(v,f){ if(this._setX(v)&&f) this.sendEvent("input"),this.sendEvent("change"); } setY(v,f){ if(this._setY(v)&&f) this.sendEvent("input"),this.sendEvent("change"); } wheel(e) { let delta=(this.max-this.min)*0.01; delta=e.deltaY>0?-delta:delta; if(!e.shiftKey) delta*=5; if(Math.abs(delta) < this.step) delta = (delta > 0) ? +this.step : -this.step; this.setValue(+this.value+delta,true); e.preventDefault(); e.stopPropagation(); this.redraw(); } pointerdown(ev){ if(!this.enable) return; let e=ev; if(ev.touches){ e = ev.changedTouches[0]; this.identifier=e.identifier; } else { if(e.buttons!=1 && e.button!=0) return; } this.elem.focus(); this.drag=1; this.showtip(0); let pointermove=(ev)=>{ let e=ev; if(ev.touches){ for(let i=0;i{ let e=ev; if(ev.touches){ for(let i=0;;){ if(ev.changedTouches[i].identifier==this.identifier){ break; } if(++i>=ev.changedTouches.length) return; } } this.drag=0; this.showtip(0); this.startPosX = this.startPosY = null; window.removeEventListener('mousemove', pointermove); window.removeEventListener('touchmove', pointermove, {passive:false}); window.removeEventListener('mouseup', pointerup); window.removeEventListener('touchend', pointerup); window.removeEventListener('touchcancel', pointerup); document.body.removeEventListener('touchstart', preventScroll,{passive:false}); this.sendEvent("change"); } pointermove(ev); let preventScroll=(e)=>{ e.preventDefault(); } if(e.touches) e = e.touches[0]; if(e.ctrlKey || e.metaKey) this.setValue(this.defvalue,true); else { this.startPosX = e.pageX; this.startPosY = e.pageY; this.startVal = this.value; window.addEventListener('mousemove', pointermove); window.addEventListener('touchmove', pointermove, {passive:false}); } window.addEventListener('mouseup', pointerup); window.addEventListener('touchend', pointerup); window.addEventListener('touchcancel', pointerup); document.body.addEventListener('touchstart', preventScroll,{passive:false}); e.preventDefault(); e.stopPropagation(); return false; } }); } catch(error){ console.log("webaudio-xypad already defined"); } // FOR MIDI LEARN class WebAudioControlsMidiManager { constructor(){ this.midiAccess = null; this.listOfWidgets = []; this.listOfExternalMidiListeners = []; this.updateWidgets(); this.initWebAudioControls(); } addWidget(w){ this.listOfWidgets.push(w); } updateWidgets(){ // this.listOfWidgets = document.querySelectorAll("webaudio-knob,webaudio-slider,webaudio-switch"); } initWebAudioControls() { if(navigator.requestMIDIAccess) { navigator.requestMIDIAccess().then( (ma)=>{this.midiAccess = ma,this.enableInputs()}, (err)=>{ console.log("MIDI not initialized - error encountered:" + err.code)} ); } } enableInputs() { let inputs = this.midiAccess.inputs.values(); console.log("Found " + this.midiAccess.inputs.size + " MIDI input(s)"); for(let input = inputs.next(); input && !input.done; input = inputs.next()) { console.log("Connected input: " + input.value.name); input.value.onmidimessage = this.handleMIDIMessage.bind(this); } } midiConnectionStateChange(e) { console.log("connection: " + e.port.name + " " + e.port.connection + " " + e.port.state); enableInputs(); } onMIDIStarted(midi) { this.midiAccess = midi; midi.onstatechange = this.midiConnectionStateChange; enableInputs(midi); } // Add hooks for external midi listeners support addMidiListener(callback) { this.listOfExternalMidiListeners.push(callback); } getCurrentConfigAsJSON() { return currentConfig.stringify(); } handleMIDIMessage(event) { this.listOfExternalMidiListeners.forEach(function (externalListener) { externalListener(event); }); if(((event.data[0] & 0xf0) == 0xf0) || ((event.data[0] & 0xf0) == 0xb0 && event.data[1] >= 120)) return; for(let w of this.listOfWidgets) { if(w.processMidiEvent) w.processMidiEvent(event); } if(opt.mididump) console.log(event.data); } contextMenuOpen(e,knob){ if(!this.midiAccess) return; let menu=document.getElementById("webaudioctrl-context-menu"); menu.style.left=e.pageX+"px"; menu.style.top=e.pageY+"px"; menu.knob=knob; menu.classList.add("active"); menu.knob.focus(); // document.activeElement.onblur=this.contextMenuClose; menu.knob.addEventListener("keydown",this.contextMenuCloseByKey.bind(this)); } contextMenuCloseByKey(e){ if(e.keyCode==27) this.contextMenuClose(); } contextMenuClose(){ let menu=document.getElementById("webaudioctrl-context-menu"); menu.knob.removeEventListener("keydown",this.contextMenuCloseByKey); menu.classList.remove("active"); let menuItemLearn=document.getElementById("webaudioctrl-context-menu-learn"); menuItemLearn.innerHTML = 'Learn'; menu.knob.midiMode = 'normal'; } contextMenuLearn(){ let menu=document.getElementById("webaudioctrl-context-menu"); let menuItemLearn=document.getElementById("webaudioctrl-context-menu-learn"); menuItemLearn.innerHTML = 'Listening...'; menu.knob.midiMode = 'learn'; } contextMenuClear(e){ let menu=document.getElementById("webaudioctrl-context-menu"); menu.knob.midiController={}; this.contextMenuClose(); } } if(window.UseWebAudioControlsMidi||opt.useMidi) window.webAudioControlsMidiManager = new WebAudioControlsMidiManager(); } ================================================ FILE: src/assets/js/webcomponents-lite.js ================================================ (function(){/* Copyright (c) 2016 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ 'use strict';var p,q="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this,ba="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)};function ca(){ca=function(){};q.Symbol||(q.Symbol=da)}var da=function(){var a=0;return function(b){return"jscomp_symbol_"+(b||"")+a++}}(); function ea(){ca();var a=q.Symbol.iterator;a||(a=q.Symbol.iterator=q.Symbol("iterator"));"function"!=typeof Array.prototype[a]&&ba(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return fa(this)}});ea=function(){}}function fa(a){var b=0;return ha(function(){return b"+this.innerHTML+""},set:function(a){if(this.parentNode){J.body.innerHTML=a;for(a=this.ownerDocument.createDocumentFragment();J.body.firstChild;)l.call(a, J.body.firstChild);m.call(this.parentNode,a,this)}else throw Error("Failed to set the 'outerHTML' property on 'Element': This element has no parent node.");},configurable:!0})};na(a.prototype);aa(a.prototype);a.J=function(c){c=b(c,"template");for(var d=0,e=c.length,f;d]/g, U=function(a){switch(a){case "&":return"&";case "<":return"<";case ">":return">";case "\u00a0":return" "}}}if(c||eb){a.ca=function(a,b){var c=f.call(a,!1);this.D&&this.D(c);b&&(l.call(c.content,f.call(a.content,!0)),fb(c.content,a.content));return c};var fb=function(c,d){if(d.querySelectorAll&&(d=b(d,"template"),0!==d.length)){c=b(c,"template");for(var e=0,f=c.length,h,g;e]*)(rel=['|"]?stylesheet['|"]?[^>]*>)/g,x={La:function(a,b){a.href&&a.setAttribute("href",x.Y(a.getAttribute("href"),b));a.src&&a.setAttribute("src",x.Y(a.getAttribute("src"),b));if("style"===a.localName){var c=x.ta(a.textContent,b,Ca);a.textContent=x.ta(c,b,Da)}},ta:function(a,b,c){return a.replace(c, function(a,c,d,e){a=d.replace(/["']/g,"");b&&(a=x.Y(a,b));return c+"'"+a+"'"+e})},Y:function(a,b){if(void 0===x.ba){x.ba=!1;try{var c=new URL("b","http://a");c.pathname="c%20d";x.ba="http://a/c%20d"===c.href}catch(Lg){}}if(x.ba)return(new URL(a,b)).href;c=x.Ba;c||(c=document.implementation.createHTMLDocument("temp"),x.Ba=c,c.ma=c.createElement("base"),c.head.appendChild(c.ma),c.la=c.createElement("a"));c.ma.href=b;c.la.href=a;return c.la.href||a}},na={async:!0,load:function(a,b,c){if(a)if(a.match(/^data:/)){a= a.split(",");var d=a[1];d=-1e.status?b(d,a):c(d)};e.send()}else c("error: href must be specified")}},aa=/Trident/.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent); k.prototype.loadImports=function(a){var b=this;a=m(a,"link[rel=import]");n(a,function(a){return b.s(a)})};k.prototype.s=function(a){var b=this,c=a.href;if(void 0!==this.a[c]){var d=this.a[c];d&&d.__loaded&&(a.__import=d,this.h(a))}else this.b++,this.a[c]="pending",na.load(c,function(a,d){a=b.Sa(a,d||c);b.a[c]=a;b.b--;b.loadImports(a);b.L()},function(){b.a[c]=null;b.b--;b.L()})};k.prototype.Sa=function(a,b){if(!a)return document.createDocumentFragment();aa&&(a=a.replace(Ea,function(a,b,c){return-1=== a.indexOf("type=")?b+" type=import-disable "+c:a}));var c=document.createElement("template");c.innerHTML=a;if(c.content)a=c.content,l(a);else for(a=document.createDocumentFragment();c.firstChild;)a.appendChild(c.firstChild);if(c=a.querySelector("base"))b=x.Y(c.getAttribute("href"),b),c.removeAttribute("href");c=m(a,'link[rel=import],link[rel=stylesheet][href][type=import-disable],style:not([type]),link[rel=stylesheet][href]:not([type]),script:not([type]),script[type="application/javascript"],script[type="text/javascript"]'); var d=0;n(c,function(a){h(a);x.La(a,b);a.setAttribute("import-dependency","");"script"===a.localName&&!a.src&&a.textContent&&(a.setAttribute("src","data:text/javascript;charset=utf-8,"+encodeURIComponent(a.textContent+("\n//# sourceURL="+b+(d?"-"+d:"")+".js\n"))),a.textContent="",d++)});return a};k.prototype.L=function(){var a=this;if(!this.b){this.c.disconnect();this.flatten(document);var b=!1,c=!1,d=function(){c&&b&&(a.loadImports(document),a.b||(a.c.observe(document.head,{childList:!0,subtree:!0}), a.Pa()))};this.Ua(function(){c=!0;d()});this.Ta(function(){b=!0;d()})}};k.prototype.flatten=function(a){var b=this;a=m(a,"link[rel=import]");n(a,function(a){var c=b.a[a.href];(a.__import=c)&&c.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&(b.a[a.href]=a,a.readyState="loading",a.__import=a,b.flatten(c),a.appendChild(c))})};k.prototype.Ta=function(a){function b(e){if(e]/g;function hc(a){switch(a){case "&":return"&";case "<":return"<";case ">":return">";case '"':return""";case "\u00a0":return" "}}function ic(a){for(var b={},c=0;c";break a;case Node.TEXT_NODE:g=g.data;g=k&&kc[k.localName]?g:g.replace(gc,hc);break a;case Node.COMMENT_NODE:g="\x3c!--"+g.data+"--\x3e";break a;default:throw window.console.error(g), Error("not implemented");}}c+=g}return c};var B={},D=document.createTreeWalker(document,NodeFilter.SHOW_ALL,null,!1),E=document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,null,!1);function mc(a){var b=[];D.currentNode=a;for(a=D.firstChild();a;)b.push(a),a=D.nextSibling();return b}B.parentNode=function(a){D.currentNode=a;return D.parentNode()};B.firstChild=function(a){D.currentNode=a;return D.firstChild()};B.lastChild=function(a){D.currentNode=a;return D.lastChild()};B.previousSibling=function(a){D.currentNode=a;return D.previousSibling()}; B.nextSibling=function(a){D.currentNode=a;return D.nextSibling()};B.childNodes=mc;B.parentElement=function(a){E.currentNode=a;return E.parentNode()};B.firstElementChild=function(a){E.currentNode=a;return E.firstChild()};B.lastElementChild=function(a){E.currentNode=a;return E.lastChild()};B.previousElementSibling=function(a){E.currentNode=a;return E.previousSibling()};B.nextElementSibling=function(a){E.currentNode=a;return E.nextSibling()}; B.children=function(a){var b=[];E.currentNode=a;for(a=E.firstChild();a;)b.push(a),a=E.nextSibling();return b};B.innerHTML=function(a){return lc(a,function(a){return mc(a)})};B.textContent=function(a){switch(a.nodeType){case Node.ELEMENT_NODE:case Node.DOCUMENT_FRAGMENT_NODE:a=document.createTreeWalker(a,NodeFilter.SHOW_TEXT,null,!1);for(var b="",c;c=a.nextNode();)b+=c.nodeValue;return b;default:return a.nodeValue}};var nc=Object.getOwnPropertyDescriptor(Element.prototype,"innerHTML")||Object.getOwnPropertyDescriptor(HTMLElement.prototype,"innerHTML"),oc=document.implementation.createHTMLDocument("inert"),pc=Object.getOwnPropertyDescriptor(Document.prototype,"activeElement"),qc={parentElement:{get:function(){var a=this.__shady&&this.__shady.parentNode;a&&a.nodeType!==Node.ELEMENT_NODE&&(a=null);return void 0!==a?a:B.parentElement(this)},configurable:!0},parentNode:{get:function(){var a=this.__shady&&this.__shady.parentNode; return void 0!==a?a:B.parentNode(this)},configurable:!0},nextSibling:{get:function(){var a=this.__shady&&this.__shady.nextSibling;return void 0!==a?a:B.nextSibling(this)},configurable:!0},previousSibling:{get:function(){var a=this.__shady&&this.__shady.previousSibling;return void 0!==a?a:B.previousSibling(this)},configurable:!0},className:{get:function(){return this.getAttribute("class")||""},set:function(a){this.setAttribute("class",a)},configurable:!0},nextElementSibling:{get:function(){if(this.__shady&& void 0!==this.__shady.nextSibling){for(var a=this.nextSibling;a&&a.nodeType!==Node.ELEMENT_NODE;)a=a.nextSibling;return a}return B.nextElementSibling(this)},configurable:!0},previousElementSibling:{get:function(){if(this.__shady&&void 0!==this.__shady.previousSibling){for(var a=this.previousSibling;a&&a.nodeType!==Node.ELEMENT_NODE;)a=a.previousSibling;return a}return B.previousElementSibling(this)},configurable:!0}},rc={childNodes:{get:function(){if(vb(this)){if(!this.__shady.childNodes){this.__shady.childNodes= [];for(var a=this.firstChild;a;a=a.nextSibling)this.__shady.childNodes.push(a)}var b=this.__shady.childNodes}else b=B.childNodes(this);b.item=function(a){return b[a]};return b},configurable:!0},childElementCount:{get:function(){return this.children.length},configurable:!0},firstChild:{get:function(){var a=this.__shady&&this.__shady.firstChild;return void 0!==a?a:B.firstChild(this)},configurable:!0},lastChild:{get:function(){var a=this.__shady&&this.__shady.lastChild;return void 0!==a?a:B.lastChild(this)}, configurable:!0},textContent:{get:function(){if(vb(this)){for(var a=[],b=0,c=this.childNodes,d;d=c[b];b++)d.nodeType!==Node.COMMENT_NODE&&a.push(d.textContent);return a.join("")}return B.textContent(this)},set:function(a){if("undefined"===typeof a||null===a)a="";switch(this.nodeType){case Node.ELEMENT_NODE:case Node.DOCUMENT_FRAGMENT_NODE:for(;this.firstChild;)this.removeChild(this.firstChild);(0b.__shady.assignedNodes.length&&(b.__shady.ia=!0)}b.__shady.ia&&(b.__shady.ia=!1,od(this,b))}a=this.o;b=[];for(c=0;cb.indexOf(d))||b.push(d);for(a=0;a "+b}))}a=a.replace(Df,function(a,b,c){return'[dir="'+c+'"] '+b+", "+b+'[dir="'+c+'"]'});return{value:a,Ka:b,stop:f}}function Bf(a,b){a=a.split(Ef);a[0]+=b;return a.join(Ef)} function Af(a,b){var c=a.match(Ff);return(c=c&&c[2].trim()||"")?c[0].match(Gf)?a.replace(Ff,function(a,c,f){return b+f}):c.split(Gf)[0]===b?c:Hf:a.replace(wf,b)}function If(a){a.selector===Jf&&(a.selector="html")}hf.prototype.c=function(a){return a.match(zf)?this.b(a,Kf):Bf(a.trim(),Kf)};q.Object.defineProperties(hf.prototype,{a:{configurable:!0,enumerable:!0,get:function(){return"style-scope"}}}); var uf=/:(nth[-\w]+)\(([^)]+)\)/,Kf=":not(.style-scope)",sf=",",xf=/(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=[])+)/g,Gf=/[[.:#*]/,wf=":host",Jf=":root",zf="::slotted",vf=new RegExp("^("+zf+")"),Ff=/(:host)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/,Cf=/(?:::slotted)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/,Df=/(.*):dir\((?:(ltr|rtl))\)/,qf=".",Ef=":",mf="class",Hf="should_not_match",W=new hf;function Lf(a,b,c,d){this.w=a||null;this.b=b||null;this.ja=c||[];this.G=null;this.P=d||"";this.a=this.u=this.B=null}function X(a){return a?a.__styleInfo:null}function Mf(a,b){return a.__styleInfo=b}Lf.prototype.c=function(){return this.w};Lf.prototype._getStyleRules=Lf.prototype.c;var Nf,Of=window.Element.prototype;Nf=Of.matches||Of.matchesSelector||Of.mozMatchesSelector||Of.msMatchesSelector||Of.oMatchesSelector||Of.webkitMatchesSelector;var Pf=navigator.userAgent.match("Trident");function Qf(){}function Rf(a){var b={},c=[],d=0;af(a,function(a){Sf(a);a.index=d++;a=a.i.cssText;for(var c;c=Ve.exec(a);){var e=c[1];":"!==c[2]&&(b[e]=!0)}},function(a){c.push(a)});a.b=c;a=[];for(var e in b)a.push(e);return a} function Sf(a){if(!a.i){var b={},c={};Tf(a,c)&&(b.v=c,a.rules=null);b.cssText=a.parsedCssText.replace(Ye,"").replace(Te,"");a.i=b}}function Tf(a,b){var c=a.i;if(c){if(c.v)return Object.assign(b,c.v),!0}else{c=a.parsedCssText;for(var d;a=Te.exec(c);){d=(a[2]||a[3]).trim();if("inherit"!==d||"unset"!==d)b[a[1].trim()]=d;d=!0}return d}} function Uf(a,b,c){b&&(b=0<=b.indexOf(";")?Vf(a,b,c):ff(b,function(b,e,f,h){if(!e)return b+h;(e=Uf(a,c[e],c))&&"initial"!==e?"apply-shim-inherit"===e&&(e="inherit"):e=Uf(a,c[f]||f,c)||f;return b+(e||"")+h}));return b&&b.trim()||""} function Vf(a,b,c){b=b.split(";");for(var d=0,e,f;d *"===f||"html"===f,g=0===f.indexOf(":host")&&!h;"shady"===c&&(h=f===e+" > *."+e||-1!==f.indexOf("html"),g=!h&&0===f.indexOf(e));"shadow"===c&&(h=":host > *"===f||"html"===f,g=g&&!h);if(h||g)c=e,g&&(R&&!b.m&&(b.m=rf(W,b,W.b,a?qf+a:"",e)),c=b.m||e),d({Xa:c,Qa:g,hb:h})}} function Yf(a,b){var c={},d={},e=b&&b.__cssBuild;af(b,function(b){Xf(a,b,e,function(e){Nf.call(a.b||a,e.Xa)&&(e.Qa?Tf(b,c):Tf(b,d))})},null,!0);return{Wa:d,Oa:c}} function Zf(a,b,c,d){var e=V(b),f=pf(e.is,e.P),h=new RegExp("(?:^|[^.#[:])"+(b.extends?"\\"+f.slice(0,-1)+"\\]":f)+"($|[.:[\\s>+~])");e=X(b).w;var g=$f(e,d);return nf(b,e,function(b){var e="";b.i||Sf(b);b.i.cssText&&(e=Vf(a,b.i.cssText,c));b.cssText=e;if(!R&&!cf(b)&&b.cssText){var k=e=b.cssText;null==b.ra&&(b.ra=We.test(e));if(b.ra)if(null==b.W){b.W=[];for(var n in g)k=g[n],k=k(e),e!==k&&(e=k,b.W.push(n))}else{for(n=0;n=l._useCount&&l.parentNode&&l.parentNode.removeChild(l));R?f.a?(f.a.textContent=e,d=f.a):e&&(d=df(e,g,a.shadowRoot,f.b)):d?d.parentNode|| (Pf&&-1 ({ type: CHANNELS_CONSTANTS.SET_CHANNEL_GAIN, payload: { channel, gain, }, }); export const setChannelPan = (channel, pan) => ({ type: CHANNELS_CONSTANTS.SET_CHANNEL_PAN, payload: { channel, pan, }, }); export const setChannelPitchCoarse = (channel, pitchCoarse) => ({ type: CHANNELS_CONSTANTS.SET_CHANNEL_PITCH_COARSE, payload: { channel, pitchCoarse, }, }); export const setChannelPitchFine = (channel, pitchFine) => ({ type: CHANNELS_CONSTANTS.SET_CHANNEL_PITCH_FINE, payload: { channel, pitchFine, }, }); export const setChannelMuted = (channel, muted) => ({ type: CHANNELS_CONSTANTS.SET_CHANNEL_MUTED, payload: { channel, muted, }, }); export const setChannelSolo = (channel, solo) => ({ type: CHANNELS_CONSTANTS.SET_CHANNEL_SOLO, payload: { channel, solo, }, }); export const addChannel = (channel) => ({ type: CHANNELS_CONSTANTS.ADD_CHANNEL, payload: channel, }); export const removeChannel = (id) => ({ type: CHANNELS_CONSTANTS.REMOVE_CHANNEL, payload: id, }); export const updateChannelOrder = (oldIndex, newIndex) => ({ type: CHANNELS_CONSTANTS.UPDATE_CHANNEL_ORDER, payload: { oldIndex, newIndex, }, }); export const replaceChannels = (channels) => ({ type: CHANNELS_CONSTANTS.SET_CHANNELS, payload: channels, }); export const sampleLoaded = (channelID, isLoaded) => ({ type: CHANNELS_CONSTANTS.SAMPLE_LOADED, payload: { channelID, isLoaded, }, }); export const setChannelSample = (channel, sampleURL) => ({ type: CHANNELS_CONSTANTS.SET_CHANNEL_SAMPLE, payload: { channel, sampleURL, }, }); export const setChannelReverb = (channel, reverb) => ({ type: CHANNELS_CONSTANTS.SET_CHANNEL_REVERB, payload: { channel, reverb, }, }); export const loadSampleStatefully = (dispatch, channel) => { dispatch(sampleLoaded(channel.id, false)); loadSample(channel.sample).then((success) => { if (success) { dispatch(sampleLoaded(channel.id, true)); } }); }; export const loadChannels = (channels, notes) => (dispatch) => { channels.forEach((channel) => { loadSampleStatefully(dispatch, channel); }); dispatch(replaceChannels(channels)); dispatch(setNotes(notes)); }; export const newChannel = () => (dispatch) => { const channelToAdd = { id: uuid(), sample: factorySamples[0].url, gain: 1, pitchCoarse: 0, pitchFine: 0, pan: 0, }; dispatch(addChannel(channelToAdd)); dispatch(initializeChannelNotes(channelToAdd.id)); dispatch(setSelectedChannel(channelToAdd.id)); loadSampleStatefully(dispatch, channelToAdd); }; export const loadAndSetChannelSample = (channelID, sampleURL) => (dispatch) => { dispatch(sampleLoaded(channelID, false)); loadSample(sampleURL).then((success) => { if (success) { dispatch(sampleLoaded(channelID, true)); } else { dispatch(showFlashMessage(FLASH_MESSAGES.SAMPLE_LOAD_ERROR)); } }); dispatch(setChannelSample(channelID, sampleURL)); }; export const deleteChannel = (channelID, channels, selectedChannelId) => (dispatch) => { if (channels.length === 1) { dispatch(newChannel()); } if (selectedChannelId === channelID) { dispatch(setSelectedChannel(channels[0].id)); } dispatch(removeChannel(channelID)); }; ================================================ FILE: src/common/channels/channels.constants.js ================================================ export const CHANNELS_CONSTANTS = { ADD_CHANNEL: 'ADD_CHANNEL', REMOVE_CHANNEL: 'REMOVE_CHANNEL', SET_CHANNEL_SAMPLE: 'SET_CHANNEL_SAMPLE', SET_CHANNEL_PITCH_COARSE: 'SET_CHANNEL_PITCH_COARSE', SET_CHANNEL_FINE: 'SET_CHANNEL_FINE', SET_CHANNEL_GAIN: 'SET_CHANNEL_GAIN', SET_CHANNEL_PAN: 'SET_CHANNEL_PAN', SET_CHANNEL_REVERB: 'SET_CHANNEL_REVERB', SET_CHANNEL_MUTED: 'SET_CHANNEL_MUTED', SET_CHANNEL_SOLO: 'SET_CHANNEL_SOLO', SET_CHANNELS: 'SET_CHANNELS', SAMPLE_LOADED: 'SAMPLE_LOADED', UPDATE_CHANNEL_ORDER: 'UPDATE_CHANNEL_ORDER', }; ================================================ FILE: src/common/channels/channels.reducer.js ================================================ import * as R from 'ramda'; import { CHANNELS_CONSTANTS } from './channels.constants'; import presets from '../../presets'; export const channelsInitialState = R.clone(presets[1].channels); export const channelsReducer = (state = channelsInitialState, action) => { switch (action.type) { case CHANNELS_CONSTANTS.SET_CHANNEL_SAMPLE: return state.map((channel) => { if (channel.id === action.payload.channel) { return { ...channel, sample: action.payload.sampleURL }; } return channel; }); case CHANNELS_CONSTANTS.SET_CHANNEL_GAIN: return state.map((channel) => { if (channel.id === action.payload.channel) { return { ...channel, gain: action.payload.gain }; } return channel; }); case CHANNELS_CONSTANTS.SET_CHANNEL_PAN: return state.map((channel) => { if (channel.id === action.payload.channel) { return { ...channel, pan: action.payload.pan }; } return channel; }); case CHANNELS_CONSTANTS.SET_CHANNEL_PITCH_COARSE: return state.map((channel) => { if (channel.id === action.payload.channel) { return { ...channel, pitchCoarse: action.payload.pitchCoarse }; } return channel; }); case CHANNELS_CONSTANTS.SET_CHANNEL_PITCH_FINE: return state.map((channel) => { if (channel.id === action.payload.channel) { return { ...channel, pitchFine: action.payload.pitchFine }; } return channel; }); case CHANNELS_CONSTANTS.SET_CHANNEL_REVERB: return state.map((channel) => { if (channel.id === action.payload.channel) { return { ...channel, reverb: action.payload.reverb }; } return channel; }); case CHANNELS_CONSTANTS.ADD_CHANNEL: return [...state, action.payload]; case CHANNELS_CONSTANTS.REMOVE_CHANNEL: return state.filter((channel) => channel.id !== action.payload); case CHANNELS_CONSTANTS.SAMPLE_LOADED: return state.map((channel) => { if (channel.id === action.payload.channelID) { return { ...channel, sampleLoaded: action.payload.isLoaded }; } return channel; }); case CHANNELS_CONSTANTS.SET_CHANNEL_MUTED: return state.map((channel) => { if (channel.id === action.payload.channel) { return { ...channel, muted: action.payload.muted, solo: false, }; } return channel; }); case CHANNELS_CONSTANTS.SET_CHANNEL_SOLO: return state.map((channel) => { if (channel.id === action.payload.channel) { return { ...channel, solo: action.payload.solo, muted: false, }; } return channel; }); case CHANNELS_CONSTANTS.UPDATE_CHANNEL_ORDER: return R.insert( action.payload.newIndex, state[action.payload.oldIndex], R.remove(action.payload.oldIndex, 1, state), ); case CHANNELS_CONSTANTS.SET_CHANNELS: return [...action.payload]; default: return state; } }; ================================================ FILE: src/common/channels/channels.reducer.test.js ================================================ import { channelsInitialState, channelsReducer } from './channels.reducer'; import { setChannelSample, setChannelGain, setChannelPan, addChannel, removeChannel, replaceChannels, setChannelPitchCoarse, setChannelPitchFine, setChannelReverb, setChannelMuted, setChannelSolo, } from './channels.actions'; jest.mock('../../presets'); jest.mock('../../samples.config'); jest.mock('../../services/featureChecks'); const testSample = '/fake/sample/b/url.wav'; describe('setChannelSample', () => { test('should change a sample', () => { const state = channelsReducer( channelsInitialState, setChannelSample(channelsInitialState[0].id, testSample), ); expect(state[0].sample).toEqual(testSample); }); }); describe('setChannelGain', () => { test('should change gain for a channel', () => { const state = channelsReducer( channelsInitialState, setChannelGain(channelsInitialState[0].id, 0.5), ); expect(state[0].gain).toEqual(0.5); }); }); describe('setChannelPan', () => { test('should change pan for a channel', () => { const state = channelsReducer( channelsInitialState, setChannelPan(channelsInitialState[0].id, 0.5), ); expect(state[0].pan).toEqual(0.5); }); }); describe('setChannelPitchCoarse', () => { test('should change pitch (coarse) for a channel', () => { const state = channelsReducer( channelsInitialState, setChannelPitchCoarse(channelsInitialState[0].id, 5), ); expect(state[0].pitchCoarse).toEqual(5); }); }); describe('setChannelPitchFine', () => { test('should change pitch (fine) for a channel', () => { const state = channelsReducer( channelsInitialState, setChannelPitchFine(channelsInitialState[0].id, -50), ); expect(state[0].pitchFine).toEqual(-50); }); }); describe('setChannelReverb', () => { test('should change reverb for a channel', () => { const state = channelsReducer( channelsInitialState, setChannelReverb(channelsInitialState[0].id, 0.5), ); expect(state[0].reverb).toEqual(0.5); }); }); describe('setChannelMuted', () => { test('should mute a channel', () => { const state = channelsReducer( channelsInitialState, setChannelMuted(channelsInitialState[0].id, true), ); expect(state[0].muted).toEqual(true); }); test('should set solo to false if it was true', () => { const soloState = channelsReducer( channelsInitialState, setChannelSolo(channelsInitialState[0].id, true), ); expect(soloState[0].solo).toEqual(true); const state = channelsReducer( soloState, setChannelMuted(channelsInitialState[0].id, true), ); expect(state[0].solo).toEqual(false); }); }); describe('setChannelSolo', () => { test('should solo a channel', () => { const state = channelsReducer( channelsInitialState, setChannelSolo(channelsInitialState[0].id, true), ); expect(state[0].solo).toEqual(true); }); test('should set muted to false if it was true', () => { const mutedState = channelsReducer( channelsInitialState, setChannelMuted(channelsInitialState[0].id, true), ); expect(mutedState[0].muted).toEqual(true); const state = channelsReducer( mutedState, setChannelSolo(channelsInitialState[0].id, true), ); expect(state[0].muted).toEqual(false); }); }); describe('addChannel', () => { test('should add a channel', () => { const state = channelsReducer( channelsInitialState, addChannel({ id: '12345', gain: 1, sample: {}, }), ); expect(state.length).toEqual(channelsInitialState.length + 1); }); }); describe('removeChannel', () => { test('should remove a channel that exists', () => { const state = channelsReducer( channelsInitialState, removeChannel(channelsInitialState[0].id), ); expect(state.length).toEqual(channelsInitialState.length - 1); }); test('should do nothing if no channel matches the ID', () => { const state = channelsReducer( channelsInitialState, removeChannel('foo'), ); expect(state.length).toEqual(channelsInitialState.length); }); }); describe('replaceChannels', () => { test('should replace existing channels', () => { const state = channelsReducer( channelsInitialState, replaceChannels([ { id: 'empty_channel', sample: 'test', gain: 1, }, ]), ); expect(state.length).toEqual(1); }); }); ================================================ FILE: src/common/channels/channels.selectors.js ================================================ import * as R from 'ramda'; export const channelsSelector = R.path(['channels']); ================================================ FILE: src/common/channels/index.js ================================================ export * from './channels.reducer'; export * from './channels.selectors'; export * from './channels.actions'; ================================================ FILE: src/common/index.js ================================================ export * from './playbackSession'; export * from './channels'; export * from './tempo'; export * from './master'; export * from './notes'; export * from './presets'; export * from './window'; export * from './userSamples'; ================================================ FILE: src/common/master/index.js ================================================ export * from './master.reducer'; export * from './master.selectors'; export * from './master.actions'; ================================================ FILE: src/common/master/master.actions.js ================================================ import { MASTER_CONSTANTS } from './master.constants'; export const setPattern = (patternIndex) => ({ type: MASTER_CONSTANTS.SET_PATTERN, payload: patternIndex, }); export const setSelectedChannel = (channelID) => ({ type: MASTER_CONSTANTS.SET_SELECTED_CHANNEL, payload: channelID, }); ================================================ FILE: src/common/master/master.constants.js ================================================ export const MASTER_CONSTANTS = { SET_PATTERN: 'SET_PATTERN', SET_SELECTED_CHANNEL: 'SET_SELECTED_CHANNEL', }; ================================================ FILE: src/common/master/master.reducer.js ================================================ import { MASTER_CONSTANTS } from './master.constants'; import presets from '../../presets'; export const masterInitialState = { pattern: 0, selectedChannel: presets[1].channels[0].id, }; export const masterReducer = (state = masterInitialState, action) => { switch (action.type) { case MASTER_CONSTANTS.SET_PATTERN: return { ...state, pattern: action.payload, }; case MASTER_CONSTANTS.SET_SELECTED_CHANNEL: return { ...state, selectedChannel: action.payload, }; default: return state; } }; ================================================ FILE: src/common/master/master.reducer.test.js ================================================ import { masterInitialState, masterReducer } from './master.reducer'; import { setPattern, } from './master.actions'; describe('setPattern', () => { test('should change the pattern', () => { const state = masterReducer(masterInitialState, setPattern(1)); expect(state.pattern).toEqual(1); }); }); ================================================ FILE: src/common/master/master.selectors.js ================================================ import * as R from 'ramda'; export const patternSelector = R.path(['master', 'pattern']); export const selectedChannelSelector = R.path(['master', 'selectedChannel']); ================================================ FILE: src/common/notes/index.js ================================================ export * from './notes.reducer'; export * from './notes.selectors'; export * from './notes.actions'; ================================================ FILE: src/common/notes/notes.actions.js ================================================ import { NOTES_CONSTANTS } from './notes.constants'; export const initializeChannelNotes = (channelID) => ({ type: NOTES_CONSTANTS.INITIALIZE_CHANNEL, payload: channelID, }); export const removeChannelNotes = (channelID) => ({ type: NOTES_CONSTANTS.REMOVE_CHANNEL, payload: channelID, }); export const setNotes = (notes) => ({ type: NOTES_CONSTANTS.SET_NOTES, payload: notes, }); export const toggleNote = (channelID, pattern, beat) => ({ type: NOTES_CONSTANTS.TOGGLE_NOTE, payload: { channelID, pattern, beat, }, }); ================================================ FILE: src/common/notes/notes.constants.js ================================================ export const NOTES_CONSTANTS = { INITIALIZE_CHANNEL: 'INITIALIZE_CHANNEL_NOTES', REMOVE_CHANNEL: 'REMOVE_CHANNEL_NOTES', TOGGLE_NOTE: 'TOGGLE_NOTE', SET_NOTES: 'SET_NOTES', }; ================================================ FILE: src/common/notes/notes.reducer.js ================================================ import { uuid } from '../../services/uuid'; import presets from '../../presets'; import { EMPTY_NOTE_ROW } from '../../presets/empty'; import { NOTES_CONSTANTS } from './notes.constants'; export const notesInitialState = presets[1].notes; // Returns a new noteAr clone with a note at beat either added or removed const toggleNote = (noteAr, beat) => { if (noteAr.find((note) => note.beat === beat)) { return noteAr.filter((note) => note.beat !== beat); } return [ ...noteAr, { beat, id: uuid(), }, ]; }; // Returns new state object with note at beat on pattern toggled const toggleNoteState = (state, { channelID, pattern, beat }) => ({ ...state, [channelID]: state[channelID].map((noteAr, patternIndex) => { if (patternIndex === pattern) { // This is the active pattern return toggleNote(noteAr, beat); } return noteAr; // Do nothing to other patterns }), }); export const notesReducer = (state = notesInitialState, action) => { switch (action.type) { case NOTES_CONSTANTS.TOGGLE_NOTE: return toggleNoteState(state, action.payload); case NOTES_CONSTANTS.INITIALIZE_CHANNEL: return { ...state, [action.payload]: EMPTY_NOTE_ROW, // TO DO: add empty array for each pattern }; case NOTES_CONSTANTS.REMOVE_CHANNEL: return { ...state, [action.payload]: undefined, }; case NOTES_CONSTANTS.SET_NOTES: return { ...action.payload, }; default: return state; } }; ================================================ FILE: src/common/notes/notes.reducer.test.js ================================================ import { notesReducer } from './notes.reducer'; import { toggleNote, initializeChannelNotes, removeChannelNotes, setNotes, } from './notes.actions'; jest.mock('../../presets'); jest.mock('../../samples.config'); const testNotes = { bongo: [ [ { id: 'bing', beat: 1, }, { id: 'bong', beat: 3, }, ], [ { id: 'ping', beat: 2, }, { id: 'pang', beat: 4, }, ], ], }; describe('toggleNote', () => { test('should toggle a note off', () => { const state = notesReducer(testNotes, toggleNote('bongo', 0, 1)); expect(state.bongo[0].length).toBe(1); }); test('should toggle a note on', () => { const state = notesReducer(testNotes, toggleNote('bongo', 0, 2)); expect(state.bongo[0].length).toBe(3); }); test('should toggle a note on the second preset on', () => { const state = notesReducer(testNotes, toggleNote('bongo', 1, 1)); expect(state.bongo[1].length).toBe(3); }); }); describe('initializeChannel', () => { test('should add a channel', () => { const state = notesReducer( testNotes, initializeChannelNotes('cowbell'), ); expect(state.cowbell).not.toBeUndefined(); }); }); describe('removeChannel', () => { test('should remove a channel that exists', () => { const state = notesReducer( testNotes, removeChannelNotes('bongo'), ); expect(state.bongo).toBeUndefined(); }); test('should do nothing if no channel matches the ID', () => { const state = notesReducer( testNotes, removeChannelNotes('foobar'), ); expect(state.bongo).not.toBeUndefined(); expect(state.foobar).toBeUndefined(); }); }); describe('setNotes', () => { test('should replace existing channels', () => { const state = notesReducer( testNotes, setNotes({ maracas: [ [], [], ], }), ); expect(state.bongo).toBeUndefined(); expect(state.maracas).not.toBeUndefined(); }); }); ================================================ FILE: src/common/notes/notes.selectors.js ================================================ import * as R from 'ramda'; export const notesSelector = R.path(['notes']); ================================================ FILE: src/common/playbackSession/index.js ================================================ export * from './playbackSession.actions'; export * from './playbackSession.reducer'; export * from './playbackSession.selectors'; ================================================ FILE: src/common/playbackSession/playbackSession.actions.js ================================================ import { PLAYBACK_SESSION_CONSTANTS } from './playbackSession.constants'; import { getAudioContext } from '../../services/audioContext'; import { unmute } from '../../services/unmute'; export const startPlayback = () => ({ type: PLAYBACK_SESSION_CONSTANTS.START_PLAYBACK, }); export const stopPlayback = () => ({ type: PLAYBACK_SESSION_CONSTANTS.STOP_PLAYBACK, }); export const setStartTime = (val) => ({ type: PLAYBACK_SESSION_CONSTANTS.SET_START_TIME, payload: val, }); export const startPlaybackAndResume = () => (dispatch) => { unmute(); getAudioContext().resume(); dispatch(startPlayback()); }; ================================================ FILE: src/common/playbackSession/playbackSession.constants.js ================================================ export const PLAYBACK_SESSION_CONSTANTS = { START_PLAYBACK: 'START_PLAYBACK', STOP_PLAYBACK: 'STOP_PLAYBACK', SET_START_TIME: 'SET_START_TIME', }; ================================================ FILE: src/common/playbackSession/playbackSession.reducer.js ================================================ import { PLAYBACK_SESSION_CONSTANTS } from './playbackSession.constants'; import { getAudioContext } from '../../services/audioContext'; import { LOOKAHEAD } from '../../services/audioEngine.config'; export const playbackSessionInitialState = { playing: false, startTime: null, currentBeat: 1, }; export const playbackSessionReducer = (state = playbackSessionInitialState, action) => { switch (action.type) { case PLAYBACK_SESSION_CONSTANTS.START_PLAYBACK: return { ...state, playing: true, startTime: getAudioContext().currentTime + LOOKAHEAD + LOOKAHEAD, }; case PLAYBACK_SESSION_CONSTANTS.STOP_PLAYBACK: return { ...state, playing: false, startTime: null, }; case PLAYBACK_SESSION_CONSTANTS.SET_START_TIME: return { ...state, startTime: action.payload, }; default: return state; } }; ================================================ FILE: src/common/playbackSession/playbackSession.reducer.test.js ================================================ import { playbackSessionInitialState, playbackSessionReducer, } from './playbackSession.reducer'; import { startPlayback, stopPlayback, setStartTime, } from './playbackSession.actions'; import { LOOKAHEAD } from '../../services/audioEngine.config'; jest.mock('../../services/audioContext.js'); describe('startPlayback', () => { test('should set playing to true', () => { const state = playbackSessionReducer(playbackSessionInitialState, startPlayback()); expect(state.playing).toBe(true); }); test('should set startTime to current time plus lookahead', () => { const state = playbackSessionReducer(playbackSessionInitialState, startPlayback()); expect(state.startTime).toBe(1 + LOOKAHEAD + LOOKAHEAD); }); }); describe('startPlayback', () => { test('should set playing to false', () => { const state = playbackSessionReducer(playbackSessionInitialState, stopPlayback()); expect(state.playing).toBe(false); }); test('should set startTime to null', () => { const state = playbackSessionReducer(playbackSessionInitialState, stopPlayback()); expect(state.startTime).toBeNull(); }); }); describe('setStartTime', () => { test('should set startTime', () => { const state = playbackSessionReducer(playbackSessionInitialState, setStartTime(2.1234)); expect(state.startTime).toBe(2.1234); }); }); ================================================ FILE: src/common/playbackSession/playbackSession.selectors.js ================================================ import * as R from 'ramda'; export const playingSelector = R.path(['playbackSession', 'playing']); export const startTimeSelector = R.path(['playbackSession', 'startTime']); ================================================ FILE: src/common/presets/index.js ================================================ export * from './presets.actions'; export * from './presets.reducer'; export * from './presets.selectors'; ================================================ FILE: src/common/presets/presets.actions.js ================================================ import { setBPM, setSwing } from '../tempo'; import { loadChannels } from '../channels'; import { setPattern, setSelectedChannel } from '../master'; import { PRESETS_CONSTANTS } from './presets.constants'; import presets from '../../presets'; import { showFlashMessage, FLASH_MESSAGES } from '../window'; import { currentStateSelector } from './presets.selectors'; export const setPreset = (presetName) => ({ type: PRESETS_CONSTANTS.SET_PRESET, payload: presetName, }); export const savePreset = (preset) => ({ type: PRESETS_CONSTANTS.SAVE_PRESET, payload: preset, }); export const savePresetAs = (preset) => ({ type: PRESETS_CONSTANTS.SAVE_PRESET_AS, payload: preset, }); export const deletePreset = (presetName) => ({ type: PRESETS_CONSTANTS.DELETE_PRESET, payload: presetName, }); export const loadPreset = (preset) => (dispatch) => { dispatch(setBPM(preset.bpm)); dispatch(setSwing(preset.swing)); dispatch(loadChannels(preset.channels, preset.notes)); dispatch(setPreset(preset.name)); dispatch(setPattern(0)); dispatch(setSelectedChannel(preset.channels[0].id)); }; export const erasePreset = (presetName) => (dispatch) => { dispatch(setBPM(presets[0].bpm)); dispatch(setSwing(presets[0].swing)); dispatch(loadChannels(presets[0].channels, presets[0].notes)); dispatch(setPreset(presets[0].name)); dispatch(setPattern(0)); dispatch(setSelectedChannel(presets[0].channels[0].id)); dispatch(deletePreset(presetName)); dispatch(showFlashMessage(FLASH_MESSAGES.PRESET_DELETED)); }; export const doSavePresetAs = (presetName) => (dispatch, getState) => { const currentState = currentStateSelector(getState()); dispatch(savePresetAs({ ...currentState, name: presetName, })); dispatch(setPreset(presetName)); dispatch(showFlashMessage(FLASH_MESSAGES.PRESET_SAVED)); }; export const doSavePreset = (presetName) => (dispatch, getState) => { const currentState = currentStateSelector(getState()); dispatch(savePreset({ ...currentState, name: presetName, })); dispatch(showFlashMessage(FLASH_MESSAGES.PRESET_SAVED)); }; ================================================ FILE: src/common/presets/presets.constants.js ================================================ export const PRESETS_CONSTANTS = { SAVE_PRESET: 'SAVE_PRESET', SAVE_PRESET_AS: 'SAVE_PRESET_AS', DELETE_PRESET: 'DELETE_PRESET', SET_PRESET: 'SET_PRESET', }; ================================================ FILE: src/common/presets/presets.reducer.js ================================================ import { PRESETS_CONSTANTS } from './presets.constants'; import defaultPresets from '../../presets'; export const presetsInitialState = { userPresets: [], preset: defaultPresets[1].name, }; export const presetsReducer = (state = presetsInitialState, action) => { switch (action.type) { case PRESETS_CONSTANTS.SET_PRESET: return { ...state, preset: action.payload, }; case PRESETS_CONSTANTS.SAVE_PRESET: return { ...state, userPresets: state.userPresets.map( (userPreset) => (userPreset.name === action.payload.name ? action.payload : userPreset), ), }; case PRESETS_CONSTANTS.SAVE_PRESET_AS: return { ...state, userPresets: [ ...state.userPresets.filter( (userPreset) => userPreset.name !== action.payload.name, ), action.payload, ], }; case PRESETS_CONSTANTS.DELETE_PRESET: return { ...state, userPresets: state.userPresets.filter( (userPreset) => userPreset.name !== action.payload, ), }; default: return state; } }; ================================================ FILE: src/common/presets/presets.reducer.test.js ================================================ import { presetsInitialState, presetsReducer } from './presets.reducer'; import { setPreset, savePreset, savePresetAs, deletePreset, } from './presets.actions'; jest.mock('../../presets'); jest.mock('../../services/featureChecks'); const testPreset = { name: 'Test preset', bpm: 120, }; describe('setPreset', () => { test('should change the preset', () => { const state = presetsReducer(presetsInitialState, setPreset('hello')); expect(state.preset).toEqual('hello'); }); }); describe('savePresetAs', () => { test('should add a new user preset', () => { const state = presetsReducer(presetsInitialState, savePresetAs(testPreset)); expect(state.userPresets.length).toEqual(1); }); }); describe('savePreset', () => { test('should update a user preset', () => { const state = presetsReducer(presetsInitialState, savePresetAs(testPreset)); expect(state.userPresets.length).toEqual(1); expect(state.userPresets[0].bpm).toEqual(120); const newState = presetsReducer(state, savePreset({ name: 'Test preset', bpm: 100, })); expect(newState.userPresets.length).toEqual(1); expect(newState.userPresets[0].bpm).toEqual(100); }); }); describe('deletePreset', () => { test('should add a new user preset', () => { const state = presetsReducer(presetsInitialState, savePresetAs(testPreset)); expect(state.userPresets.length).toEqual(1); const newState = presetsReducer(state, deletePreset('Test preset')); expect(newState.userPresets.length).toEqual(0); }); }); ================================================ FILE: src/common/presets/presets.selectors.js ================================================ import * as R from 'ramda'; import { createSelector } from 'reselect'; import { channelsSelector } from '../channels'; import { notesSelector } from '../notes'; import { bpmSelector, swingSelector } from '../tempo'; export const userPresetsSelector = R.path(['presets', 'userPresets']); export const presetSelector = R.path(['presets', 'preset']); export const currentStateSelector = createSelector( channelsSelector, notesSelector, bpmSelector, swingSelector, (channels, notes, bpm, swing) => ({ notes, bpm, swing, channels: channels.map( (channel) => R.omit(['sampleLoaded'], channel), ), }), ); ================================================ FILE: src/common/tempo/index.js ================================================ export * from './tempo.actions'; export * from './tempo.reducer'; export * from './tempo.selectors'; ================================================ FILE: src/common/tempo/tempo.actions.js ================================================ import { TEMPO_CONSTANTS } from './tempo.constants'; export const setBPM = (val) => ({ type: TEMPO_CONSTANTS.SET_BPM, payload: val, }); export const setSwing = (val) => ({ type: TEMPO_CONSTANTS.SET_SWING, payload: val, }); ================================================ FILE: src/common/tempo/tempo.constants.js ================================================ export const TEMPO_CONSTANTS = { SET_BPM: 'SET_BPM', SET_SWING: 'SET_SWING', }; ================================================ FILE: src/common/tempo/tempo.reducer.js ================================================ import { TEMPO_CONSTANTS } from './tempo.constants'; import presets from '../../presets'; export const tempoInitialState = { bpm: presets[1].bpm, swing: presets[1].swing, }; export const tempoReducer = (state = tempoInitialState, action) => { switch (action.type) { case TEMPO_CONSTANTS.SET_BPM: return { ...state, bpm: action.payload, }; case TEMPO_CONSTANTS.SET_SWING: return { ...state, swing: action.payload, }; default: return state; } }; ================================================ FILE: src/common/tempo/tempo.reducer.test.js ================================================ import { tempoInitialState, tempoReducer, } from './tempo.reducer'; import { setBPM, setSwing } from './tempo.actions'; jest.mock('../../presets'); describe('setBPM', () => { test('should set bpm', () => { const state = tempoReducer(tempoInitialState, setBPM(123)); expect(state.bpm).toBe(123); }); }); describe('setSwing', () => { test('should set swing', () => { const state = tempoReducer(tempoInitialState, setSwing(0.4)); expect(state.swing).toBe(0.4); }); }); ================================================ FILE: src/common/tempo/tempo.selectors.js ================================================ import * as R from 'ramda'; export const bpmSelector = R.path(['tempo', 'bpm']); export const swingSelector = R.path(['tempo', 'swing']); ================================================ FILE: src/common/userSamples/index.js ================================================ export * from './userSamples.actions'; export * from './userSamples.reducer'; export * from './userSamples.selectors'; ================================================ FILE: src/common/userSamples/userSamples.actions.js ================================================ import { saveToSampleStore } from '../../services/sampleStore'; import { USER_SAMPLES_CONSTANTS } from './userSamples.constants'; import { loadAndSetChannelSample } from '../channels'; import { showFlashMessage, FLASH_MESSAGES } from '../window'; export const addUserSample = (sample) => ({ type: USER_SAMPLES_CONSTANTS.ADD_USER_SAMPLE, payload: sample, }); export const removeUserSample = (sampleId) => ({ type: USER_SAMPLES_CONSTANTS.REMOVE_USER_SAMPLE, payload: sampleId, }); export const clearUserSamples = () => ({ type: USER_SAMPLES_CONSTANTS.CLEAR_USER_SAMPLES, }); export const saveUserSample = (channel, files) => (dispatch) => { saveToSampleStore(files[0]) .then((sampleURL) => { dispatch(addUserSample(sampleURL)); dispatch(loadAndSetChannelSample(channel, sampleURL)); }) .catch(() => { dispatch(showFlashMessage(FLASH_MESSAGES.SAMPLE_LOAD_ERROR)); }); }; ================================================ FILE: src/common/userSamples/userSamples.constants.js ================================================ export const USER_SAMPLES_CONSTANTS = { ADD_USER_SAMPLE: 'ADD_USER_SAMPLE', REMOVE_USER_SAMPLE: 'REMOVE_USER_SAMPLE', CLEAR_USER_SAMPLES: 'CLEAR_USER_SAMPLES', }; ================================================ FILE: src/common/userSamples/userSamples.reducer.js ================================================ import { USER_SAMPLES_CONSTANTS } from './userSamples.constants'; export const userSamplesInitialState = []; export const userSamplesReducer = (state = userSamplesInitialState, action) => { switch (action.type) { case USER_SAMPLES_CONSTANTS.ADD_USER_SAMPLE: return [ ...state, action.payload, ]; case USER_SAMPLES_CONSTANTS.REMOVE_USER_SAMPLE: return state.filter((userSample) => userSample.id !== action.payload); case USER_SAMPLES_CONSTANTS.CLEAR_USER_SAMPLES: return userSamplesInitialState; default: return state; } }; ================================================ FILE: src/common/userSamples/userSamples.selectors.js ================================================ import * as R from 'ramda'; export const userSamplesSelector = R.path(['userSamples']); ================================================ FILE: src/common/window/index.js ================================================ export * from './window.actions'; export * from './window.reducer'; export * from './window.selectors'; export * from './window.constants'; ================================================ FILE: src/common/window/window.actions.js ================================================ import { WINDOW_CONSTANTS } from './window.constants'; export const setPresetPrompt = (isOpen) => ({ type: WINDOW_CONSTANTS.PRESET_PROMPT_OPEN, payload: isOpen, }); export const setPresetNameField = (val) => ({ type: WINDOW_CONSTANTS.SET_PRESET_NAME_FIELD, payload: val, }); export const showFlashMessage = (messageKey) => ({ type: WINDOW_CONSTANTS.SET_FLASH_MESSAGE, payload: messageKey, }); export const clearFlashMessage = () => ({ type: WINDOW_CONSTANTS.CLEAR_FLASH_MESSAGE, }); export const setCanInstall = (canInstall) => ({ type: WINDOW_CONSTANTS.SET_CAN_INSTALL, payload: canInstall, }); ================================================ FILE: src/common/window/window.constants.js ================================================ export const WINDOW_CONSTANTS = { PRESET_PROMPT_OPEN: 'PRESET_PROMPT_OPEN', SET_PRESET_NAME_FIELD: 'SET_PRESET_NAME_FIELD', SET_FLASH_MESSAGE: 'SET_FLASH_MESSAGE', CLEAR_FLASH_MESSAGE: 'CLEAR_FLASH_MESSAGE', SET_CAN_INSTALL: 'SET_CAN_INSTALL', }; export const FLASH_MESSAGES = { INSTALL_PWA: 'FLASH_MESSAGE_INSTALL_PWA', SAMPLE_LOAD_ERROR: 'SAMPLE_LOAD_ERROR', PRESET_SAVED: 'PRESET_SAVED', PRESET_DELETED: 'PRESET_DELETED', }; ================================================ FILE: src/common/window/window.reducer.js ================================================ import { WINDOW_CONSTANTS } from './window.constants'; export const windowInitialState = { presetPromptOpen: false, flashMessageKey: null, flashMessageVisible: false, canInstall: false, }; export const windowReducer = (state = windowInitialState, action) => { switch (action.type) { case WINDOW_CONSTANTS.PRESET_PROMPT_OPEN: return { ...state, presetPromptOpen: action.payload, }; case WINDOW_CONSTANTS.SET_FLASH_MESSAGE: return { ...state, flashMessageKey: action.payload, flashMessageVisible: true, }; case WINDOW_CONSTANTS.CLEAR_FLASH_MESSAGE: return { ...state, flashMessageVisible: false, }; case WINDOW_CONSTANTS.SET_CAN_INSTALL: return { ...state, canInstall: action.payload, }; default: return state; } }; ================================================ FILE: src/common/window/window.reducer.test.js ================================================ import { windowInitialState, windowReducer } from './window.reducer'; import { setPresetPrompt, showFlashMessage, clearFlashMessage, setCanInstall, } from './window.actions'; describe('setPresetPrompt', () => { test('should set presetPromptOpen to true', () => { const state = windowReducer(windowInitialState, setPresetPrompt(true)); expect(state.presetPromptOpen).toBe(true); }); }); describe('showFlashMessage', () => { test('should set flashMessageKey to a string value', () => { const state = windowReducer(windowInitialState, showFlashMessage('foobar')); expect(state.flashMessageKey).toBe('foobar'); expect(state.flashMessageVisible).toEqual(true); }); }); describe('clearFlashMessage', () => { test('should set flashMessageKey to null', () => { const state = windowReducer(windowInitialState, showFlashMessage('foobar')); const nullState = windowReducer(state, clearFlashMessage()); expect(nullState.flashMessageKey).toBe('foobar'); expect(nullState.flashMessageVisible).toEqual(false); }); }); describe('setCanInstall', () => { test('should set canInstall to a value', () => { const state = windowReducer(windowInitialState, setCanInstall(true)); expect(state.canInstall).toBe(true); }); }); ================================================ FILE: src/common/window/window.selectors.js ================================================ import * as R from 'ramda'; export const presetPromptOpenSelector = R.path(['window', 'presetPromptOpen']); export const flashMessageKeySelector = R.path(['window', 'flashMessageKey']); export const flashMessageVisibleSelector = R.path(['window', 'flashMessageVisible']); export const canInstallSelector = R.path(['window', 'canInstall']); ================================================ FILE: src/components/AddChannelButton/AddChannelButton.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { HoverButton } from '../design-system'; export const AddChannelButtonComponent = ({ newChannel }) => ( Add Channel + ); AddChannelButtonComponent.propTypes = { newChannel: PropTypes.func.isRequired, }; ================================================ FILE: src/components/AddChannelButton/AddChannelButton.container.js ================================================ import { connect } from 'react-redux'; import { compose } from 'recompose'; import { AddChannelButtonComponent } from './AddChannelButton.component'; import { newChannel } from '../../common'; const mapDispatchToProps = { newChannel, }; export const AddChannelButton = compose( connect(null, mapDispatchToProps), )(AddChannelButtonComponent); ================================================ FILE: src/components/AddChannelButton/index.js ================================================ export * from './AddChannelButton.container'; ================================================ FILE: src/components/App.jsx ================================================ import React from 'react'; import { ThemeProvider } from 'styled-components'; import theme from '../styles/theme'; import globalStyles from '../styles/globalStyles'; import { Box, ChannelList, ChannelHeader, ChannelControls, MasterControls, Branding, GithubLink, FlashMessage, InstallButton, } from '.'; globalStyles(); const App = () => (
    ); export default App; ================================================ FILE: src/components/BPMInput/BPMInput.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import theme from '../../styles/theme'; import { Box, Label, TextInput, Button, } from '../design-system'; const ShinyBox = Box.extend` background: linear-gradient(190deg, #19191D 0%, #303036 50%,#0a0e0a 51%, #29292D 100%); transition: border-color 0.2s; &:hover { border-color: ${theme.colors.gray}; } `; const BPMButton = Button.extend` &:active { background-color: rgba(255, 255, 255, 0.2); } `; export const BPMInputComponent = ({ bpm, setBPM }) => ( { setBPM(parseInt(e.target.value, 10)); }} /> { setBPM(bpm + 1); }} aria-label="Increase beat per minute" > { setBPM(bpm - 1); }} aria-label="Decrease beat per minute" > ); BPMInputComponent.propTypes = { bpm: PropTypes.number.isRequired, setBPM: PropTypes.func.isRequired, }; ================================================ FILE: src/components/BPMInput/BPMInput.container.js ================================================ import { connect } from 'react-redux'; import { compose } from 'recompose'; import { BPMInputComponent } from './BPMInput.component'; import { bpmInputSelectors } from './BPMInput.selectors'; import { setBPM } from '../../common'; const mapDispatchToProps = { setBPM, }; export const BPMInput = compose( connect(bpmInputSelectors, mapDispatchToProps), )(BPMInputComponent); ================================================ FILE: src/components/BPMInput/BPMInput.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { bpmSelector } from '../../common'; export const bpmInputSelectors = createStructuredSelector({ bpm: bpmSelector, }); ================================================ FILE: src/components/BPMInput/index.js ================================================ export * from './BPMInput.container'; ================================================ FILE: src/components/Branding.jsx ================================================ import React from 'react'; import styled from 'styled-components'; import theme from '../styles/theme'; import { Logo } from './Logo.component'; import { Box } from './design-system'; const HeaderText = styled.h1` color: ${theme.colors.steel}; font-size: 1em; font-weight: 600; margin-left: 1.5em; line-height: 1.2em; margin-top: 0.5em; max-width: 4em; `; export const Branding = () => ( Web Drum Sequencer ); ================================================ FILE: src/components/Channel/Channel.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import * as R from 'ramda'; import { Toggles } from '../Toggles'; import { Box, Text, Image, } from '../design-system'; import { RemoveButton } from './RemoveButton.component'; import { HitButton } from './HitButton.component'; import { MuteSolo } from '../MuteSolo'; import construction from '../../assets/images/construction-light.svg'; import samples from '../../samples.config'; const getSampleName = (sampleURL) => { const maybeName = R.find(R.propEq('url', sampleURL))(samples); return maybeName ? maybeName.name : sampleURL; }; const ChannelBox = Box.extend` outline: none; &.draggable-source--is-dragging { opacity: 0.2; } &.draggable-mirror { opacity: 0.9; z-index: 10; } `; const MoveImage = Image.extend` cursor: move; opacity: 0.2; transition: opacity 0.1s; &:hover, &:focus, &:active { opacity: 0.3; } `; export const ChannelComponent = ({ channel, onPressRemove, notes, pattern, onPressHitButton, onTouchChannel, selectedChannelId, }) => { const sampleName = getSampleName(channel.sample); return ( {sampleName} ); }; ChannelComponent.propTypes = { notes: PropTypes.objectOf(PropTypes.array).isRequired, channel: PropTypes.shape({ sample: PropTypes.string, id: PropTypes.string.isRequired, }).isRequired, onPressRemove: PropTypes.func.isRequired, pattern: PropTypes.number.isRequired, onPressHitButton: PropTypes.func.isRequired, onTouchChannel: PropTypes.func.isRequired, selectedChannelId: PropTypes.string.isRequired, }; ================================================ FILE: src/components/Channel/Channel.container.js ================================================ import { connect } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { ChannelComponent } from './Channel.component'; import { channelSelectors } from './Channel.selectors'; import { deleteChannel, setSelectedChannel, } from '../../common'; import { playNoteNow } from '../../services/audioScheduler'; const mapDispatchToProps = { deleteChannel, setSelectedChannel, }; const handlers = withHandlers({ onSelectSample: (props) => (sample) => { const { loadAndSetChannelSample: scs, channel } = props; scs(channel.id, sample.value); }, onTouchChannel: (props) => () => { const { channel, setSelectedChannel: sscs } = props; sscs(channel.id); }, onPressRemove: (props) => () => { const { channel, channels, selectedChannelId, deleteChannel: dc, } = props; dc(channel.id, channels, selectedChannelId); }, onPressHitButton: (props) => () => { const { channel } = props; playNoteNow(channel); }, }); export const Channel = compose( connect(channelSelectors, mapDispatchToProps), handlers, )(ChannelComponent); ================================================ FILE: src/components/Channel/Channel.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { channelsSelector, notesSelector, patternSelector, selectedChannelSelector, } from '../../common'; export const channelSelectors = createStructuredSelector({ channels: channelsSelector, notes: notesSelector, pattern: patternSelector, selectedChannelId: selectedChannelSelector, }); ================================================ FILE: src/components/Channel/HitButton.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { HoverButton } from '../design-system'; export const HitButton = ({ channel, onMouseDown }) => ( e.preventDefault()} aria-label={`Play ${channel.sample.name}`} touch-action="manipulation" /> ); HitButton.propTypes = { channel: PropTypes.shape({ sample: PropTypes.string.isRequired, gain: PropTypes.number.isRequired, }).isRequired, onMouseDown: PropTypes.func.isRequired, }; ================================================ FILE: src/components/Channel/RemoveButton.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { HoverButton } from '../design-system'; import theme from '../../styles/theme'; export const RemoveButton = ({ onClick }) => ( ); RemoveButton.propTypes = { onClick: PropTypes.func.isRequired, }; ================================================ FILE: src/components/Channel/index.js ================================================ export * from './Channel.container'; ================================================ FILE: src/components/ChannelControls/ChannelControls.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { detuneSupported, } from '../../services/featureChecks'; import { Box } from '../design-system'; import { SampleSelect } from '../SampleSelect'; import { InfoKnob } from '../InfoKnob.component'; import { LabelBox } from '../LabelBox'; const ControlCluster = Box.extend` background-color: ${({ theme }) => theme.colors.darkGray}; border-radius: 0.3rem; display: flex; margin: 0.5rem; align-items: flex-start; padding: 0.8rem; `; export const ChannelControlsComponent = ({ channel, onSetGain, onSetPan, onSetChannelPitchCoarse, onSetReverb, }) => ( {detuneSupported && ( )} ); ChannelControlsComponent.propTypes = { channel: PropTypes.shape({ id: PropTypes.string.isRequired, pitchCoarse: PropTypes.number, reverb: PropTypes.number, gain: PropTypes.number, pan: PropTypes.number, }).isRequired, onSetGain: PropTypes.func.isRequired, onSetPan: PropTypes.func.isRequired, onSetChannelPitchCoarse: PropTypes.func.isRequired, onSetReverb: PropTypes.func.isRequired, }; ================================================ FILE: src/components/ChannelControls/ChannelControls.container.js ================================================ import { connect } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { ChannelControlsComponent } from './ChannelControls.component'; import { channelControlsSelectors } from './ChannelControls.selectors'; import { setChannelGain, setChannelPitchCoarse, setChannelPan, setChannelReverb, } from '../../common'; const mapDispatchToProps = { setChannelGain, setChannelPan, setChannelPitchCoarse, setChannelReverb, }; const handlers = withHandlers({ onSetGain: (props) => (e) => { const { setChannelGain: setChannelGainConnected, channel } = props; setChannelGainConnected(channel.id, e.target.value / 100); }, onSetPan: (props) => (e) => { const { setChannelPan: setChannelPanConnected, channel } = props; setChannelPanConnected(channel.id, e.target.value); }, onSetChannelPitchCoarse: (props) => (e) => { const { setChannelPitchCoarse: setChannelPitchCoarseConnected, channel } = props; setChannelPitchCoarseConnected(channel.id, e.target.value); }, onSetReverb: (props) => (e) => { const { setChannelReverb: setChannelReverbConnected, channel } = props; setChannelReverbConnected(channel.id, e.target.value); }, }); export const ChannelControls = compose( connect(channelControlsSelectors, mapDispatchToProps), handlers, )(ChannelControlsComponent); ================================================ FILE: src/components/ChannelControls/ChannelControls.selectors.js ================================================ import { createStructuredSelector, createSelector } from 'reselect'; import { channelsSelector, selectedChannelSelector } from '../../common'; const channelSelector = createSelector( channelsSelector, selectedChannelSelector, (channels, selectedChannelID) => { const selectedChannel = channels.find( (channel) => channel.id === selectedChannelID, ); return typeof selectedChannel === 'undefined' ? channels[0] : selectedChannel; }, ); export const channelControlsSelectors = createStructuredSelector({ channel: channelSelector, }); ================================================ FILE: src/components/ChannelControls/index.js ================================================ export * from './ChannelControls.container'; ================================================ FILE: src/components/ChannelHeader/ChannelHeader.component.jsx ================================================ import React from 'react'; import { Box } from '../design-system'; import { ChannelHeaderLabel } from './ChannelHeaderLabel.component'; import { Marker } from '../Marker'; export const ChannelHeader = () => ( Channels Hit 1 2 3 4 ); ================================================ FILE: src/components/ChannelHeader/ChannelHeaderLabel.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Box, Text } from '../design-system'; const HeaderText = Text.extend` text-transform: uppercase; `; export const ChannelHeaderLabel = ({ children, centerText, ...restProps }) => ( {children} ); ChannelHeaderLabel.propTypes = { children: PropTypes.node.isRequired, centerText: PropTypes.bool, }; ChannelHeaderLabel.defaultProps = { centerText: false, }; ================================================ FILE: src/components/ChannelHeader/index.js ================================================ export * from './ChannelHeader.component'; ================================================ FILE: src/components/ChannelList/ChannelList.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Sortable } from '@shopify/draggable'; import { Box } from '../design-system'; import { Channel } from '../Channel'; import { AddChannelButton } from '../AddChannelButton'; const ChannelListBox = Box.extend` outline: none; `; export class ChannelListComponent extends React.Component { componentDidMount() { // eslint-disable-next-line const sortable = new Sortable([this.channelContainer], { draggable: '.wds-draggable', handle: '.wds-channel-handle', mirror: { constrainDimensions: true, }, }); sortable.on('sortable:stop', ({ oldIndex, newIndex }) => { const { onUpdateChannelOrder } = this.props; onUpdateChannelOrder(oldIndex, newIndex); }); } render() { const { channels } = this.props; return ( { this.channelContainer = el; }}> {channels.map(channel => )} ); } } ChannelListComponent.propTypes = { channels: PropTypes.arrayOf(PropTypes.object).isRequired, onUpdateChannelOrder: PropTypes.func.isRequired, }; ================================================ FILE: src/components/ChannelList/ChannelList.container.js ================================================ import { connect } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { ChannelListComponent } from './ChannelList.component'; import { channelListSelectors } from './ChannelList.selectors'; import { toggleNote, updateChannelOrder } from '../../common'; const mapDispatchToProps = { toggleNote, updateChannelOrder, }; const handlers = withHandlers({ onUpdateChannelOrder: (props) => (oldIndex, newIndex) => { const { updateChannelOrder: updateChannelOrderConnected } = props; updateChannelOrderConnected(oldIndex, newIndex); }, }); export const ChannelList = compose( connect(channelListSelectors, mapDispatchToProps), handlers, )(ChannelListComponent); ================================================ FILE: src/components/ChannelList/ChannelList.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { channelsSelector } from '../../common'; export const channelListSelectors = createStructuredSelector({ channels: channelsSelector, }); ================================================ FILE: src/components/ChannelList/index.js ================================================ export * from './ChannelList.container'; ================================================ FILE: src/components/FancyButton.component.jsx ================================================ import { variant } from 'styled-system'; import { Button } from './design-system'; const fancyButtonStyle = variant({ key: 'fancyButtons', }); export const FancyButton = Button.extend` ${fancyButtonStyle} transition: box-shadow 0.2s, transform 0.2s; text-transform: uppercase; height: calc(100% - 4px); &:active: { transform: translateY(0.3em); } `; ================================================ FILE: src/components/FlashMessage/FlashMessage.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import * as animol from 'animol'; import { FLASH_MESSAGES } from '../../common'; import { Box, HoverButton, } from '../design-system'; import { SampleLoadError } from '../SampleLoadError.component'; import { PresetSaved } from '../PresetSaved.component'; import { PresetDeleted } from '../PresetDeleted.component'; import { timedCallback } from '../timedCallback.hoc'; const getMessageComponent = (messageKey) => { switch (messageKey) { case FLASH_MESSAGES.SAMPLE_LOAD_ERROR: return SampleLoadError; case FLASH_MESSAGES.PRESET_SAVED: return PresetSaved; case FLASH_MESSAGES.PRESET_DELETED: return PresetDeleted; default: return undefined; } }; export class FlashMessageComponent extends React.Component { componentDidMount() { this.animateBox(); } componentDidUpdate() { this.animateBox(); } animateBox() { const { flashMessageVisible, messageKey } = this.props; if (messageKey && flashMessageVisible) { this.flashBox.style.display = 'block'; animol.css( this.flashBox, 500, { opacity: 0, transform: { translateY: '10%' } }, { opacity: 1, transform: { translateY: '0%' } }, animol.Easing.easeOutCubic, ); } else if (messageKey) { const animation = animol.css( this.flashBox, 200, { opacity: 1 }, { opacity: 0 }, animol.Easing.easeInCubic, ); animation.promise.then(() => { this.flashBox.style.display = 'none'; }); } } render() { const { messageKey, onDismiss } = this.props; const Message = getMessageComponent(messageKey); const DisappearingMessage = timedCallback(onDismiss, 6000)(Message); return Message ? ( { this.flashBox = comp; }} opacity="0" > { this.flashMessage = comp; }} p={4} > ) : null; } } FlashMessageComponent.propTypes = { messageKey: PropTypes.string, onDismiss: PropTypes.func.isRequired, flashMessageVisible: PropTypes.bool, }; FlashMessageComponent.defaultProps = { messageKey: null, flashMessageVisible: false, }; ================================================ FILE: src/components/FlashMessage/FlashMessage.container.js ================================================ import { connect } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { FlashMessageComponent } from './FlashMessage.component'; import { clearFlashMessage } from '../../common'; import { flashMessageSelectors } from './FlashMessage.selectors'; const mapDispatchToProps = { clearFlashMessage, }; const handlers = withHandlers({ onDismiss: (props) => () => { const { clearFlashMessage: connectedClearFlashMessage } = props; connectedClearFlashMessage(); }, }); export const FlashMessage = compose( connect(flashMessageSelectors, mapDispatchToProps), handlers, )(FlashMessageComponent); ================================================ FILE: src/components/FlashMessage/FlashMessage.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { flashMessageKeySelector, flashMessageVisibleSelector } from '../../common'; export const flashMessageSelectors = createStructuredSelector({ messageKey: flashMessageKeySelector, flashMessageVisible: flashMessageVisibleSelector, }); ================================================ FILE: src/components/FlashMessage/index.js ================================================ export * from './FlashMessage.container'; ================================================ FILE: src/components/GithubLink.component.jsx ================================================ import React from 'react'; import { Image, HoverLink, Text, Box, } from './design-system'; import octocat from '../assets/images/github.svg'; export const GithubLink = () => ( Github Github Logo ); ================================================ FILE: src/components/InfoKnob.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Knob } from './Knob.component'; import { ControlLabel, Box } from './design-system'; export const InfoKnob = ({ label, minLabel, maxLabel, ...rest }) => ( {label} {minLabel} {maxLabel} ); InfoKnob.propTypes = { label: PropTypes.string.isRequired, minLabel: PropTypes.string.isRequired, maxLabel: PropTypes.string.isRequired, }; ================================================ FILE: src/components/InstallButton.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { HoverButton } from './design-system'; import { promptToInstall } from '../services/pwaInstall'; import { canInstallSelector } from '../common/window/window.selectors'; const isStandalone = window.matchMedia('(display-mode: standalone)').matches; const InstallButtonComponent = ({ canInstall }) => (canInstall && !isStandalone ? ( { promptToInstall(); }} width="auto" bg="blue" color="white" hoverColor="nearWhite" hoverBg="darkBlue" transitionSpeed="0.2s" p="0.6rem 1.2rem" > INSTALL ) : null); InstallButtonComponent.propTypes = { canInstall: PropTypes.bool.isRequired, }; const mapStateToProps = state => ({ canInstall: canInstallSelector(state), }); export const InstallButton = connect(mapStateToProps)(InstallButtonComponent); ================================================ FILE: src/components/Knob.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; // import '../assets/js/webcomponents-lite'; import knobImage from '../assets/images/maschine-50.png'; import '../assets/js/webaudio-controls'; export class Knob extends React.Component { componentDidMount() { const { onChange } = this.props; this.knob.addEventListener('input', onChange); } componentDidUpdate() { const { value } = this.props; this.knob.setValue(value); } render() { const { size, value, ...rest } = this.props; return ( { this.knob = element; }} src={knobImage} sprites="50" min="0" max="100" width={size} height={size} value={value} {...rest} /> ); } } Knob.propTypes = { onChange: PropTypes.func.isRequired, size: PropTypes.number.isRequired, value: PropTypes.number.isRequired, }; ================================================ FILE: src/components/LabelBox.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import theme from '../styles/theme'; import { Box, Text } from './design-system'; const HoverBox = Box.extend` transition: border-color 0.2s; &:hover { ${({ hoverEffect }) => ( hoverEffect ? `border-color: ${theme.colors.gray};` : '')} } `; export const LabelBox = ({ label, children, hoverEffect }) => ( {label} {children} ); LabelBox.propTypes = { label: PropTypes.string.isRequired, children: PropTypes.node.isRequired, hoverEffect: PropTypes.bool, }; LabelBox.defaultProps = { hoverEffect: false, }; ================================================ FILE: src/components/Logo.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import theme from '../styles/theme'; export const Logo = ({ color, width }) => ( ); Logo.propTypes = { width: PropTypes.string.isRequired, color: PropTypes.string.isRequired, }; ================================================ FILE: src/components/Marker/Marker.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Box } from '../design-system'; import { getCurrentBeat } from '../../services/audioContext'; import theme from '../../styles/theme'; const Container = Box.extend` overflow: hidden; `; export class MarkerComponent extends React.PureComponent { componentDidMount() { this.updateMarker.bind(this); this.updateMarker(); } updateMarker() { const { playing, startTime, bpm } = this.props; if (playing) { const currentBeat = getCurrentBeat(bpm, startTime); const progress = (currentBeat - 1) / 4 * 100; this.marker.style.width = `${progress}%`; } window.requestAnimationFrame(() => { this.updateMarker(); }); } render() { const { children } = this.props; return (
    { this.marker = ref; }} style={{ height: '100%', backgroundColor: theme.colors.darkGray, position: 'absolute', width: 0, }} /> {children} ); } } MarkerComponent.defaultProps = { startTime: null, }; MarkerComponent.propTypes = { startTime: PropTypes.number, bpm: PropTypes.number.isRequired, playing: PropTypes.bool.isRequired, children: PropTypes.node.isRequired, }; ================================================ FILE: src/components/Marker/Marker.container.js ================================================ import { connect } from 'react-redux'; import { compose } from 'recompose'; import { MarkerComponent } from './Marker.component'; import { markerSelectors } from './Marker.selectors'; export const Marker = compose( connect(markerSelectors, null), )(MarkerComponent); ================================================ FILE: src/components/Marker/Marker.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { bpmSelector, startTimeSelector, playingSelector, } from '../../common'; export const markerSelectors = createStructuredSelector({ bpm: bpmSelector, startTime: startTimeSelector, playing: playingSelector, }); ================================================ FILE: src/components/Marker/index.js ================================================ export * from './Marker.container'; ================================================ FILE: src/components/MasterControls/MasterControls.component.jsx ================================================ import React from 'react'; import { Box } from '../design-system'; import { PlayButton } from '../PlayButton'; import { BPMInput } from '../BPMInput'; import { PresetSelector } from '../PresetSelector'; import { PatternSelector } from '../PatternSelector'; import { SwingControl } from '../SwingControl'; import { VolumeMeter } from '../VolumeMeter.component'; export const MasterControls = () => ( ); ================================================ FILE: src/components/MasterControls/index.js ================================================ export * from './MasterControls.component'; ================================================ FILE: src/components/Modal.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Box } from './design-system'; export const Modal = ({ children, show }) => ( {children} ); Modal.propTypes = { children: PropTypes.node.isRequired, show: PropTypes.bool.isRequired, }; ================================================ FILE: src/components/MuteSolo/MuteSolo.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Box, Button } from '../design-system'; const MSButton = Button.extend` width: 0.8rem; height: 0.8rem; padding: 0; border-radius: 100%; transition: all 0.1s; `; export const MuteSoloComponent = ({ onPressMuted, onPressSolo, channel }) => ( ); MuteSoloComponent.propTypes = { onPressMuted: PropTypes.func.isRequired, onPressSolo: PropTypes.func.isRequired, channel: PropTypes.shape({ solo: PropTypes.bool, muted: PropTypes.bool, }).isRequired, }; ================================================ FILE: src/components/MuteSolo/MuteSolo.container.js ================================================ import { connect } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { MuteSoloComponent } from './MuteSolo.component'; import { setChannelMuted, setChannelSolo, } from '../../common'; const mapDispatchToProps = { setChannelMuted, setChannelSolo, }; const handlers = withHandlers({ onPressMuted: (props) => () => { const { channel, setChannelMuted: setChannelMutedConnected } = props; setChannelMutedConnected(channel.id, !channel.muted); }, onPressSolo: (props) => () => { const { channel, setChannelSolo: setChannelSoloConnected } = props; setChannelSoloConnected(channel.id, !channel.solo); }, }); export const MuteSolo = compose( connect(null, mapDispatchToProps), handlers, )(MuteSoloComponent); ================================================ FILE: src/components/MuteSolo/index.js ================================================ export * from './MuteSolo.container'; ================================================ FILE: src/components/PatternSelector/PatternSelector.component.jsx ================================================ import React from 'react'; import * as R from 'ramda'; import PropTypes from 'prop-types'; import { LabelBox } from '../LabelBox'; import { HoverButton } from '../design-system'; export const PatternSelectorComponent = ({ onSelectPattern, pattern }) => { const buttons = R.range(0, 8).map(buttonNumber => ( { onSelectPattern(buttonNumber); }} fontWeight="500" fontSize="0.7em" color="rgba(0,0,0,0.5)" activeBg="primaryDark" disabled={pattern === buttonNumber} aria-label={`Enable pattern ${buttonNumber}`} lineHeight="1.4em" > {buttonNumber + 1} )); return ( {buttons} ); }; PatternSelectorComponent.propTypes = { pattern: PropTypes.number.isRequired, onSelectPattern: PropTypes.func.isRequired, }; ================================================ FILE: src/components/PatternSelector/PatternSelector.container.js ================================================ import { connect } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { PatternSelectorComponent } from './PatternSelector.component'; import { patternSelectorSelectors } from './PatternSelector.selectors'; import { setPattern } from '../../common'; const mapDispatchToProps = { setPattern, }; export const PatternSelector = compose( connect(patternSelectorSelectors, mapDispatchToProps), withHandlers({ onSelectPattern: (props) => (patternIndex) => { const { setPattern: connectedSetPattern, } = props; connectedSetPattern(patternIndex); }, }), )(PatternSelectorComponent); ================================================ FILE: src/components/PatternSelector/PatternSelector.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { patternSelector } from '../../common'; export const patternSelectorSelectors = createStructuredSelector({ pattern: patternSelector, }); ================================================ FILE: src/components/PatternSelector/index.js ================================================ export * from './PatternSelector.container'; ================================================ FILE: src/components/PlayButton/PlayButton.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { FancyButton } from '../FancyButton.component'; import { Text } from '../design-system'; const StyledPlayButton = FancyButton.extend` margin-bottom: 1px; width: 8rem; display: flex; flex-direction: row; align-items: center; justify-content: center; `; export function PlayButtonComponent({ startPlaybackAndResume, stopPlayback, playing, }) { return playing ? ( Stop ) : ( PLAY ); } PlayButtonComponent.propTypes = { startPlaybackAndResume: PropTypes.func.isRequired, stopPlayback: PropTypes.func.isRequired, playing: PropTypes.bool.isRequired, }; ================================================ FILE: src/components/PlayButton/PlayButton.container.js ================================================ import { connect } from 'react-redux'; import { compose } from 'recompose'; import { PlayButtonComponent } from './PlayButton.component'; import { playButtonSelectors } from './PlayButton.selectors'; import { startPlaybackAndResume, stopPlayback } from '../../common'; const mapDispatchToProps = { startPlaybackAndResume, stopPlayback, }; export const PlayButton = compose( connect(playButtonSelectors, mapDispatchToProps), )(PlayButtonComponent); ================================================ FILE: src/components/PlayButton/PlayButton.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { playingSelector } from '../../common'; export const playButtonSelectors = createStructuredSelector({ playing: playingSelector, }); ================================================ FILE: src/components/PlayButton/index.js ================================================ export * from './PlayButton.container'; ================================================ FILE: src/components/PresetDeleted.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Box, Text, HoverButton, } from './design-system'; export const PresetDeleted = ({ onDismiss }) => ( User preset deleted. OK ); PresetDeleted.propTypes = { onDismiss: PropTypes.func.isRequired, }; ================================================ FILE: src/components/PresetSaved.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Box, Text, HoverButton, } from './design-system'; export const PresetSaved = ({ onDismiss }) => ( User preset saved. OK ); PresetSaved.propTypes = { onDismiss: PropTypes.func.isRequired, }; ================================================ FILE: src/components/PresetSelector/PresetSelector.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import Select from 'react-select'; import * as R from 'ramda'; import theme from '../../styles/theme'; import { Box, Text } from '../design-system'; import { SavePresetModal } from '../SavePresetModal'; export const PresetSelectorComponent = ({ onSelectPreset, presets, currentPreset, isEdited, userPresets, }) => { const defaultPresetOptions = presets.map(preset => ({ label: preset.name, value: preset, })); const userPresetOptions = userPresets.map(preset => ({ label: preset.name, value: preset, })); const groupedOptions = [ { label: 'Default', options: defaultPresetOptions, }, { label: 'User', options: userPresetOptions, }, { label: 'Memory', options: [ { label: 'Save As...', value: 'SAVE_PRESET_AS', }, { label: `Save “${currentPreset.name}”`, value: 'SAVE_PRESET', disabled: !isEdited || (defaultPresetOptions.find( option => option.label === currentPreset.name, ) !== undefined), }, { label: `Delete “${currentPreset.name}”`, value: 'DELETE_PRESET', disabled: (defaultPresetOptions.find( option => option.label === currentPreset.name, ) !== undefined), }, ], }, ]; let selectedOption = [...defaultPresetOptions, ...userPresetOptions].find( option => option.label === currentPreset.name, ); if (isEdited && selectedOption) { selectedOption = R.clone(selectedOption); selectedOption.label += ' *'; } return ( PRESETS { if (choice.value === 'CHOOSE_FILE') { openFileInput.current.click(); } else { onSelectSample(choice); } }} value={currentOption} isSearchable={false} styles={{ container: styles => ({ ...styles, height: '3rem', }), control: styles => ({ ...styles, backgroundColor: 'black', border: `2px solid ${theme.colors.steel}`, height: '100%', borderRadius: '0.5em', }), singleValue: styles => ({ ...styles, color: theme.colors.nearWhite, opacity: channel.sampleLoaded ? 1 : 0.3, }), menu: styles => ({ ...styles, fontSize: '0.8rem', width: '16rem', }), option: styles => ({ ...styles, paddingTop: '0.2em', paddingBottom: '0.2em', }), }} /> ); }; SampleSelectComponent.propTypes = { onSelectSample: PropTypes.func.isRequired, onSampleFileChosen: PropTypes.func.isRequired, channel: PropTypes.shape({ sample: PropTypes.string, sampleLoaded: PropTypes.bool, id: PropTypes.string.isRequired, }).isRequired, userSamples: PropTypes.arrayOf(PropTypes.string).isRequired, }; ================================================ FILE: src/components/SampleSelect/SampleSelect.container.js ================================================ import { connect } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { SampleSelectComponent } from './SampleSelect.component'; import { saveUserSample, loadAndSetChannelSample } from '../../common'; import { sampleSelectSelectors } from './SampleSelect.selectors'; const mapDispatchToProps = { loadAndSetChannelSample, saveUserSample, }; const handlers = withHandlers({ onSelectSample: (props) => (sample) => { const { loadAndSetChannelSample: connectedSetChannelSample, channel } = props; connectedSetChannelSample(channel.id, sample.value); }, onSampleFileChosen: (props) => (e) => { const { saveUserSample: connectedSaveUserSample, channel } = props; connectedSaveUserSample(channel.id, e.target.files); }, }); export const SampleSelect = compose( connect(sampleSelectSelectors, mapDispatchToProps), handlers, )(SampleSelectComponent); ================================================ FILE: src/components/SampleSelect/SampleSelect.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { channelsSelector, notesSelector, patternSelector, selectedChannelSelector, userSamplesSelector, } from '../../common'; export const sampleSelectSelectors = createStructuredSelector({ channels: channelsSelector, notes: notesSelector, pattern: patternSelector, selectedChannelId: selectedChannelSelector, userSamples: userSamplesSelector, }); ================================================ FILE: src/components/SampleSelect/index.js ================================================ export * from './SampleSelect.container'; ================================================ FILE: src/components/SavePresetModal/SavePresetModal.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { TextInput, Text, Button, HoverButton, Form, } from '../design-system'; import { Modal } from '../Modal.component'; import theme from '../../styles/theme'; export class SavePresetModalComponent extends React.Component { componentDidUpdate() { const { presetPromptOpen } = this.props; if (presetPromptOpen) { this.nameInput.focus(); } } render() { const { presetPromptOpen, nameField, onChangeNameField, onClose, onSubmit, error, } = this.props; return (
    SAVE
    ); } } SavePresetModalComponent.propTypes = { onClose: PropTypes.func.isRequired, presetPromptOpen: PropTypes.bool.isRequired, onChangeNameField: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, nameField: PropTypes.string.isRequired, error: PropTypes.string, }; SavePresetModalComponent.defaultProps = { error: null, }; ================================================ FILE: src/components/SavePresetModal/SavePresetModal.container.js ================================================ import { connect } from 'react-redux'; import { compose, withHandlers, withState } from 'recompose'; import { SavePresetModalComponent } from './SavePresetModal.component'; import { savePresetModalSelectors } from './SavePresetModal.selectors'; import { setPresetPrompt, doSavePresetAs, } from '../../common'; import defaultPresets from '../../presets'; const mapDispatchToProps = { setPresetPrompt, doSavePresetAs, }; const isNameUnique = (proposedName, userPresets) => [...defaultPresets, ...userPresets].find( (preset) => preset.name === proposedName, ) === undefined; const handlers = { onChangeNameField: ({ updateNameField, setError }) => (event) => { if (event.target.value.length < 32) { updateNameField(event.target.value); setError(null); } }, onClose: (props) => () => { const { setPresetPrompt: connectedSetPresetPrompt, updateNameField, } = props; updateNameField(''); connectedSetPresetPrompt(false); }, onSubmit: (props) => (event) => { event.preventDefault(); const { setPresetPrompt: connectedSetPresetPrompt, doSavePresetAs: connectedDoSavePresetAs, updateNameField, nameField, setError, userPresets, } = props; if (nameField.length < 1) { setError('Min length 1'); } else if (nameField.length > 32) { setError('Max length 32'); } else if (!isNameUnique(nameField, userPresets)) { setError('Must be unique'); } else { connectedSetPresetPrompt(false); connectedDoSavePresetAs(nameField); updateNameField(''); setError(null); } }, }; export const SavePresetModal = compose( connect(savePresetModalSelectors, mapDispatchToProps), withState('nameField', 'updateNameField', ''), withState('error', 'setError', null), withHandlers(handlers), )(SavePresetModalComponent); ================================================ FILE: src/components/SavePresetModal/SavePresetModal.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { presetPromptOpenSelector, currentStateSelector, userPresetsSelector, } from '../../common'; export const savePresetModalSelectors = createStructuredSelector({ userPresets: userPresetsSelector, presetPromptOpen: presetPromptOpenSelector, currentState: currentStateSelector, }); ================================================ FILE: src/components/SavePresetModal/index.js ================================================ export * from './SavePresetModal.container'; ================================================ FILE: src/components/SwingControl/SwingControl.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Knob } from '../Knob.component'; import { Box, Text } from '../design-system'; const LabelText = Text.extend` transform: translateY(-0.3em); `; export const SwingControlComponent = ({ onSetSwing, swing, }) => ( SWING ); SwingControlComponent.propTypes = { onSetSwing: PropTypes.func.isRequired, swing: PropTypes.number.isRequired, }; ================================================ FILE: src/components/SwingControl/SwingControl.container.js ================================================ import { connect } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { SwingControlComponent } from './SwingControl.component'; import { swingControlSelectors } from './SwingControl.selectors'; import { setSwing } from '../../common'; const mapDispatchToProps = { setSwing }; const handlers = withHandlers({ onSetSwing: (props) => (e) => { const { setSwing: setSwingConnected } = props; setSwingConnected(e.target.value); }, }); export const SwingControl = compose( connect(swingControlSelectors, mapDispatchToProps), handlers, )(SwingControlComponent); ================================================ FILE: src/components/SwingControl/SwingControl.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { swingSelector } from '../../common'; export const swingControlSelectors = createStructuredSelector({ swing: swingSelector, }); ================================================ FILE: src/components/SwingControl/index.js ================================================ export * from './SwingControl.container'; ================================================ FILE: src/components/Toggles/Toggle.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import * as ss from 'styled-system'; import { Box } from '../design-system'; import theme from '../../styles/theme'; const gradient = `linear-gradient(180deg, ${theme.colors.primary} 0%, ${theme.colors.secondary} 100%);`; const BeatButton = styled.button` ${ss.color} ${ss.space} ${ss.width} ${ss.height} ${ss.borders} ${ss.borderRadius} padding: 0; outline: none; transition: background-color 0.1s; position: relative; background: ${({ isActive }) => (isActive ? gradient : theme.colors.darkGray)} &:focus { box-shadow: 0 0 5px 5px rgba(100, 180, 255, 0.5); } `; BeatButton.defaultProps = { border: 'none', borderRadius: '100%', }; export const Toggle = ({ isActive, onClick, beat }) => ( ); Toggle.propTypes = { isActive: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, beat: PropTypes.number.isRequired, }; ================================================ FILE: src/components/Toggles/ToggleGroup.component.jsx ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Box } from '../design-system'; export const ToggleGroup = ({ children }) => ( {children} ); ToggleGroup.propTypes = { children: PropTypes.node.isRequired, }; ================================================ FILE: src/components/Toggles/Toggles.component.jsx ================================================ import React from 'react'; import * as R from 'ramda'; import PropTypes from 'prop-types'; import { Box } from '../design-system'; import { Toggle } from './Toggle.component'; import { ToggleGroup } from './ToggleGroup.component'; const isActive = (notes, beat) => notes.find(note => note.beat === beat) !== undefined; const sixteenthNotes = R.range(0, 16); export class TogglesComponent extends React.PureComponent { render() { const { notes, channelID, toggleNote, bpm, playing, pattern, } = this.props; const toggles = sixteenthNotes.map((index) => { const beat = 1 + index / 4; return ( { toggleNote(channelID, pattern, beat); }} bpm={bpm} playing={playing} beat={beat} /> ); }); const toggleGroups = R.splitEvery(4, toggles); return ( {toggleGroups.map((toggleGroup, i) => ( {toggleGroup} ))} ); } } TogglesComponent.propTypes = { notes: PropTypes.arrayOf(PropTypes.object).isRequired, channelID: PropTypes.string.isRequired, toggleNote: PropTypes.func.isRequired, bpm: PropTypes.number.isRequired, playing: PropTypes.bool.isRequired, pattern: PropTypes.number.isRequired, }; ================================================ FILE: src/components/Toggles/Toggles.container.js ================================================ import { connect } from 'react-redux'; import { compose } from 'recompose'; import { TogglesComponent } from './Toggles.component'; import { toggleNote } from '../../common'; import { togglesSelectors } from './Toggles.selectors'; const mapDispatchToProps = { toggleNote, }; export const Toggles = compose( connect(togglesSelectors, mapDispatchToProps), )(TogglesComponent); ================================================ FILE: src/components/Toggles/Toggles.selectors.js ================================================ import { createStructuredSelector } from 'reselect'; import { bpmSelector, playingSelector, patternSelector, } from '../../common'; export const togglesSelectors = createStructuredSelector({ bpm: bpmSelector, playing: playingSelector, pattern: patternSelector, }); ================================================ FILE: src/components/Toggles/index.js ================================================ export * from './Toggles.container'; export * from './Toggle.component'; ================================================ FILE: src/components/VolumeMeter.component.jsx ================================================ import React, { useEffect, useRef } from 'react'; import { Box } from './design-system'; import { getVolume } from '../services/audioAnalyzer'; const DECAY = 0.95; let prevVal = 0; export const VolumeMeter = () => { const ref = useRef(); function updateVolumeMeter() { if (ref.current) { const currentVolume = getVolume(); // Decay slowly const isIncreasing = currentVolume > prevVal; let meteredVolume; if (isIncreasing) { meteredVolume = currentVolume; } else { meteredVolume = currentVolume + (prevVal - currentVolume) * DECAY; } prevVal = meteredVolume; // Convert to CSS const percent = Math.min(Math.round(meteredVolume * 75), 50); const color = `hsla(16, 100%, ${percent}%, 1)`; ref.current.style.backgroundColor = color; window.requestAnimationFrame(() => { updateVolumeMeter(); }); } } useEffect(() => { window.requestAnimationFrame(() => { updateVolumeMeter(); }); }); return ( ); }; ================================================ FILE: src/components/design-system/Box.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; export const Box = styled.div` ${ss.color} ${ss.space} ${ss.borders} ${ss.borderColor} ${ss.borderRadius} ${ss.width} ${ss.height} ${ss.flex} ${ss.flexDirection} ${ss.display} ${ss.justifyContent} ${ss.opacity} ${ss.position} ${ss.alignItems} ${ss.left} ${ss.top} ${ss.bottom} ${ss.right} ${ss.zIndex} ${ss.boxShadow} ${ss.maxWidth} ${ss.minWidth} ${ss.maxHeight} ${ss.minHeight} box-sizing: border-box; `; ================================================ FILE: src/components/design-system/Button.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; const Button = styled.button` ${ss.color} ${ss.width} ${ss.height} ${ss.space} ${ss.borders} ${ss.borderRadius} ${ss.fontWeight} ${ss.fontSize} ${ss.alignSelf} ${ss.width} ${ss.height} ${ss.flex} ${ss.position} ${ss.left} ${ss.top} ${ss.bottom} ${ss.right} ${ss.display} ${ss.alignItems} ${ss.justifyContent} ${ss.opacity} ${ss.minWidth} outline: ${({ outline }) => outline}; touch-action: manipulation; `; Button.defaultProps = { border: 'none', fontWeight: 'bold', borderRadius: '0.25rem', variant: 'primary', width: 5, }; export { Button }; ================================================ FILE: src/components/design-system/ControlLabel.js ================================================ import { Text } from './Text'; export const ControlLabel = Text.extend` font-size: 0.7em; text-transform: uppercase; color: white; `; ================================================ FILE: src/components/design-system/Form.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; export const Form = styled.form` ${ss.color} ${ss.space} ${ss.borders} ${ss.borderColor} ${ss.borderRadius} ${ss.width} ${ss.height} ${ss.flex} ${ss.flexDirection} ${ss.display} ${ss.justifyContent} ${ss.opacity} ${ss.position} ${ss.alignItems} ${ss.left} ${ss.top} ${ss.bottom} ${ss.right} ${ss.zIndex} box-sizing: border-box; `; ================================================ FILE: src/components/design-system/Heading.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; export const Heading = styled.h1` ${ss.color} ${ss.fontSize} ${ss.fontWeight} ${ss.space} ${ss.fontFamily} `; ================================================ FILE: src/components/design-system/HoverButton.js ================================================ import { Button } from './Button'; const getColor = (theme, color) => theme.colors[color] || color; export const HoverButton = Button.extend` transition: all ${({ transitionSpeed }) => transitionSpeed} &:hover { color: ${({ theme, hoverColor }) => getColor(theme, hoverColor)}; background-color: ${({ theme, hoverBg }) => getColor(theme, hoverBg)}; opacity: ${({ hoverOpacity }) => hoverOpacity}; } &:active { background-color: ${({ theme, activeBg }) => getColor(theme, activeBg)}; opacity: ${({ hoverOpacity }) => hoverOpacity}; } `; ================================================ FILE: src/components/design-system/HoverLink.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; export const HoverLink = styled.a` ${ss.opacity} text-decoration: none; display: inline-block; transition: all ${({ transitionSpeed }) => transitionSpeed}; &:hover, &:focus { opacity: ${({ hoverOpacity }) => hoverOpacity}; } &:active { opacity: ${({ activeOpacity }) => activeOpacity}; } `; ================================================ FILE: src/components/design-system/Image.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; export const Image = styled.img` ${ss.color} ${ss.space} ${ss.width} ${ss.height} ${ss.flex} ${ss.display} ${ss.justifyContent} ${ss.opacity} ${ss.position} user-select: ${({ userSelect }) => userSelect}; `; ================================================ FILE: src/components/design-system/Label.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; const Label = styled.label` ${ss.color} ${ss.fontWeight} ${ss.fontSize} ${ss.space} ${ss.position} ${ss.left} ${ss.top} ${ss.letterSpacing} ${ss.height} display: block; line-height: 1em; `; Label.defaultProps = { m: 0, p: 0, }; export { Label }; ================================================ FILE: src/components/design-system/Line.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; const Line = styled.div` ${ss.color} ${ss.space} ${ss.borderRadius} ${ss.width} ${ss.height} ${ss.flex} ${ss.display} ${ss.opacity} ${ss.position} ${ss.alignItems} `; Line.defaultProps = { bg: 'nearWhite', width: '100%', display: 'block', height: 1, }; export { Line }; ================================================ FILE: src/components/design-system/Text.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; const Text = styled.span` ${ss.color} ${ss.fontWeight} ${ss.fontSize} ${ss.space} ${ss.position} ${ss.left} ${ss.top} ${ss.letterSpacing} ${ss.height} ${ss.zIndex} ${ss.borderRadius} ${ss.textAlign} ${ss.opacity} ${ss.lineHeight} ${ss.display} ${ss.verticalAlign} user-select: ${({ userSelect }) => userSelect}; `; Text.defaultProps = { m: 0, p: 0, lineHeight: '1em', display: 'block', }; export { Text }; ================================================ FILE: src/components/design-system/TextInput.js ================================================ import styled from 'styled-components'; import * as ss from 'styled-system'; const TextInput = styled.input` ${ss.color} ${ss.fontWeight} ${ss.fontSize} ${ss.space} ${ss.position} ${ss.zIndex} ${ss.width} ${ss.height} ${ss.boxShadow} display: block; border: none; `; TextInput.defaultProps = { m: 0, p: 0, }; export { TextInput }; ================================================ FILE: src/components/design-system/index.js ================================================ export * from './Heading'; export * from './Box'; export * from './Button'; export * from './HoverButton'; export * from './Text'; export * from './Line'; export * from './Image'; export * from './TextInput'; export * from './Form'; export * from './HoverLink'; export * from './Label'; export * from './ControlLabel'; ================================================ FILE: src/components/index.js ================================================ export * from './design-system'; export * from './ChannelList'; export * from './Channel'; export * from './ChannelHeader'; export * from './Toggles'; export * from './PlayButton'; export * from './BPMInput'; export * from './Marker'; export * from './FancyButton.component'; export * from './AddChannelButton'; export * from './PresetSelector'; export * from './MasterControls'; export * from './Modal.component'; export * from './Logo.component'; export * from './GithubLink.component'; export * from './ChannelControls'; export * from './FlashMessage'; export * from './SwingControl'; export * from './Branding'; export * from './InstallButton'; ================================================ FILE: src/components/timedCallback.hoc.jsx ================================================ import React from 'react'; export const timedCallback = (callback, delay) => WrappedEl => class extends React.Component { constructor() { super(); this.timer = setTimeout(callback, delay); } componentWillUnmount() { clearTimeout(this.timer); } render() { return ; } }; ================================================ FILE: src/index.html ================================================ WDS-1: Web Drum Sequencer
    ================================================ FILE: src/index.jsx ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; import App from './components/App'; import { initializeAudio } from './services/audioLoop'; import { configureStore } from './store'; import { loadSampleStatefully } from './common'; import { startAnimations } from './services/animations'; import { initializePwaInstall } from './services/pwaInstall'; import { initializeDB } from './services/database'; const { store, persistor } = configureStore(); /** * Watch for user going online, and try to load any samples * that haven't been loaded (e.g. because user was offline) */ window.addEventListener('online', () => { const { channels } = store.getState(); channels.forEach((channel) => { if (!channel.sampleLoaded) { loadSampleStatefully(store.dispatch, channel); } }); }); // Register service worker if (import.meta.env.DEV && 'serviceWorker' in navigator) { try { navigator.serviceWorker.register('./sw.js', { scope: '/', }); } catch (error) { console.error(`Registration failed with ${error}`); } } ReactDOM.render( , document.getElementById('root'), ); initializeAudio(store); startAnimations(store); initializePwaInstall(store); initializeDB().then(() => { const { channels } = store.getState(); // Load up all the initial samples channels.forEach((channel) => { loadSampleStatefully(store.dispatch, channel); }); }); ================================================ FILE: src/presets/707.js ================================================ import { samples } from '../samples.config'; export default { name: '707', bpm: 133, swing: 0.2, channels: [ { id: 'empty_channel', sample: samples.tr707Bd, gain: 1, }, { id: '19a36a53-a907-41f2-baef-394f0fbf0403', sample: samples.tr707SdHigh, gain: 1, }, { id: 'ca0d0d5f-61af-45a4-8d36-8648d20e3132', sample: samples.tr707Ch, gain: 0.19, }, { id: '69aa5d1a-085a-4083-93c5-b3aa7a334278', sample: samples.tr707Oh, gain: 0.32, }, { id: '5a2f4293-159f-46e9-a9de-52eaeb326483', sample: samples.tr707Clap, gain: 0.75, }, { id: '1449a5d5-79ea-4332-83e0-d5513a82d16f', sample: samples.tr707Tamb, gain: 0.37, }, ], notes: { empty_channel: [ [ { beat: 1, id: '79773a84-0861-4c5f-8377-17a103e953f5', }, { beat: 2.5, id: '896a049b-288a-4441-850c-f364106345c1', }, { beat: 3.5, id: '27865470-1309-4439-88fc-141c425a0954', }, ], [ { beat: 1, id: 'fdbc7ee8-b1e6-49fa-8ce9-7acc3ad86d35', }, { beat: 3.5, id: '90c9541e-3d94-4f4f-885a-a362bca2e973', }, ], [ { beat: 1, id: 'e81aee0f-87e5-48d1-abdd-d86318fc1943', }, { beat: 1.75, id: 'f5b70c60-ad16-4537-b436-4f05f1e5da6c', }, { beat: 2.5, id: '1046df31-74c8-4f3e-8307-68f7933eee8b', }, { beat: 3.5, id: 'f4871aa7-b265-4f9b-bfd4-7622a20ef9ca', }, { beat: 4.5, id: '1b3d4594-fd4e-4d88-ba9f-72018b6130db', }, ], [ { beat: 1, id: '21a840df-bbf6-4d66-be0f-2f9c779af3a8', }, { beat: 4.5, id: 'eed92161-5fde-42ac-b2a1-12c9f4cf8490', }, { beat: 2.5, id: '0721650c-59f6-41c6-81dd-b426ab07bbe5', }, ], [ { beat: 1, id: '131ee00c-23a9-4acc-bc90-6aac8b5575b1', }, { beat: 3, id: '081ea16f-ec75-48cf-9867-33116a3ff052', }, { beat: 3.75, id: 'afa06799-5e9a-4289-81e2-350ddfd0470d', }, { beat: 4.5, id: 'ad7dfa99-402b-4398-8d4c-d7263194b635', }, ], [ { beat: 1, id: 'd96dc48b-b2fb-4e3c-9b8d-8c41b4559885', }, { beat: 1.25, id: 'e399b29b-7ff8-4212-a122-1564cbe347a9', }, { beat: 2.75, id: '4ed059cf-f917-458f-a9ac-ad41ddca64ab', }, { beat: 2.5, id: 'ae266807-6e79-4cda-9b82-7b45b5ccc3cf', }, ], [ { beat: 1, id: '5d85f763-3650-4bfb-9e02-e0e51a2dd543', }, { beat: 3.5, id: 'dc87df2e-0b5b-4668-acbe-0d1f043e95c8', }, ], [ { beat: 1, id: '921e81f6-5f69-4b0e-a2d4-4f8270f150de', }, { beat: 1.75, id: '736030e0-a968-4062-828d-9ffe54bcfd93', }, { beat: 2.5, id: 'd4d5a045-e563-4495-b824-c7fdf91ac10d', }, { beat: 3.5, id: '9a64edf9-c4d0-4ab5-b710-61ac068d4aa3', }, { beat: 4.25, id: 'afdaf0d4-8735-4a76-b158-c7f67eb5170e', }, ], ], 'd65e1669-fbce-49ee-8bfc-dc7928407385': [ [], [ { beat: 2, id: '8f290791-dc31-4b76-a536-041af5a1d495', }, { beat: 4, id: 'ab838c5e-d3e0-4e18-a6e8-44a309b965d4', }, ], [], [], [], [], [], [], ], '19a36a53-a907-41f2-baef-394f0fbf0403': [ [ { beat: 2, id: 'b2c8970e-9790-40b2-862e-6abca6648d7d', }, { beat: 4, id: '540ab903-42e1-401e-b0ef-9fa131cd81de', }, ], [ { beat: 2, id: '21e78843-24c7-49c9-80cf-e24fb9cd1b17', }, { beat: 4, id: 'afc253e4-4669-4992-b37d-bb620bc94992', }, ], [ { beat: 2, id: '5db6f04b-04d9-4f7b-93c6-64b0501e4e78', }, { beat: 4, id: 'db751285-c481-46fd-82b1-d5d6dadce699', }, ], [ { beat: 2, id: '7ee58927-5006-44ff-b570-44956a5c34af', }, { beat: 3.5, id: '658b91d8-05c8-4228-93f5-68b35a56790b', }, ], [ { beat: 2, id: '49a9078d-3663-4ca7-b264-6ae3d2b3d25b', }, { beat: 4, id: 'a6a1fd44-312f-49bb-a023-4ceaf8cad4a2', }, ], [ { beat: 2, id: '1fc7662d-ed97-42c3-bb32-4b4a61a98c0c', }, { beat: 4, id: 'c7902b4a-23b8-4ef9-afe8-7fee06fe86de', }, ], [], [ { beat: 2, id: '68364efb-1515-41b9-af10-062e11effca7', }, { beat: 3, id: '60271671-559f-4f4c-9b92-69814e908bf4', }, { beat: 3.25, id: '9e3cfbff-b83f-4f51-871b-18dc225f6020', }, { beat: 3.75, id: '373b4464-8a24-4495-955f-c7998677e054', }, { beat: 4.5, id: 'b7e45875-7453-4a6f-8440-4e0374b66ae3', }, ], ], 'ca0d0d5f-61af-45a4-8d36-8648d20e3132': [ [ { beat: 1, id: '49dcbbcf-7d18-4ab1-ad39-248fe4ff62c6', }, { beat: 2, id: 'd59f046b-a64c-4db8-b6f0-d50f7affe89c', }, { beat: 3, id: 'aab9a08c-c0c7-4f04-94b5-1fd0e45fb820', }, { beat: 4, id: '543315bb-a931-4754-8f68-bdb2765b88f0', }, ], [], [ { beat: 1.5, id: '83e7d304-0dc5-4089-80ba-50ba20f918d9', }, { beat: 2.5, id: 'd00d0a86-75e3-4dc5-98e2-7f92eef51103', }, { beat: 3.5, id: '746e4b6b-b9cf-4c68-903e-76da267d05d7', }, { beat: 4.5, id: '723ebc55-f518-43ca-bf47-85fd4b1b9c9c', }, ], [ { beat: 1, id: '208ea2bd-4970-4134-a2e4-e0276d4f5455', }, { beat: 2, id: 'ff4d906f-9e2a-437a-91c2-cf9a8e8f80f0', }, { beat: 3, id: '0da187a0-bf27-41f2-9ffa-70b586bb3d8c', }, { beat: 4, id: 'af8ff590-9759-4ec0-9491-25cbaf23416d', }, ], [ { beat: 2, id: '61702e67-426e-4084-a762-eedfbab99739', }, { beat: 3, id: 'af1567ab-0b9c-49dd-8fde-284086dae025', }, { beat: 1, id: '05a9287e-4ef1-4e3e-a7ba-35c822ccc696', }, { beat: 4, id: 'a229b9b8-fcef-4f65-820c-f02a08614ff1', }, ], [ { beat: 1, id: 'edc9fc6a-2bb6-4217-9df8-bc283cec4207', }, { beat: 1.25, id: 'bf9b5e2b-9b79-4e22-9a66-01351045306a', }, { beat: 1.5, id: 'a206bcc9-5848-4aca-b6c4-fc619b88a06a', }, { beat: 1.75, id: '2312e7d0-aa40-4bdc-bd45-09345652aa3f', }, { beat: 2, id: '57e63bad-6765-4eb3-8e98-cb1de36d9eab', }, { beat: 2.25, id: '68b2d4b2-9f04-4f0b-894e-17f74719ed7d', }, { beat: 2.5, id: '3b0bcaff-1f8e-4bbf-b2e6-693d2f5000c5', }, { beat: 2.75, id: 'b010de6a-01ea-46c2-8c21-62d2eaaeb2ca', }, { beat: 3, id: 'a2880e10-c4d5-43be-be5a-4c1aae9e929f', }, { beat: 3.25, id: 'f88dd5f3-8d35-40f2-b818-69415072bdce', }, { beat: 3.5, id: '8766fc70-2160-42e1-8e53-229c005e305e', }, { beat: 3.75, id: 'a18c7d1e-238c-480c-bfb0-626df60bee9c', }, { beat: 4, id: '37e1c631-6abb-4044-9400-149005de63ce', }, { beat: 4.25, id: 'd0308433-5217-4371-bd7a-e353da21aaa7', }, { beat: 4.75, id: 'f255ca9c-b6be-468b-95a1-7b2b6cc48a1a', }, ], [ { beat: 1, id: '32d56fe7-a68c-45ce-b1e4-8f47e377e7ba', }, { beat: 2, id: '924a0f29-1876-4b5d-9750-84146cc7b3de', }, { beat: 4, id: 'cde3d121-02c9-4376-b60f-c13965fc9e77', }, { beat: 3, id: 'b2171ad3-8d5e-473f-82be-2d9b9dfc1fbc', }, ], [ { beat: 1, id: 'e54c169d-999e-4c5b-b860-1a077ef1b89e', }, { beat: 1.75, id: '6379021f-bf80-45cd-a5b0-96d0172a6992', }, { beat: 2.5, id: 'b9ff3cfa-eb1e-4b4a-8947-64c1ced9912a', }, { beat: 3, id: 'd12eab75-cc84-4cea-adbd-a48cab39fb90', }, { beat: 3.25, id: '8535ac71-ef73-4b4a-a832-2a9253ffa050', }, { beat: 3.75, id: '067adfda-0c10-4eef-af41-13344599c979', }, { beat: 4.25, id: '511d9a2a-bd48-4aa1-b8e7-b4861f5d53dd', }, { beat: 4.5, id: '6fd510b6-e966-4e9e-b0ce-754de3f5de80', }, ], ], '69aa5d1a-085a-4083-93c5-b3aa7a334278': [ [], [ { beat: 4.5, id: 'b3b987ad-53cf-4fac-8585-95e70b02b8bb', }, ], [], [], [], [ { beat: 4.5, id: '8ba64f81-82c5-4d1a-94fe-7c7b2a8d618f', }, ], [], [ { beat: 2, id: '4b588776-4189-4642-897a-b49f4bd5d825', }, ], ], '5a2f4293-159f-46e9-a9de-52eaeb326483': [ [ { beat: 4, id: 'bf626801-b8b7-4ed9-b8d4-25bcb3197a9a', }, { beat: 2.5, id: '09d3f172-bae4-4c02-bab5-f5c52c9d5944', }, ], [ { beat: 2, id: '470249a0-7783-4129-95e5-0e44cf3ef520', }, { beat: 3.5, id: '53a4d4f3-dcd1-41a6-9964-61d66b0ce4d9', }, { beat: 4, id: 'c82e8cb2-5bb6-4ad1-85a2-f54d0a9f5aa7', }, ], [], [ { beat: 2, id: '979fd4bc-1c06-462d-887c-f2c67f710a38', }, { beat: 4, id: 'c8f0619e-6577-47b6-9ca6-c1cf08348702', }, ], [ { beat: 2, id: 'a56f2267-1086-45a4-891f-ed20b0a8b830', }, { beat: 4, id: '85566726-c759-4b3f-9c98-cab677c81b24', }, ], [ { beat: 2.5, id: '9d78a8e3-95c5-42a8-8b04-b13fa221c9fc', }, { beat: 4, id: '4fb62413-2073-4e0a-8e83-9ee507743185', }, ], [ { beat: 2, id: 'bc12ec84-7d10-4478-8702-e8b9694e9f90', }, { beat: 4, id: '3b6c3ce3-61e6-4486-bd3a-091e6ec4b61f', }, ], [ { beat: 3, id: '118885eb-9f4e-4f75-b974-be31bfc88b23', }, { beat: 4, id: '03112e7d-c9a3-43e9-81a0-b3868ba23429', }, ], ], '1449a5d5-79ea-4332-83e0-d5513a82d16f': [ [], [ { beat: 1, id: '40d7c694-19cb-4671-98d6-e0cbd128ad8b', }, { beat: 1.5, id: 'd2d07448-5c59-472a-ab8c-a12222b042b6', }, { beat: 2, id: '5c6b8efb-0f18-44d9-ba93-305bc5c04eb4', }, { beat: 2.5, id: 'bb5d511d-febc-4557-a658-e3bbb6f743cf', }, { beat: 3, id: 'a5d5cd55-516c-4307-a946-45ac8043899a', }, { beat: 3.5, id: '23501c3c-97ac-4ceb-819f-dd1076c278ca', }, { beat: 4, id: '299a7e10-d73a-47b3-9f82-fbc8061f3b39', }, { beat: 4.5, id: '0c981e72-048e-42c8-9c67-73d12d3e0acf', }, ], [ { beat: 1, id: '454dafea-f810-454e-970c-d046239f2a4e', }, { beat: 3, id: 'c952921a-fee1-4221-9a54-1b2031ae854d', }, ], [], [ { beat: 1, id: '29da9814-96af-4c98-a0dd-f5e655a808b3', }, { beat: 4, id: '3a12f5b2-7cf1-44f5-848b-eeaf5b14a9d5', }, { beat: 3, id: 'e0046def-f622-4d39-a951-91aab5896818', }, { beat: 2, id: '2e0c328a-4ddd-4109-bac6-eb6d9a19f8e9', }, ], [ { beat: 3, id: '53c354e4-f8e7-4ad1-a4e4-56b91130d599', }, { beat: 1, id: 'cd8b58cc-e634-449a-bcf7-59e2a7ab52af', }, { beat: 2, id: 'bf82ff02-f111-4ab3-aef4-41a432678129', }, { beat: 4, id: '6344d678-2281-4595-b397-fcb31efc7a91', }, ], [ { beat: 1, id: '9d87a6f8-ce7f-480c-8005-9bf306d06e4f', }, { beat: 3.5, id: 'cf323090-5b13-4d8b-a923-a3e05dbc608d', }, ], [ { beat: 4.5, id: 'ee95c968-439d-4f0a-bfd0-6652d70e2759', }, ], ], }, }; ================================================ FILE: src/presets/808.js ================================================ import { samples } from '../samples.config'; export default { name: '808', bpm: 105, swing: 0, channels: [ { id: 'empty_channel', sample: samples.tr808BdShort, gain: 0.83, }, { id: 'eb405dca-32d1-4867-b7e0-ef37198f5fee', sample: samples.tr808BdLong, gain: 1, }, { id: '59fe4d57-c587-441b-b10d-9a2b54d588fd', sample: samples.tr808Sd, gain: 1, }, { id: '6a2a5e54-8cbc-4e7e-a81c-68fc4b34c0bc', sample: samples.tr808Clap, gain: 0.85, }, { id: '9b6de154-8a6e-4941-80db-4b9f47d9c241', sample: samples.tr808Ch, gain: 0.21, }, { id: 'fb2edb2c-3d21-49fd-98d8-459b153fb083', sample: samples.tr808Oh, gain: 0.28, }, { id: '27f949f4-f5f7-4846-abfb-1d0e544b5bff', sample: samples.tr808Cowbell, gain: 0.55, }, { id: 'd8d65425-e980-43a6-b3b3-1c14b55b8c94', sample: samples.tr808Cym, gain: 0.37, }, { id: 'dbbfd24a-0f5d-409f-b4ed-ec330b47e2e5', sample: samples.tr808Clav, gain: 0.31, }, { id: '206fcf08-67ff-4e98-905e-2495383fb83c', sample: samples.tr808Rs, gain: 0.43, }, { id: '0ea2f943-2144-4434-92f7-8d689d49633a', sample: samples.tr808Ht, gain: 0.71, }, { id: '2f090e95-37f7-4c49-b0c5-eb35d64216c8', sample: samples.tr808Mt, gain: 0.59, }, { id: '816f7250-0afb-4353-bc6c-84ad97f9b8c6', sample: samples.tr808Lt, gain: 0.71, }, ], notes: { empty_channel: [ [], [], [], [ { beat: 1, id: '8d680b7a-2d25-4e13-867f-8b6b8b6415b0', }, { beat: 3, id: '420a2676-67a4-4006-a068-22cdb19fb5ac', }, { beat: 4, id: '91ffc7b1-ef92-452e-99df-02ce05d01277', }, { beat: 2, id: 'a044d8e8-9a4a-4221-8f81-071897fb94a9', }, ], [], [ { beat: 3.5, id: 'b32de0b1-4eb6-484f-8cb8-b6550bf7d5ff', }, { beat: 4.25, id: '5f017b71-3a67-4735-83b0-b4dc5eb0e55d', }, ], [ { beat: 1, id: '5c7e76ca-fdf3-4f39-8379-d847298dcbe8', }, { beat: 2, id: '9630e240-98a3-482b-a956-1b1cbbc3f6f7', }, { beat: 3, id: '64f339ef-5a10-4b23-8dfb-ad68c6b8a3dc', }, { beat: 4, id: '0471ef77-08b4-4d5a-815b-3a7b2541513a', }, ], [], ], 'eb405dca-32d1-4867-b7e0-ef37198f5fee': [ [ { beat: 1, id: '29ddd774-51b9-47eb-aa90-15b5647af688', }, { beat: 2.5, id: '97fc68b1-722d-4f86-bd01-bb7dca96d6e5', }, ], [ { beat: 1, id: 'fd0bfd6d-8faa-4e78-9d7c-d3c176a0c2fc', }, { beat: 3.5, id: 'fe3b3f20-fa95-4204-86ae-eb30e63f8b49', }, ], [], [], [ { beat: 1, id: '032ecd95-d72e-426d-a742-a5deb4bcfb88', }, { beat: 3.5, id: '29082399-8b29-4ce7-829c-6ae8d15da51c', }, ], [ { beat: 1, id: '86b85f42-dcdb-44a8-a0eb-425d4bf97ffa', }, { beat: 2.5, id: '15f8b82c-df03-41e8-ba72-4087c1c90663', }, ], [], [ { beat: 1, id: '6fc5428e-80b6-4d11-a3bd-42aefb1230f6', }, ], ], '59fe4d57-c587-441b-b10d-9a2b54d588fd': [ [ { beat: 4, id: '4e31c684-0f4c-47f1-a891-50709c3bf1fc', }, { beat: 2, id: '64743cf2-27a8-4ad6-af11-621f5b2acdfa', }, ], [ { beat: 2, id: '4600798b-3b4a-4465-a494-4a071d9d4d84', }, { beat: 4, id: '32205ee0-e933-493f-af20-eece6f97e2ab', }, ], [ { beat: 4.75, id: '25531c10-3a3f-425a-8814-7800830a2cde', }, ], [ { beat: 2, id: '8c3a23f5-e6d8-458c-92de-c1f92be0f0f9', }, { beat: 4, id: 'b3af60f9-8a57-4a00-ac9b-0175eafffc77', }, ], [ { beat: 2, id: '88e1fa22-f678-4cd2-926b-296cd1a09948', }, { beat: 4, id: '5dab556e-f303-4ed2-b85c-9ec47dbf026d', }, ], [ { beat: 2, id: 'c0ed62e3-2763-4bf5-b80c-58aa5154cf99', }, { beat: 4, id: '95355109-8f8e-4964-b44d-831a6c3f1433', }, ], [], [ { beat: 3.5, id: '3cf162e3-5d2d-4ff9-b2b6-6aa372280ea8', }, { beat: 4.5, id: '401eeb6f-ec89-4b57-b687-9d87b0f7d04f', }, { beat: 4.75, id: '9ddab637-94c1-438e-9ec7-331df982d034', }, { beat: 4, id: '0c0b94fe-ceda-424b-8d8b-7c717f13ec01', }, ], ], '6a2a5e54-8cbc-4e7e-a81c-68fc4b34c0bc': [ [ { beat: 2, id: '0b7163d9-dfdc-4e0c-b5b0-f7730c198f54', }, { beat: 4, id: '0ec12d1a-8ed9-4f44-99d4-d2a9cbc8cd99', }, ], [ { beat: 2, id: '4a11b3b3-81c2-4bd8-8b27-de87dabe02ac', }, { beat: 2.75, id: '71160d69-5717-4369-99bf-e15dcf4c7bc6', }, ], [ { beat: 2, id: '0fc99548-9184-4ff0-8531-cbec8a5c11c2', }, { beat: 4, id: '78b6b419-5274-4b35-a1aa-d05bed345509', }, ], [], [ { beat: 2.75, id: '8b8d953f-3345-40af-90ce-3d1f786f7bdb', }, ], [ { beat: 2, id: '2e01cf3a-15c4-4693-990d-c237c5691c82', }, { beat: 4, id: 'af6bc3b9-479c-4efe-b06a-355a44991302', }, ], [ { beat: 2, id: 'd389f8c8-565c-4d31-a9bb-ca3f72cb8a07', }, { beat: 4, id: 'ab33470e-2ee2-421b-9541-e0fe340f32a6', }, ], [ { beat: 2, id: '9ad61605-2b9c-407b-aa82-45972b2bd4aa', }, { beat: 4, id: '5c475e3c-8487-4d0d-8ec0-d3afacaaa556', }, ], ], '9b6de154-8a6e-4941-80db-4b9f47d9c241': [ [ { beat: 1, id: 'ea650f53-6330-492d-a92e-47ad46e49c8b', }, { beat: 1.5, id: '76e109d2-7a93-475a-a1ca-e635262e824b', }, { beat: 2, id: 'be5d8b74-1d00-4ce7-a075-1f4770bdeb04', }, { beat: 2.5, id: '09aa3eba-d22b-4987-b1bd-07d796b055e1', }, { beat: 3, id: '9d4f42eb-ad56-45ea-a183-6034047786fa', }, { beat: 4, id: 'd151c78e-dee5-4e3a-bb08-d8916c226d70', }, { beat: 3.5, id: '2dce2fae-5413-40ef-9192-49d00dee623c', }, ], [ { beat: 1.5, id: '2942f843-440c-46e9-aa5e-8cebee9e86f3', }, { beat: 2.5, id: '1598985e-fbda-4325-9df7-4e4fdd426061', }, { beat: 3.5, id: '01140c00-7e38-4c47-bb2f-866c87e9c2bf', }, { beat: 4.5, id: 'c8eb2e22-36b7-4891-96a4-75baaefe1cd8', }, ], [ { beat: 1.5, id: '7d6d2ef5-50e6-4d25-b583-416ea1b67a73', }, { beat: 1, id: '955cb6a2-05a1-4c72-9436-2c9da602f3a0', }, { beat: 2, id: 'e9cbcf6f-acde-4cb4-a5c7-334fcf2f9854', }, { beat: 2.5, id: '472ac95d-e5a3-4938-84cc-e6b8204833bf', }, { beat: 2.75, id: '5f69d201-c2d9-4051-83bf-34fd4c5346e4', }, { beat: 3.5, id: 'bdf0b3dc-cfe4-4384-84e1-80a390f6e165', }, { beat: 4, id: '3a1d6250-7a3e-4bea-93fe-cc1f0015534c', }, { beat: 4.5, id: 'ddb60cd9-b9e3-4bf5-810c-2824ef1241ba', }, ], [ { beat: 1.5, id: '2c2fb20d-c409-4fee-bc44-e35e9b494cc3', }, { beat: 2.5, id: '613f728c-16ff-4d4b-9f74-84d2923ad13d', }, { beat: 3.5, id: 'f1a762f3-4481-44d3-9b31-ae3d488c54c6', }, ], [ { beat: 1, id: '6edb0a4b-c91f-42dc-bb43-2e54eee77288', }, { beat: 1.5, id: 'ae2f66d3-100d-431c-a3fb-c7c8ddd1c2f3', }, { beat: 2, id: 'f0543739-d954-435c-a5fe-93d72ee582f2', }, { beat: 3, id: 'a2edccea-0cf4-4947-b149-4191f07bdf1e', }, { beat: 3.5, id: 'cee93951-856d-4093-9cea-2f3b516f792d', }, { beat: 4, id: '4c604170-949c-4ff9-8789-01bd72d55f46', }, { beat: 4.5, id: 'a4f3ad08-12bd-4e5e-9a2a-1f0e0b53b4d5', }, ], [ { beat: 1, id: 'c4ef6a9a-bd43-4878-8753-3a4ef5813266', }, { beat: 1.25, id: 'af3c946e-b413-4dfd-9a8e-cc9277edd88c', }, { beat: 1.5, id: '3c60b720-f348-445e-a9e3-74a5ac94c9bf', }, { beat: 1.75, id: '36f75f4b-12ad-4f10-a563-f243cdac6492', }, { beat: 2, id: '14582379-11b5-4496-ac28-ef2fc9ff92c8', }, { beat: 2.25, id: '2cbb4cf7-d2c5-4b73-bd09-12476967bc32', }, { beat: 2.5, id: 'b4a6edab-10c5-4b48-927f-fe36d8711e4b', }, { beat: 2.75, id: 'b48dcdb6-085b-43f6-a5f5-430d5f357425', }, { beat: 3, id: 'e69253b1-bedc-4126-843c-e8373732b347', }, { beat: 3.25, id: 'c6d5f82a-699b-4b78-a37d-053d4d3dcb5b', }, { beat: 3.75, id: 'b171d982-949f-41c7-96e5-34461596e5ab', }, { beat: 4, id: '193c8efc-41e8-415f-9eb9-e3130cd5705f', }, { beat: 4.25, id: '9300786a-def9-4a5c-afbb-576342fb80f5', }, { beat: 4.75, id: 'cf3053bf-0e9c-4d2c-ac08-b47ee626b288', }, ], [ { beat: 1.5, id: 'ef337ebe-946a-47c1-aedd-450564a97014', }, { beat: 2.5, id: '9bca0ede-1f8e-4e0d-8af8-c5999609c001', }, { beat: 3.5, id: '3a5729e6-34f5-47e6-8d50-7519415ca13b', }, { beat: 4.5, id: 'ab176a41-9776-497a-af60-07ec36b23606', }, ], [ { beat: 1.5, id: 'e2dbd7b2-1f0e-4dc8-b815-10beb669ff27', }, { beat: 2.5, id: 'ddeb3419-3116-43d7-a0f2-34e46e4ae886', }, { beat: 3.5, id: '9820216b-00d6-4295-b0b9-ea809e81e782', }, { beat: 4.5, id: 'f1b516d1-a2fb-43d2-b42e-1596d3191b0d', }, ], ], 'fb2edb2c-3d21-49fd-98d8-459b153fb083': [ [ { beat: 4.5, id: '77238ce7-2fc0-44c2-8af2-789edaeba92e', }, ], [], [], [ { beat: 4.5, id: '03b7e1b0-2fb8-4dca-b982-6f0c77ae6673', }, ], [ { beat: 2.5, id: '07eb2589-17e6-45d8-b18b-5b4d4a0aa8f5', }, ], [ { beat: 3.5, id: 'afba7c4b-7f29-4602-8852-602d138f6946', }, { beat: 4.5, id: '26924a16-d4c6-4b7f-b257-aaeb155db511', }, ], [], [], ], '27f949f4-f5f7-4846-abfb-1d0e544b5bff': [ [ { beat: 2.75, id: '0f04bb15-6c36-4c58-974d-6aa004382576', }, { beat: 3.5, id: 'e1e4bb50-c57b-4260-89a2-b3726635cfd4', }, ], [], [ { beat: 1.5, id: '24746c40-9fb0-483c-939c-3842b767b1d4', }, { beat: 4.75, id: 'e7d0ff1f-fa09-431e-9a1a-fc6aa7cf0a3f', }, ], [], [], [ { beat: 1.5, id: '3d7b1deb-47dc-4029-b89e-0d701691b963', }, { beat: 4.75, id: 'afb4d24b-41e1-4388-a724-04d90a637991', }, ], [], [ { beat: 1, id: '08e0071c-c3a3-4a96-aa1d-8c770c3b4c27', }, { beat: 1.5, id: 'dee01a92-967a-4af0-8a19-a2899b5044e0', }, ], ], 'd8d65425-e980-43a6-b3b3-1c14b55b8c94': [ [], [], [ { beat: 1, id: '29d70f6a-df60-47d3-bbb5-5031c29e4390', }, ], [], [], [], [], [ { beat: 1, id: '03a3e0e5-9278-4d51-a41c-6ec04de5de0f', }, ], ], 'dbbfd24a-0f5d-409f-b4ed-ec330b47e2e5': [ [], [], [ { beat: 1, id: 'b1ce6e18-9e22-405f-bb53-0b60a2c0c2c3', }, { beat: 3, id: '64e011d1-93e0-46ac-b6e1-c51955edece1', }, ], [ { beat: 1, id: '34dc07d7-1698-41be-bb7b-973f38a6dc16', }, { beat: 1.25, id: 'a5e62b92-f494-431d-a325-1b747b27186b', }, { beat: 2, id: '62e8cae9-f00d-47db-b985-b6acf219ed54', }, { beat: 2.75, id: '7cea74a4-dd0d-43bf-9db3-f5b44c0de7f6', }, { beat: 2.5, id: '5dd16303-e248-4b87-b988-ade5d68a0160', }, { beat: 3.5, id: '20e36003-3b5d-43cb-a966-53d94bfe3379', }, { beat: 4.5, id: '81d5797e-4c59-4a1d-ad4b-a77b916c757d', }, { beat: 4, id: '53644024-31c7-4c18-a116-e2bca5e7fa36', }, ], [], [], [], [], ], '206fcf08-67ff-4e98-905e-2495383fb83c': [ [], [ { beat: 3.5, id: '34424649-30bb-4195-bc4c-46e5cb2dbd5c', }, { beat: 3.75, id: '957478f5-b2b5-4feb-8626-0b41121764f3', }, { beat: 4.25, id: 'b5e9576d-7cbf-4c8d-86bf-06e0eb66d685', }, { beat: 4.5, id: '9946c9a4-c59e-4b32-8574-f65536dc56a6', }, { beat: 4, id: 'd9b5d411-bb73-46f4-91f2-befc027c3b99', }, ], [], [], [ { beat: 1, id: 'b9206d8e-272e-4e76-beee-ba008688cc67', }, { beat: 2.75, id: 'd0e18285-2b00-4893-b8be-abad2a3f848b', }, { beat: 3.5, id: '1f20cfc1-c299-416c-b141-4b42c05e16e9', }, { beat: 4.5, id: 'f126fead-b0b9-4aca-9f88-2ca16d46ab12', }, { beat: 1.5, id: '523e145e-ea70-47c6-b977-4eced0d34b12', }, { beat: 2.25, id: '43d1e8de-2c07-447f-9e9e-d4df8ec42c68', }, ], [], [], [], ], '0ea2f943-2144-4434-92f7-8d689d49633a': [ [], [], [], [ { beat: 1.75, id: '69330ba3-a1d0-48a3-ba1e-f797c0571231', }, ], [ { beat: 4.5, id: 'e280cdfd-291a-4562-90a7-287613e891b5', }, ], [], [ { beat: 1.75, id: '10e9f6c6-698c-4d01-a9ea-508a213c435a', }, { beat: 3.75, id: '97db3af1-342f-438c-a4ed-a45130b155ea', }, ], [], ], '2f090e95-37f7-4c49-b0c5-eb35d64216c8': [ [], [], [], [ { beat: 2.5, id: '37346254-891c-45a9-8a55-8cf653e79e97', }, ], [], [], [], [], ], '816f7250-0afb-4353-bc6c-84ad97f9b8c6': [ [], [ { beat: 2.5, id: '1af0deed-c640-4d2c-8b31-8675eb13b914', }, ], [], [ { beat: 1, id: '69ec2523-4eba-4ba0-83e2-35edb3ebc1a7', }, { beat: 3, id: '3069e262-7b65-4da6-a401-eea332498f43', }, ], [], [], [ { beat: 2.5, id: '65bd6d4d-4b5e-45cd-ac0f-3ce821888d51', }, ], [], ], }, }; ================================================ FILE: src/presets/__mocks__/index.js ================================================ export default [ { name: 'Roland 707', bpm: 130, channels: [ { id: 'channel-id', sample: 'channel-sample', gain: 1, }, ], notes: [], }, { name: 'Roland 707', bpm: 130, channels: [ { id: 'channel-id', sample: 'channel-sample', gain: 1, }, ], notes: [], }, ]; ================================================ FILE: src/presets/ace.js ================================================ import { samples } from '../samples.config'; export default { name: 'Ace Drum', bpm: 140, swing: 0.3, channels: [ { id: 'empty_channel', sample: samples.AcetoneBd, gain: 1, }, { id: 'efd00923-e1b5-4bcd-b99c-2cbde8d92634', sample: samples.AcetoneSd1, gain: 0.73, }, { id: '6bf16a32-f8a1-4910-9046-be8b8c81591e', sample: samples.AcetoneCh, gain: 0.28, }, { id: 'ab6ea4cd-f211-43a3-95ae-9a1cf7459de2', sample: samples.AcetoneOh, gain: 0.46, }, { id: '8d761f84-9f82-48b3-bfa1-a11c7a7deeff', sample: samples.AcetonePerc1, gain: 0.45, }, { id: '8735d786-5ac0-4b36-a3fb-c58a55ee2b08', sample: samples.AcetonePerc2, gain: 0.41, }, ], notes: { empty_channel: [ [ { beat: 1, id: '1d197b88-bc89-4047-abfa-4b9ff0027813', }, { beat: 3.5, id: '207feb2f-a19d-452c-9d2e-928dc1eb587c', }, ], [ { beat: 1, id: '3dc0bd8b-be9c-4d03-85ad-d0efcbe9a375', }, { beat: 2.5, id: '833ad698-fadb-4358-baba-cb6623bad000', }, { beat: 3.5, id: '1a250f9b-9fc6-4cb0-b828-c287ada6b46b', }, ], [ { beat: 1, id: '93ed5e15-da3d-4904-9687-c4dc329dc8d8', }, { beat: 2.5, id: '927ea811-5546-40a3-9b16-e936cf817b83', }, { beat: 3.5, id: '735ec419-eb7a-4bad-979e-6f1109b3cb3d', }, ], [ { beat: 1, id: '632cc651-1a98-4594-bb7e-266cee5ddcde', }, { beat: 2.75, id: '888ec786-218b-4318-bccb-df803e449ef8', }, { beat: 3.5, id: 'fbe1f3ce-e6af-465c-8759-84161d83083f', }, { beat: 4.25, id: '006f1ae9-87ee-49f0-aeae-54c1bf3f84ba', }, ], [ { beat: 1, id: 'c949dc68-7b9c-4393-80f1-2879922fbf49', }, { beat: 2, id: 'c7e21ef3-aea4-4275-96d3-6492cfbb1ca9', }, { beat: 3, id: '1f8944c1-fecd-40c0-98f8-295b59d2403b', }, { beat: 3.75, id: '47970992-a6d2-47b6-896d-c2a89e354986', }, { beat: 4.5, id: '50a594a6-3de0-49a0-b3ce-b84135253de9', }, ], [ { beat: 1, id: 'c0cb875b-302f-4072-a793-e870d79f4643', }, { beat: 4.5, id: 'f04b647e-24c3-4a22-8464-704cd7e83dae', }, { beat: 3, id: 'f6bd950c-2531-4b08-afbd-58241e01ccc7', }, { beat: 4, id: '72d5107c-cc47-4c6e-9043-ae85b73783b9', }, { beat: 2, id: '47174fff-ab43-46cb-b514-51de2aa3a07f', }, ], [ { beat: 1, id: '256a5c83-dd5a-4b9a-aad8-073280d52c52', }, { beat: 1.5, id: '0305f16b-acb3-43a3-9ba3-6c5d4d0098bd', }, { beat: 2, id: '48edd551-f99f-4c87-be19-a151e9c05716', }, { beat: 3, id: '2757cc34-a21d-4149-ac91-aee19447f5fb', }, { beat: 3.25, id: '65b5aa1e-4c06-41a4-9665-3c12e984fe6d', }, { beat: 4, id: 'c4af9b8b-785f-4eee-8a9d-0231432c66f3', }, ], [ { beat: 1, id: '7ecb09d0-daf0-4e2b-82ce-d55f30440fda', }, { beat: 2.5, id: '98f32a80-e900-4077-8ce7-ff693f1fdb1b', }, { beat: 2.75, id: '9aad9f46-ed88-46ff-8660-d9c168507652', }, { beat: 3.5, id: '69118664-0826-49a9-bbb4-c0325a73d771', }, { beat: 4.25, id: '953259db-7370-4ecc-b503-2e13ab00ba50', }, ], ], 'efd00923-e1b5-4bcd-b99c-2cbde8d92634': [ [ { beat: 2, id: 'fa741890-aa03-43b8-8f45-e4f24dfd7c1d', }, { beat: 4, id: '5df81234-d6ce-4113-936a-15303ab7bea9', }, ], [ { beat: 2, id: 'fd4be3e8-e5cc-4249-8260-731c19e21f4b', }, { beat: 4, id: '45af7433-99b8-415a-9688-40e2cc1c012e', }, ], [ { beat: 4, id: '50c05847-546e-4413-8835-95071e2c560e', }, { beat: 2, id: '2539ca56-5ff2-41b0-b6c1-343e10cbdc3e', }, ], [ { beat: 2, id: '4ccc091a-f4db-483d-b117-005f388b19e8', }, { beat: 4, id: 'c37ca0cd-84bb-44df-a4e9-395efc0c7603', }, ], [ { beat: 2, id: '3929c7e3-f56f-42bc-80f6-92fe885bdcd2', }, { beat: 4, id: '94ddf04c-3096-45f4-a12f-ce10de25c0fe', }, ], [ { beat: 1.5, id: 'b56918b1-9558-4679-884f-9c0d377a6777', }, { beat: 2, id: 'c0c78af4-b022-4208-9c97-75938d4d89b6', }, { beat: 3, id: 'c25f15bc-fcd7-4787-a9ea-58f5c03439e4', }, { beat: 3.75, id: 'e7769870-3c75-473b-981b-6f444a2dcf78', }, { beat: 4.5, id: '4aabf733-ec94-4755-9778-2a0cc522299d', }, ], [ { beat: 2, id: 'a68fdced-bd7a-4cd5-a283-787099881710', }, { beat: 4, id: 'ef791fa3-4566-4fe4-8d5f-13e0863f3744', }, { beat: 4.5, id: '4ae59c1f-973b-4607-af96-179258cd7a76', }, { beat: 4.75, id: '11228b04-49ab-4845-b60a-1691299701e3', }, ], [ { beat: 2, id: 'efaab763-2dbe-45e2-9a31-dbf793d83879', }, { beat: 4, id: '37a3521d-02e1-4ef7-827d-b2d9d243e140', }, ], ], '6bf16a32-f8a1-4910-9046-be8b8c81591e': [ [ { beat: 1, id: '78f93101-1efe-4081-a917-fdbe5b9adf92', }, { beat: 2, id: '1643b6bb-9242-4caf-bf82-2286f5457314', }, { beat: 3, id: '34bebd08-a9d9-4325-9ffa-8e532587d156', }, { beat: 4, id: 'fc8fd45c-7834-41f6-afd7-88429dc56a90', }, ], [ { beat: 1, id: '2f75c27d-2ded-4176-8d71-de32d8901c39', }, { beat: 1.5, id: 'a4d10956-006f-4658-92ff-5fd653f61c1a', }, { beat: 2, id: 'ce068960-2854-4784-9a5e-5d9c19d22e5a', }, { beat: 2.5, id: '50b400c6-7913-4391-bb8b-dddec30e94a4', }, { beat: 3, id: '0f235fc4-ba58-475f-b450-b41bb639fa17', }, { beat: 3.5, id: 'a961c8c3-5ade-4f66-afe8-a6c356c6e0da', }, { beat: 4, id: 'bf747af4-2102-407e-8f8b-b5b42a2de454', }, ], [ { beat: 1, id: '5a277a9e-8068-4756-a455-123ee20d0296', }, { beat: 3, id: '88c03234-5353-4f2e-b9ff-3c6fa9c05c5a', }, { beat: 4.5, id: '6e94f0b3-e560-4367-b77b-9b4dccdba2e3', }, ], [ { beat: 1.5, id: '925e99ee-c598-4081-bcce-d8899261dd19', }, { beat: 2, id: '44d4f22b-52dc-4fc9-ba64-745e498c04d9', }, { beat: 2.5, id: '39d569a0-c87b-4faa-a9eb-2ac4a5219bae', }, { beat: 3, id: '2040ccbf-9693-47f9-a2ad-2524595152df', }, { beat: 3.5, id: 'a79f9f2e-7585-4234-be3d-41f143448d9e', }, { beat: 4, id: '444ff59e-a333-4a8f-aef2-80b9c28b21d1', }, { beat: 4.25, id: '3fc91e38-866a-442c-8af9-aed48c9fd15f', }, ], [ { beat: 1.5, id: '50d19196-01b9-4260-bec1-9a38e6b2e3ee', }, { beat: 2.5, id: '85d11a31-3723-403e-baf7-ca7e4bc68e7a', }, { beat: 3.5, id: '83ff99bb-affb-4076-8f5c-4e09b587815b', }, { beat: 4.5, id: '734bc331-735e-42fd-bf17-9b75f53472ce', }, ], [ { beat: 3.5, id: '96c79893-8b36-4260-865d-62ef35bd0204', }, { beat: 4.5, id: 'b3da5898-9761-4563-b5a0-0af1c399d72f', }, ], [ { beat: 1, id: 'acc5c4b0-cb58-4430-b9a6-45148b35d4ec', }, { beat: 1.5, id: 'de3559e6-e087-4ea2-ad01-8ca24a66e1da', }, { beat: 2, id: '4e0f84df-c163-4666-9601-c63d9a782b62', }, { beat: 2.5, id: '748cae03-1a09-44f3-8fd2-c7c04a170885', }, { beat: 3, id: 'dbe92a4f-69fc-4525-99bf-a02047745bb6', }, { beat: 3.5, id: 'a19739e4-0850-47be-8515-0aa1d3dddb9e', }, { beat: 4, id: '7c7b50cd-49c7-4691-8b97-e6db431265f3', }, { beat: 4.25, id: '1d44e23b-d1c3-42db-b5ea-06a32aac407d', }, ], [ { beat: 2, id: '17f3f971-06b9-4eae-97ba-4018affbd3a8', }, { beat: 4.5, id: '0053433c-dd30-46d1-87b5-6718ad1eaf52', }, { beat: 1, id: 'a89b9d91-d035-49f1-9b50-b4a65f0139d4', }, { beat: 1.5, id: 'ec68efa2-a52c-4622-b63b-88f19e2d55ac', }, { beat: 3, id: 'c28629f1-77cf-4523-9f72-52e4b789a394', }, { beat: 4, id: 'aab2f193-bfe4-4541-98e6-dbd3197b0ffc', }, ], ], 'ab6ea4cd-f211-43a3-95ae-9a1cf7459de2': [ [ { beat: 4.5, id: '07fa6ab6-750e-4391-995d-31f7f5827c5c', }, ], [ { beat: 4.5, id: '3bb0e308-be8b-4c09-923c-80b168181efe', }, ], [ { beat: 2, id: '813addc6-8d78-4c37-8498-1c04723528a0', }, { beat: 4, id: '7854fc2e-b394-4ca7-92cd-b4bf7171d299', }, ], [ { beat: 1, id: 'e538d894-6c24-45f2-adfb-b0e10cf21fd8', }, { beat: 4.5, id: '76221cb4-1e93-479e-82ce-cee6cb7b8eb6', }, ], [], [ { beat: 1.5, id: '6bd88f44-43c0-42d6-8e03-89f029f254ae', }, { beat: 2.5, id: '05411202-e052-416c-92d1-bdc4918bcbc9', }, ], [ { beat: 4.5, id: '74cf1b66-000f-42e5-a56c-03ec29b778aa', }, ], [ { beat: 3.5, id: 'b97d36aa-f1c9-40b5-8d84-b90c8bf93507', }, { beat: 2.5, id: 'd4b1e512-6334-4658-b617-dc32bb582dbd', }, ], ], '8d761f84-9f82-48b3-bfa1-a11c7a7deeff': [ [ { beat: 1, id: 'd6726903-94b1-4cbb-857e-27c5e1a40431', }, { beat: 2.5, id: '2c10b8fc-bce7-4561-9b21-dce14118d55d', }, ], [ { beat: 1, id: 'b20fd85d-8aaa-4ca6-b3fa-9190e55aa0ea', }, { beat: 2.5, id: 'd86c62d6-4ed3-4cd3-9987-b60382ec74d3', }, ], [ { beat: 1.5, id: '969aa4d1-010d-4107-8474-081841c33465', }, { beat: 1.75, id: '78f75939-efd5-49cb-ac49-86fb9ca3b7a6', }, ], [], [ { beat: 1, id: 'f6fe983f-36b9-4445-9962-d583a50f6ded', }, { beat: 1.5, id: 'f5ad27ce-13a8-45af-b6b6-5944c645fc59', }, { beat: 2, id: '5a7e221d-467d-4dda-a12a-000ff9b9ca15', }, { beat: 2.5, id: '14ff4fcc-742a-4745-989b-4bfa1e1d5b89', }, { beat: 3, id: 'd6d8889a-3f5e-443c-be69-ea212752fb51', }, { beat: 3.75, id: '20f95ff5-77ef-4736-aa3a-2887f16f48a0', }, { beat: 4.5, id: '53f31cb5-81c5-49ea-bbc6-9aaa4c7d4619', }, ], [ { beat: 2, id: 'fdcfa8fa-90da-4c43-a515-92030094a7d7', }, { beat: 3, id: '06b59b4d-ddc1-4217-848d-8ea2babcf16a', }, { beat: 3.75, id: 'c1d478bf-8837-464d-8b37-26314f332fd5', }, { beat: 4.5, id: 'a315391d-9da3-42d2-877c-757c3e4f641c', }, ], [], [ { beat: 4.5, id: '835e969b-f3d2-4be8-bed7-e5aa641045f3', }, ], ], '8735d786-5ac0-4b36-a3fb-c58a55ee2b08': [ [], [ { beat: 3.5, id: '5f8ee195-3f73-4c07-badf-4a365aef6dfd', }, ], [ { beat: 2.75, id: '39aa3d74-a386-4cd6-832c-89a2c741e5d6', }, { beat: 3.5, id: '7dbeb24e-7995-47cc-a84f-cb762fdfbb28', }, ], [ { beat: 1, id: 'b636a41e-a9d6-444f-9b6e-3d86c0a34c79', }, { beat: 3, id: '2b365537-7fc5-48b4-8e80-988d9991eed7', }, ], [], [ { beat: 1, id: '15624899-9287-47fb-8bdc-96a979bcf8e2', }, ], [], [ { beat: 4.75, id: '578a74ab-f44e-49c4-9a00-625e87181fe5', }, ], ], }, }; ================================================ FILE: src/presets/empty.js ================================================ import sampleList from '../samples.config'; export const EMPTY_NOTE_ROW = [[], [], [], [], [], [], [], []]; export default { name: 'Empty', bpm: 80, swing: 0, channels: [ { id: 'empty_channel', sample: sampleList[0].url, gain: 1, }, ], notes: { empty_channel: EMPTY_NOTE_ROW, }, }; ================================================ FILE: src/presets/hip-hop.js ================================================ import { samples } from '../samples.config'; export default { name: 'Hip Hop', bpm: 98, swing: 0.4, channels: [ { id: 'e75658e5-e17b-47dd-b8ca-4d03b06068d9', sample: samples.HipHopBd1, gain: 1, }, { id: 'empty_channel', sample: samples.HipHopBd2, gain: 1, }, { id: '8c754c56-6cdc-479a-95b2-890ce93856c2', sample: samples.HipHopSd1, gain: 1, }, { id: '6182dd6e-188c-4157-9b97-86d904f446d5', sample: samples.HipHopSd2, gain: 0.6, }, { id: '5c34f5d0-6605-471b-95f1-eea186f332ea', sample: samples.HipHopCh1, gain: 1, }, { id: '3a8d1712-d85b-4a1c-b812-4e16da1f3731', sample: samples.HipHopCh2, gain: 0.46, }, { id: '26feb931-cfb9-4a93-a0ab-6f9ca189bb40', sample: samples.HipHopOh, gain: 0.33, }, ], notes: { empty_channel: [ [ { beat: 1, id: 'd3c6c0be-f5bb-4ac7-bc0f-b491ad6d430d', }, { beat: 3, id: 'd1e824b8-2d4c-4c73-8d46-14761d7bd50d', }, { beat: 3.5, id: 'cbc9b4de-5b86-40a9-98d9-5bbce91496dd', }, { beat: 4.5, id: '3dd7953e-9d07-471c-9dfd-fd0d5f55ead4', }, ], [ { beat: 1, id: 'e1e6024c-4ef0-409e-961c-d1f885587b3e', }, { beat: 1.75, id: 'ff1ff67c-c19c-4a17-a28d-69b707b5b013', }, { beat: 2, id: 'c559f161-50ef-41e0-88e2-9d6487f84f00', }, { beat: 2.75, id: '8ce49ba6-e328-4cb6-be6d-e04e6328603c', }, { beat: 3, id: 'ae33399b-725e-4fb3-a5b5-dcb88e62b611', }, { beat: 3.75, id: '99175bf1-c886-4064-a4b1-52a0713b9e9f', }, { beat: 4, id: 'd481c0d1-501f-48ef-8d2a-e3975907e609', }, ], [ { beat: 1, id: '6ac66ee0-20c9-4ce2-a1fc-6eb8563fcbad', }, { beat: 2.75, id: '18b6a0cb-fa7d-42c2-89f2-dd341649dddb', }, { beat: 3.5, id: '3e09868f-745e-4d46-b151-1ed985e6ea1d', }, ], [], [], [], [ { beat: 1, id: 'de69c191-f2ed-4990-b838-c8b08e28d2f7', }, { beat: 2.5, id: 'eb13514a-180e-473c-9b91-64031760640e', }, { beat: 3.5, id: 'bebb2046-f859-4c1e-befc-d8159c746dd5', }, ], [], ], '8c754c56-6cdc-479a-95b2-890ce93856c2': [ [ { beat: 2, id: 'a6df6bb6-cbe8-4115-bb97-0c6a201cbc67', }, { beat: 4, id: 'beb74b51-6bac-461d-8f9d-6197c9909364', }, ], [ { beat: 4, id: '6697284c-72e5-44bd-90c8-be9c064fc65f', }, ], [ { beat: 4, id: 'c2ecc059-975f-4d9a-88d6-fab3eac53224', }, { beat: 2, id: 'df1c1f60-ce06-4457-8895-335d4996c0f3', }, ], [ { beat: 2, id: '04bec360-6f92-453d-aea7-61befc0731c1', }, { beat: 4, id: '3389f856-266e-4f1b-9953-67ced6637cee', }, ], [ { beat: 2.75, id: 'a1ed3fef-94d0-401b-9365-eee79596ed80', }, { beat: 2, id: 'b4e4c591-ba8d-4d85-af7c-6429a8096290', }, ], [ { beat: 4, id: '4d0f189e-44f1-4f04-9de0-25f637684e35', }, { beat: 2, id: '9ea7858b-b2a4-425d-ad01-8f73d64f2480', }, ], [ { beat: 2, id: 'f7034f89-4832-4fef-8e74-af464fb28c60', }, { beat: 4, id: 'dc1fe315-ba48-49a5-861c-e305a38181db', }, ], [ { beat: 1.5, id: '887ba194-4ee1-4b59-9c92-698170137d83', }, { beat: 2, id: '8307c06e-5df6-4209-87b0-020b9b5dd268', }, { beat: 2.75, id: 'a3325cf0-1e16-4b27-a72a-8ba8461a4ed7', }, { beat: 4, id: '3eb6e9c4-9b24-4f24-82a4-20bd6fff4f86', }, { beat: 4.25, id: '16079935-eab8-4083-8d97-ad29dd8137c8', }, { beat: 4.5, id: '8818fa69-74e0-468d-8ce2-5ce3c33640ae', }, { beat: 4.75, id: '31a5d2bf-9d81-4a80-8e79-3a31e7cd4bde', }, ], ], '5c34f5d0-6605-471b-95f1-eea186f332ea': [ [ { beat: 1.5, id: '7cfe904a-1a84-44b4-b7e6-b3f9556045a6', }, { beat: 2.5, id: '549656ec-7445-46c0-9e3b-034ddeaf74bb', }, { beat: 3.5, id: '53c9e7e8-d207-42ea-b42f-a25c7c2b10fe', }, { beat: 4.5, id: '0aabcd2e-6bee-4212-afbe-6e5035aab297', }, ], [ { beat: 1.5, id: '340e9484-71f7-4e95-88d7-93f4068c15ea', }, { beat: 2.5, id: 'a1726663-0aad-4983-9d94-a80c725edcdb', }, { beat: 3.5, id: 'e6bfb5f0-27d0-498e-9d17-77ace30629a3', }, { beat: 4.5, id: '201e2706-784e-4194-9449-d6c7476e1697', }, ], [ { beat: 3.5, id: 'dff397b4-b6eb-4f9e-8546-9fed2b091023', }, ], [ { beat: 2, id: '4342fcc5-40be-4ba9-98ac-867b2bf96dba', }, { beat: 4.5, id: '24433b21-42f6-428d-832a-8e87b026bc52', }, { beat: 3.25, id: '17511f0c-f907-4a21-845d-0f0ab587f4af', }, { beat: 1.5, id: '4eca3412-5097-408e-9ad8-0fe1e8184a71', }, ], [ { beat: 1.5, id: 'f2a1d585-318c-43ce-be75-e40062bd11ec', }, { beat: 2.5, id: 'f09b258c-7609-4cd6-8479-1f040f7b29f7', }, { beat: 3.5, id: 'fbdbd55b-5912-4391-b5e2-fa6d007a73a1', }, ], [ { beat: 1.5, id: '3bdda5b4-acc2-4a34-a939-7c5c382aa349', }, { beat: 2.5, id: '4592ea4f-7db4-403e-ba95-b565fc14c70e', }, ], [ { beat: 1.25, id: '9947817d-817f-42b8-a307-7342db701165', }, { beat: 2, id: 'c0e01823-710b-4164-b319-74210ad518e8', }, { beat: 2.25, id: '84f70ba1-87a3-4c8c-aca6-ac499cf7e2ce', }, { beat: 2.75, id: '161ed9ce-767f-4f59-80f5-18a1beaa4c9c', }, { beat: 3.25, id: '42871ece-729b-4d35-b067-24d571e01149', }, { beat: 3.75, id: 'ec13afb1-ae07-483d-92d8-f62d7e8af142', }, { beat: 4.25, id: '5824151b-8e50-46c3-b4b1-c2c5fa4bd71a', }, { beat: 4.75, id: '4697821e-184c-4ee5-8273-53c177bcbe47', }, { beat: 1.75, id: '88dda45f-6912-41ac-a0f0-0190f87d35a0', }, { beat: 1.5, id: 'c8ab9783-59f1-4b41-b3e4-1d0cf0f2b666', }, { beat: 3, id: '627da411-a872-45d2-b30e-d6c19add874c', }, ], [ { beat: 1.5, id: '37ff0f84-0c41-4d12-a013-35b841332186', }, { beat: 3.75, id: '33065041-592e-4631-bc66-67a89cdc6504', }, ], ], '3a8d1712-d85b-4a1c-b812-4e16da1f3731': [ [ { beat: 1, id: '595f6d29-46da-49f5-a05d-0e7411e4ec89', }, { beat: 3, id: 'db6aa05f-e63a-4205-a05f-00d816b00f58', }, ], [ { beat: 3, id: 'ec5c2059-0ee4-4a3d-b987-e210d96e2e7b', }, { beat: 1, id: '4f625cda-20c7-47c7-8bef-dcf76f49af1d', }, { beat: 2, id: '868dd5cd-e763-4d4a-aed0-27976a8f2485', }, ], [ { beat: 1, id: '7ed9a16e-e674-49a8-a2fe-fb60a18782f6', }, { beat: 2, id: '1107910d-48e4-4bb8-b773-884f863e387e', }, { beat: 4, id: 'a3b4052f-4012-4bd0-b516-948319144448', }, { beat: 3, id: 'eff4244b-8533-4807-bef2-586ab5b5c48a', }, ], [ { beat: 1, id: '6044a1a4-83db-4827-8b14-9bff985533e2', }, { beat: 4, id: '79dec85a-c49a-4687-bc6b-139f22eb457b', }, { beat: 2.75, id: 'cdc4e782-6628-4415-9eae-3eb20a076e0e', }, ], [ { beat: 1, id: '55c8b1e8-0d3f-440a-8918-72ebfb68ad62', }, { beat: 2, id: 'f395b792-a4b0-4b96-9bb1-c4f082ee055c', }, { beat: 3, id: '7c4a3ec0-50d9-4ae7-b7c0-363a2fd5815f', }, { beat: 4, id: 'c3f87d26-0e84-44e5-b36c-697d3a8a608a', }, ], [ { beat: 1, id: 'e76e6472-75ba-4de6-a6ba-8fd61dab9bff', }, { beat: 3, id: '3f8c7947-0d92-4cee-9e82-ea9fa16eb4b9', }, { beat: 3.5, id: '469785b0-9f13-4ce3-ab59-f4569a84d585', }, { beat: 4, id: 'ac2022f7-dd6b-47ed-9425-d474d2337785', }, ], [ { beat: 1, id: 'd0db5bf2-72cd-47c8-96c9-9738c92b98de', }, { beat: 2.5, id: 'a45af499-562a-46cf-9094-11b6a4fec17c', }, { beat: 4, id: '9d3447fb-26c5-4fb9-bd9f-c2a13cd1d91a', }, { beat: 3.5, id: '8030f3d7-9e0d-4e1e-9ad9-32a86d30f0a1', }, ], [ { beat: 1, id: 'c3ce07ba-e4c1-4ad9-82b6-b51761c3826d', }, { beat: 4.25, id: 'b4810e34-e93e-4f80-9a94-833865e46dbc', }, { beat: 4.75, id: 'bcd3e697-9028-4c8b-a3b7-48e9d4f435d4', }, { beat: 2, id: 'a7292ad7-2557-435f-ae47-be9028cd18a9', }, { beat: 3.25, id: '74ff49fc-b78a-4406-9519-ab21d21ec350', }, ], ], '6182dd6e-188c-4157-9b97-86d904f446d5': [ [ { beat: 4, id: '0e7f5542-7eee-47eb-8581-702434ac7f24', }, ], [ { beat: 4.5, id: 'b5d20e2a-a7ff-4247-a97e-8009d3068f52', }, ], [ { beat: 4, id: '1285f3cf-2e23-4810-9076-0cb0cd34fb9f', }, ], [ { beat: 4, id: '8f070f1a-ab0d-4cfc-9240-478ef9b209eb', }, ], [ { beat: 2, id: 'f8364458-f508-4c9c-aab3-7c57c99328f9', }, ], [ { beat: 4, id: 'c243b510-f46d-4ba7-963a-f5718c56dd45', }, ], [ { beat: 2, id: '8538a736-527c-4d88-a579-6cff14cb445e', }, { beat: 4.75, id: '3aa58d8b-03b6-400d-b919-6c83af261832', }, ], [ { beat: 2, id: '8f29d886-c7b6-492d-ac0b-1d122e8a7b04', }, { beat: 4, id: 'f2f2acb4-a893-45e0-9a7b-e6b05739d784', }, { beat: 2.75, id: '42087e3b-05ea-4475-b3e4-fa9d78997b87', }, { beat: 4.75, id: '9522b33d-5c5a-49ed-8472-6622af4b25b9', }, ], ], '26feb931-cfb9-4a93-a0ab-6f9ca189bb40': [ [ { beat: 4.5, id: '169b577c-59ab-47b3-b1b2-7902029ecc81', }, ], [ { beat: 4, id: '3f51236d-7d20-45a1-9733-b4870fb6eeb8', }, ], [ { beat: 4.5, id: 'd226b632-eada-405a-b9a6-afdfb7bf178e', }, ], [ { beat: 4.5, id: 'd8074acc-5baf-4e70-bb25-f1fdb846316b', }, { beat: 3.5, id: '4f1900c2-64d1-4912-83e1-d43693d02248', }, ], [ { beat: 4.5, id: '582fbf75-84a2-4b65-81c1-8982df340d33', }, ], [ { beat: 2, id: '87055d0b-3d04-498f-b7c7-60bdad4bb420', }, { beat: 4.5, id: '4ece5f5a-68a2-4b20-9b4f-dc09565bdfba', }, ], [ { beat: 4.5, id: 'c5b96f26-e6ba-4cf0-a909-f8de9a429046', }, ], [ { beat: 1.5, id: '16e46966-326c-4690-a71b-835c1bae05e7', }, { beat: 2.75, id: '03755834-8d4a-432d-9efd-29cf02eba211', }, { beat: 4, id: '052e1671-ce04-4d3d-b075-7d1f3d65110e', }, { beat: 4.5, id: '630eeb48-9718-468d-9a47-a4011264abed', }, ], ], 'e75658e5-e17b-47dd-b8ca-4d03b06068d9': [ [ { beat: 2.75, id: '686d2334-4323-4eec-a829-f8aa12adc25c', }, ], [], [ { beat: 3.25, id: '9e6fbd33-5aa2-44b1-a636-5ac99bcc145a', }, ], [], [ { beat: 1, id: '842ecdc5-f92f-4b94-8233-c277bca2cc01', }, { beat: 3.5, id: 'ec3a001f-fe77-4262-b3df-a1a61bdc095f', }, { beat: 4.5, id: '674f9739-5c27-4f54-994c-a456400e9134', }, ], [ { beat: 1, id: 'aa146f7d-3d84-4ec0-bfce-597f6c82459e', }, { beat: 2.75, id: '3663c589-85de-4233-b9ff-22cf3c79ba3b', }, { beat: 3, id: '593b5441-f935-4b95-badd-f1c91c7ea2de', }, { beat: 3.5, id: 'ac1590d7-2b09-46f9-98f3-53f28cbf0e3c', }, { beat: 4.5, id: '6897a6f9-6dd1-47bd-b1ac-3b99116fbebf', }, ], [], [ { beat: 2.5, id: '22862c2b-56a7-4a65-a407-f5cf72414bf1', }, { beat: 1, id: '54be7db8-3e8e-4097-be8a-90d8f3884604', }, { beat: 3.25, id: '0d86e28e-fd88-4aa0-a636-53fd1a2468b2', }, { beat: 3.75, id: 'dcc8d5ec-6a42-4647-849c-dd22f2259c0c', }, ], ], }, }; ================================================ FILE: src/presets/index.js ================================================ import empty from './empty'; import hipHop from './hip-hop'; import lDrum from './ldrum'; import sevenohseven from './707'; import eightoheight from './808'; import ace from './ace'; export default [empty, eightoheight, ace, lDrum, hipHop, sevenohseven]; ================================================ FILE: src/presets/ldrum.js ================================================ import { samples } from '../samples.config'; export default { name: 'LDrum', bpm: 124, swing: 0.2, channels: [ { id: 'empty_channel', sample: samples.LinnBd, gain: 1, }, { id: 'eca3906c-9577-4a38-a025-87f6c7b8fa88', sample: samples.LinnSd, gain: 0.65, reverb: 0.1, pan: 0, }, { id: 'c068fa91-9977-4fb1-9f41-ec3fe8473cea', sample: samples.LinnCh, gain: 0.1, reverb: 0.2, pan: 0.3, }, { id: '4ed97fa7-8798-4cf6-8c7b-0e42c76f1612', sample: samples.LinnPh, gain: 0.15, reverb: 0.2, pan: 0.3, }, { id: '7ac094ef-282c-4d9d-8d7c-390118dd925a', sample: samples.LinnHt, gain: 0.26, reverb: 0.35, pan: -0.5, }, { id: '046c982a-2576-453d-b021-963c6d3076ee', sample: samples.LinnMt, gain: 0.27, reverb: 0.35, pan: -0.3, }, { id: '12a11640-05d9-4a1e-88ff-c4c7525385b1', sample: samples.LinnLt, gain: 0.3, reverb: 0.35, pan: -0.1, }, { id: 'e72fec64-9e3d-4848-81ee-c35643a70624', sample: samples.LinnCowbell, gain: 0.24, reverb: 0.2, pan: 0.4, }, { id: '06126c09-a20b-4cf4-8c05-a812d3ebc7c8', sample: samples.LinnClap, gain: 0.44, reverb: 0.1, pan: -0.5, }, { id: 'c4b37dfa-8f2f-491b-942a-b271b7b24c71', sample: samples.LinnRim, gain: 0.43, reverb: 0.1, pan: 0.5, }, { id: '3bd51432-6e07-4ec5-93b0-04eef8be49f6', sample: samples.LinnTamb, gain: 0.28, reverb: 0.3, pan: 0.7, }, ], notes: { empty_channel: [ [ { beat: 1, id: 'f9537c53-f916-434d-9697-86e500da2414', }, { beat: 2, id: 'a8aecd47-1ef3-433a-8844-ad0b6e9550ff', }, { beat: 3, id: '94e06d7f-fa55-4314-9caf-27191b706b8c', }, { beat: 4, id: '9a610633-0c4e-4ebc-a907-da90f1a98e9e', }, ], [ { beat: 1, id: '38af500c-c153-4f12-bc5c-2c8ec6b8eafa', }, { beat: 2, id: '4eb43cec-9542-4a0e-9616-35fa43b45d56', }, { beat: 4, id: '5b1c927e-6eb7-489e-bf67-9e59620467bb', }, { beat: 3, id: 'de2cf269-ea81-45f3-9b68-ccbf4fe41ca9', }, ], [ { beat: 1, id: '1c6b732a-ce76-4f07-90f2-7ba9d3f14da0', }, { beat: 2, id: 'e2793169-3989-4e5d-bb97-91c642172104', }, { beat: 3, id: '28009164-8f89-4a17-82cb-2c904be196a8', }, { beat: 4, id: 'c4285da8-5e44-44a7-8a09-f7d0da6b3aa2', }, { beat: 4.5, id: 'f5768e8f-b408-4dcf-a86b-1a65eac0f13c', }, { beat: 4.75, id: 'd2ddfcdf-2cf6-420b-ae35-56c49e4f8f4e', }, ], [ { beat: 1, id: '43069058-8f8e-4423-a9be-ce4f469b6f0f', }, { beat: 3.5, id: '4c901c5a-7ace-42e8-89bc-d8381a6aca65', }, { beat: 4.25, id: '9b0af2ff-3596-40f9-86f7-de123046b27a', }, { beat: 2.5, id: '7e4505e6-446a-4855-9465-9f9e204a1de6', }, ], [ { beat: 1, id: '6a394bca-a802-46a2-9bda-f0376793de35', }, { beat: 2, id: '1c7de135-b429-480f-8780-2f95722f4492', }, { beat: 3, id: 'b6405cf3-6f51-4a09-8a50-2e1f9321d754', }, { beat: 4, id: 'dc15b93a-affb-47bf-87a2-ddeb1b1c54b6', }, ], [ { beat: 1, id: '85dfaf1a-d43b-4f8f-b277-1edba8188568', }, { beat: 1.75, id: '94bca68b-ea37-4ceb-b858-ea835e8bec5e', }, { beat: 2.5, id: '57618e04-00b0-44cf-ae9b-1449fbf67cc0', }, { beat: 3.5, id: 'b8cb485a-1a11-4054-ac6d-e95f78f0a327', }, { beat: 3.75, id: 'e13a78a0-aed9-4db9-a902-bba81e678b46', }, { beat: 4.5, id: '2b2e507c-d383-4a99-a8ac-b2ad0eb03afb', }, ], [ { beat: 1, id: 'abf70cdc-f4ab-477b-8b14-5bdcce752a4b', }, { beat: 4.5, id: '55a3ce00-1723-420b-b8df-1ae1dad93e2e', }, ], [ { beat: 1, id: '946594e8-8569-4ea5-8cbd-667faad73cd4', }, { beat: 2, id: '5239b5f7-a009-40d1-8ca9-77f3f5e59c39', }, { beat: 3, id: 'b67ca07c-9791-4f32-8134-1187e4bb5119', }, { beat: 3.25, id: '600a8ac1-5206-4545-86e0-3df14f165dd2', }, { beat: 3.5, id: 'b43191ed-36f0-4fad-a909-68c8736e72a5', }, { beat: 3.75, id: '977fa833-0b6b-4e2c-a634-74d0082141b0', }, { beat: 4, id: 'd63fcab7-ed08-42e0-a56f-f78e9265f0bc', }, { beat: 4.25, id: 'f02eb38c-f046-4d7c-9792-38a32e258926', }, { beat: 4.5, id: 'debc09eb-682b-4790-843d-c54f92e4801b', }, { beat: 4.75, id: '98bbe8b5-bb5e-4585-89bf-a148d15fe14b', }, ], ], 'eca3906c-9577-4a38-a025-87f6c7b8fa88': [ [ { beat: 2, id: 'c363f89a-cc72-4d2f-aa9e-2bc566f9910e', }, { beat: 4, id: '18f833c1-3fbe-49de-8556-0e564534f3f2', }, ], [ { beat: 2, id: 'a384c923-751d-4033-a556-ebacb2469a1a', }, { beat: 4, id: 'c24c4906-fbd2-48cd-a400-5e5bb15de3b2', }, ], [], [ { beat: 2, id: '00f68589-77b4-47a9-9e1a-b3d9e44a12a3', }, { beat: 4, id: 'd71d7f60-fa3b-4c22-8e67-4ecb5d5a870b', }, ], [ { beat: 4.5, id: 'cf054f6a-c3a9-4754-8005-c920e3315b93', }, { beat: 2, id: '1e9cfb91-96b9-496d-908e-84029a046582', }, ], [ { beat: 2, id: '3cf01e35-ade2-4786-a537-c4169c05e717', }, { beat: 4, id: 'f2255cb3-8e4f-4667-9b97-21f360b20b64', }, ], [ { beat: 2, id: '30cefbb0-4ae1-405c-abf5-74e7bc99620d', }, { beat: 4, id: 'e942a593-39b2-40c3-a3cd-72dbd81c0873', }, ], [ { beat: 2, id: 'be3eeed2-0522-4d07-b055-c80b06b16020', }, { beat: 4, id: '25d9b2dd-3f45-4e50-9b4f-6bb1a806b2a7', }, ], ], 'c068fa91-9977-4fb1-9f41-ec3fe8473cea': [ [ { beat: 1.5, id: '57a9a0f6-d577-4381-9e91-df55be6c6c26', }, { beat: 2.5, id: 'f97b8fee-49cd-41b0-9a9f-ea006edbaae3', }, { beat: 3.5, id: '1067fcad-8958-4712-bced-095f6210e36a', }, { beat: 4.5, id: 'f8e73df5-a57a-4928-a941-69411c0bd609', }, ], [ { beat: 1.5, id: 'cb533ea2-4485-43e9-ab9d-b62d23eeadb9', }, { beat: 2.5, id: 'b7ae837e-1ea8-47bf-9e7b-4ffd6572b9bb', }, { beat: 3.5, id: 'c62a9f34-4f8b-47ad-9548-7923fea09b9f', }, { beat: 4.5, id: '368904f3-84b8-43cd-aa78-74e518ff3b6e', }, ], [], [ { beat: 1.5, id: 'd50a7e07-0a86-43da-bbce-4989d9242bf2', }, { beat: 1.75, id: '7756766c-ee9a-423a-962d-53e9a4a08ab0', }, { beat: 2.5, id: 'bb1ad65f-8365-4475-b5f4-554d20a925b2', }, { beat: 2.75, id: '4cc38af7-889a-4868-ac4d-071637b45512', }, { beat: 3.5, id: '02cbff37-4de2-420c-b183-c3e916749788', }, { beat: 3.75, id: '8ef297a9-c1d0-40db-8e8c-cdbefe4ea651', }, { beat: 4.5, id: '3155ca15-d2f0-4faa-bd8b-7b54d61ac6bc', }, { beat: 4.75, id: '4c3235c6-8cad-4bc6-882f-2bbbe9f900ef', }, ], [ { beat: 1.25, id: '95b23210-ecd8-4861-9ece-797717a2ed77', }, { beat: 1.75, id: '5743bde4-4f27-45b9-923c-128f73c39c3a', }, { beat: 3.25, id: '458362b7-9587-4b1f-960a-8736506c5504', }, { beat: 3.5, id: 'd5c04c09-91c7-4653-ad33-203c99fe8e4d', }, { beat: 3.75, id: '63b7aa22-a55b-4c13-ac43-f48b64ee418f', }, { beat: 4.25, id: 'b3d7000b-72b6-4d25-8aa3-3a874bb385ef', }, { beat: 4.5, id: '503375ea-f43c-468a-ad87-af11412d9a2f', }, { beat: 4.75, id: '80b0e871-3a6d-4572-8428-39eb19cd3bfb', }, { beat: 1.5, id: '2f925e26-784b-4fec-9968-5b925cb24f8d', }, { beat: 2.25, id: '5b20b66b-10f7-49c1-ad42-3ef2f294cd24', }, { beat: 2.75, id: '9ab8f5e5-31fa-4598-a41b-560ce212ec50', }, { beat: 2.5, id: '8439ba8f-1ba8-4503-95c8-a9ab0454f05c', }, ], [ { beat: 1.25, id: '820057ba-35fb-4c79-abeb-f06b2ffaeca3', }, { beat: 1.5, id: '425f77ca-eee7-4d67-880e-fd8ed9216fbe', }, { beat: 1.75, id: '4726fe9d-477b-4b06-b76d-c36897bcaeaa', }, { beat: 2.25, id: 'f1adb17a-308a-4512-9ce1-036173517e48', }, { beat: 2.5, id: '88762626-761f-4fb6-866b-408e0bf64541', }, { beat: 2.75, id: '7f37b1f1-6421-431c-a624-a0dca86ba2ca', }, { beat: 3.25, id: 'c101fb88-ddf6-4725-a543-08fd2573385d', }, { beat: 3.5, id: 'db6127e2-ac00-4ff2-adb8-25ac13212792', }, { beat: 4, id: '8bce9e4f-f1c8-4857-baa2-3634f3e1f8a1', }, { beat: 4.25, id: 'fd8d21bb-1cb1-4198-94d7-8315cd5850ae', }, { beat: 4.5, id: 'f3083a12-d5d1-4319-b291-a0c25f3d18cd', }, { beat: 4.75, id: '30670921-d80f-4ad8-b672-d68bd87c68ea', }, ], [ { beat: 1.5, id: 'ab8073ab-176c-4ca5-a879-20ee3dbea4a1', }, { beat: 2, id: 'd4fb2c83-47bc-4bcf-ad5b-f722b24bba2f', }, { beat: 2.5, id: '8ce200de-203b-4c12-831d-63013b4b3102', }, { beat: 3, id: '243963db-7c85-4e61-b715-e485c3cc155f', }, { beat: 3.5, id: '5220a19d-1a8f-4428-a11f-54ea0c3f95ba', }, { beat: 4.5, id: '6ab1ccd8-e95c-4791-ba40-a14ebb477f67', }, ], [ { beat: 1.5, id: '57003873-ca6f-4554-ae80-462a7d4df020', }, { beat: 1.75, id: '6531ed77-f7fb-4735-bbe2-967fa28e35fb', }, { beat: 2.5, id: '3ab75a14-6ca6-4fcf-a801-fa0ff7c8f280', }, { beat: 2.75, id: 'b1d347d9-b22e-468f-b7c0-44ff09b436ed', }, { beat: 3.5, id: 'a5ba160e-fc4a-4df3-a1f2-1408ce0e90f1', }, { beat: 3.75, id: 'dab5bf93-eef3-4fab-9781-86f07508ad2a', }, { beat: 4.5, id: '434c58cb-4bba-48aa-9bda-fa1ecb5b5d89', }, { beat: 4.75, id: 'b5a2db63-2aae-46ee-9950-abfffb78f84a', }, ], ], '4ed97fa7-8798-4cf6-8c7b-0e42c76f1612': [ [ { beat: 1, id: '8ac39cd2-9fa9-496f-b715-17127eb5ca1e', }, { beat: 2, id: 'f5e65dcb-8ed4-438e-b82e-6d515b41778d', }, { beat: 3, id: 'fa570e68-b4b4-4845-a57b-52ebb2466ea3', }, { beat: 4, id: '7efb37f5-2f0c-471d-b45c-88982e1e5313', }, ], [ { beat: 1, id: 'd3dcb8fa-f1be-4f24-9dc7-7eae74b8bf7d', }, { beat: 2, id: 'ba2fc358-76f3-4e29-ad7d-af49262990f9', }, { beat: 3, id: 'a170429d-8d3d-4b73-81c7-eb30bfc11894', }, { beat: 4, id: '51fdcc1d-6a80-45c6-8679-69b346581c71', }, ], [ { beat: 1, id: '780939be-af1d-4090-a9bf-ce4ce9704ba4', }, { beat: 2, id: '369aa28e-56e7-4c3b-82a0-0f4ea8824342', }, { beat: 3, id: 'cec4c74a-684d-47c3-8179-20b248843dd3', }, { beat: 4, id: '68d5aaf2-01e1-4554-8068-a49854e3463b', }, ], [ { beat: 1, id: '1c818f8d-d82e-43c5-b786-b4698646ad9d', }, { beat: 2, id: 'd53c1d33-5e6b-434e-97bc-24f63ae3a0ff', }, { beat: 3, id: '7fb65445-5480-4c9a-b648-1411402d6cae', }, { beat: 4, id: 'd940e016-b171-40b5-978c-b095c4b81139', }, ], [ { beat: 1, id: '2ce68236-9d55-4b07-8325-36e11642bef5', }, { beat: 2, id: '328e608e-5e7b-4b61-9855-b7972f9ca391', }, { beat: 3, id: '861aae24-bcab-4ee6-ad64-1dcaeda79aba', }, { beat: 4, id: 'e1ac4617-7ac4-4103-81d7-0dbef5f46c03', }, ], [ { beat: 1, id: '3e597539-9b7a-4122-8ce5-6be97dec9eee', }, { beat: 2, id: '243d3858-3c81-45c3-bdd5-b241d34274dd', }, { beat: 3, id: 'b6612d76-e85b-4c76-a956-d09c2a7d242a', }, { beat: 3.75, id: 'b9587a06-60c0-48c8-92d4-654a93a7f187', }, ], [ { beat: 1, id: '3e6480dc-e080-4da9-a705-95e2ffb8120b', }, { beat: 4, id: 'ff253c70-0929-48bf-91d3-55369e57acf4', }, ], [], ], '7ac094ef-282c-4d9d-8d7c-390118dd925a': [ [], [ { beat: 1, id: '3dbfc617-d4d8-43b2-8553-ad4c394f3468', }, ], [], [], [ { beat: 3, id: '54ef53d6-4c41-4502-b402-92de238ae13c', }, { beat: 4.5, id: '9f16fcca-a4b6-4989-9b79-d0209d30fba4', }, ], [], [], [], ], '046c982a-2576-453d-b021-963c6d3076ee': [ [], [ { beat: 1.75, id: '15914063-1610-4045-8741-ca4ef89aa9ae', }, ], [], [], [ { beat: 3.5, id: 'd24fe24f-4816-49fc-8d80-786f2a81899b', }, ], [], [ { beat: 3.5, id: '4e149455-6607-4182-8b32-62bc0504910c', }, ], [], ], '12a11640-05d9-4a1e-88ff-c4c7525385b1': [ [], [ { beat: 2.5, id: 'bb83c91a-1113-4a22-86a0-f82d5f79fb75', }, { beat: 3.5, id: 'f1119e47-3fa6-4252-a2da-9f1f69fda753', }, ], [], [], [ { beat: 3.75, id: '16f782bc-9119-4027-a255-19ac4b48ace4', }, { beat: 4.5, id: 'c68bc580-927f-407c-be0e-5dbbfb9db1d1', }, ], [ { beat: 3.5, id: '47dc9fce-95b7-4251-8551-0647baa5e37a', }, ], [ { beat: 4.5, id: 'd8fef37d-24a3-4159-969d-5150903be141', }, ], [], ], 'e72fec64-9e3d-4848-81ee-c35643a70624': [ [], [], [ { beat: 1, id: '3225b1df-8e08-4f63-9ba5-90a5fc8f6ffe', }, { beat: 1.5, id: '38ce24c2-c238-45c3-8253-fa17b059e8c3', }, { beat: 2, id: 'f9088de3-375f-4412-92b4-0e2764693657', }, { beat: 2.25, id: '9fcefcd5-5724-404a-80d6-11c2e92cdae8', }, { beat: 2.75, id: 'b061538f-361d-42d0-a3c4-8a48cabb7fa8', }, { beat: 3.5, id: '5dbb95e8-8218-40b4-8a93-13ac7d45acd6', }, { beat: 4, id: 'bbcf5992-8742-4e4b-9ddd-95c4e6d81ea7', }, { beat: 4.5, id: 'd39c2d9a-3441-480b-9474-d7bfa171d08a', }, ], [ { beat: 1, id: '6b32ce50-6709-498a-a461-08d54d14f759', }, { beat: 1.5, id: '6445067d-d845-4b11-a102-44abdc5284d6', }, { beat: 2.25, id: '44c90554-c61a-443a-a64a-772929c33e07', }, { beat: 3, id: '80fc0e9d-6e9e-4634-8a54-3d054c5b84ff', }, { beat: 3.75, id: 'dba2568f-738c-4766-8938-f777d8a908b3', }, { beat: 4.5, id: '21f4d57d-2044-45a8-9b9c-b415e6e55c41', }, ], [], [], [ { beat: 1, id: '3ba9c1c3-6d7c-420c-80a0-465984c9cb18', }, { beat: 2, id: '857b1252-951e-40c1-a889-33862385bf54', }, { beat: 3, id: '5447729c-133b-4d05-be12-f0bc42d31c5e', }, { beat: 4, id: '9a7dfc2e-4921-4a9b-b0b5-2a37c66926ec', }, ], [], ], '06126c09-a20b-4cf4-8c05-a812d3ebc7c8': [ [], [], [ { beat: 2, id: '65b0d5df-89c2-45ff-b551-6887c52643a4', }, { beat: 4, id: 'd6c0e4d0-f1e1-4776-b2ce-67046a2a3c93', }, ], [], [], [ { beat: 1.5, id: 'beb5b7b4-a01f-4927-ab19-01a0a0e247cd', }, { beat: 2.5, id: '67ffddf1-db5c-42f0-9291-2dde75fbc25f', }, { beat: 3, id: '36cb3941-4d89-48c0-b172-a472797860d6', }, ], [], [ { beat: 2, id: '4f08dd26-2747-48cf-aca0-7ba192b4ec61', }, { beat: 4, id: '56530e31-f07c-42ee-806e-35c27ec3480c', }, ], ], 'c4b37dfa-8f2f-491b-942a-b271b7b24c71': [ [], [], [ { beat: 4, id: '121cb8e8-c5d1-44ce-90dd-5851da41a1af', }, ], [ { beat: 1, id: 'cd2d7c62-5c04-47c2-b353-05ec4c01b8e6', }, ], [], [ { beat: 3.75, id: '93701ee1-8442-4c34-8ec8-6232ce1ddff5', }, ], [ { beat: 2.5, id: '93caca6b-7ead-4c3c-b0eb-47faed44a9ab', }, ], [], ], '3bd51432-6e07-4ec5-93b0-04eef8be49f6': [ [], [], [], [], [], [ { beat: 1, id: '3456eb7c-091d-4698-8b66-5482a5ee2fbf', }, { beat: 3, id: '3c063e4b-361c-4dda-8df3-1ff3da9c7e95', }, ], [ { beat: 2, id: '324da79d-4cde-45c7-8d6b-5775d0fdd45b', }, { beat: 4, id: 'a1da337e-799d-445f-8670-baf480620420', }, ], [ { beat: 1, id: '506317dc-0772-43af-82c1-17dd28fe50e7', }, { beat: 2, id: '1e50f328-7070-4a69-be19-bba1649ffbd4', }, { beat: 3, id: '8343c55e-a041-43fa-8615-878057fe237a', }, { beat: 4, id: '52b072fb-eb3e-410f-906c-feedbf77faea', }, ], ], }, }; ================================================ FILE: src/reducer.js ================================================ import { combineReducers } from 'redux'; import { channelsReducer, playbackSessionReducer, tempoReducer, masterReducer, notesReducer, presetsReducer, windowReducer, userSamplesReducer, } from './common'; export default combineReducers({ channels: channelsReducer, playbackSession: playbackSessionReducer, tempo: tempoReducer, master: masterReducer, notes: notesReducer, presets: presetsReducer, window: windowReducer, userSamples: userSamplesReducer, }); ================================================ FILE: src/samples.config.js ================================================ import tr707Bd from './assets/drums/707/707-bd.mp3'; import tr707SdLow from './assets/drums/707/707-sd-low.mp3'; import tr707SdHigh from './assets/drums/707/707-sd-high.mp3'; import tr707Ch from './assets/drums/707/707-ch.mp3'; import tr707Oh from './assets/drums/707/707-oh.mp3'; import tr707Clap from './assets/drums/707/707-clap.mp3'; import tr707Tamb from './assets/drums/707/707-tamb.mp3'; import tr808BdShort from './assets/drums/808/808-bd-short.mp3'; import tr808BdLong from './assets/drums/808/808-bd-long.mp3'; import tr808Cowbell from './assets/drums/808/808-cowbell.mp3'; import tr808Sd from './assets/drums/808/808-sd.mp3'; import tr808Clap from './assets/drums/808/808-clap.mp3'; import tr808Ch from './assets/drums/808/808-ch.mp3'; import tr808Oh from './assets/drums/808/808-oh.mp3'; import tr808Cym from './assets/drums/808/808-cym.mp3'; import tr808Clav from './assets/drums/808/808-clav.mp3'; import tr808Rs from './assets/drums/808/808-rs.mp3'; import tr808Ht from './assets/drums/808/808-ht.mp3'; import tr808Mt from './assets/drums/808/808-mt.mp3'; import tr808Lt from './assets/drums/808/808-lt.mp3'; import AcetoneBd from './assets/drums/acetone/acetone-bd.mp3'; import AcetoneSd1 from './assets/drums/acetone/acetone-sd-1.mp3'; import AcetoneSd2 from './assets/drums/acetone/acetone-sd-2.mp3'; import AcetoneCh from './assets/drums/acetone/acetone-ch.mp3'; import AcetoneOh from './assets/drums/acetone/acetone-oh.mp3'; import AcetonePerc1 from './assets/drums/acetone/acetone-perc-1.mp3'; import AcetonePerc2 from './assets/drums/acetone/acetone-perc-2.mp3'; import LinnBd from './assets/drums/linndrum/linn-bd.mp3'; import LinnSd from './assets/drums/linndrum/linn-sd.mp3'; import LinnCh from './assets/drums/linndrum/linn-ch.mp3'; import LinnPh from './assets/drums/linndrum/linn-ph.mp3'; import LinnClap from './assets/drums/linndrum/linn-clap.mp3'; import LinnTamb from './assets/drums/linndrum/linn-tamb.mp3'; import LinnCowbell from './assets/drums/linndrum/linn-cowbell.mp3'; import LinnHt from './assets/drums/linndrum/linn-ht.mp3'; import LinnMt from './assets/drums/linndrum/linn-mt.mp3'; import LinnLt from './assets/drums/linndrum/linn-lt.mp3'; import LinnRim from './assets/drums/linndrum/linn-rim.mp3'; import HipHopBd1 from './assets/drums/hip-hop/hip-hop-bd-1.mp3'; import HipHopBd2 from './assets/drums/hip-hop/hip-hop-bd-2.mp3'; import HipHopSd1 from './assets/drums/hip-hop/hip-hop-sd-1.mp3'; import HipHopSd2 from './assets/drums/hip-hop/hip-hop-sd-2.mp3'; import HipHopCh1 from './assets/drums/hip-hop/hip-hop-ch-1.mp3'; import HipHopCh2 from './assets/drums/hip-hop/hip-hop-ch-2.mp3'; import HipHopOh from './assets/drums/hip-hop/hip-hop-oh.mp3'; export const samples = { tr707Bd, tr707SdLow, tr707SdHigh, tr707Ch, tr707Oh, tr707Clap, tr707Tamb, tr808BdShort, tr808BdLong, tr808Cowbell, tr808Sd, tr808Clap, tr808Ch, tr808Oh, tr808Cym, tr808Clav, tr808Rs, tr808Ht, tr808Mt, tr808Lt, AcetoneBd, AcetoneSd1, AcetoneSd2, AcetoneCh, AcetoneOh, AcetonePerc1, AcetonePerc2, LinnBd, LinnSd, LinnCh, LinnPh, LinnClap, LinnTamb, LinnCowbell, LinnHt, LinnMt, LinnLt, LinnRim, HipHopBd1, HipHopBd2, HipHopSd1, HipHopSd2, HipHopCh1, HipHopCh2, HipHopOh, }; const sampleOptions = [ { name: '707 Bass', url: tr707Bd, }, { name: '707 Snare (low)', url: tr707SdLow, }, { name: '707 Snare (high)', url: tr707SdHigh, }, { name: '707 Hi-hat Closed', url: tr707Ch, }, { name: '707 Hi-hat Open', url: tr707Oh, }, { name: '707 Clap', url: tr707Clap, }, { name: '707 Tambourine', url: tr707Tamb, }, { name: '808 Bass Short', url: tr808BdShort, }, { name: '808 Bass Long', url: tr808BdLong, }, { name: '808 Cowbell', url: tr808Cowbell, }, { name: '808 Snare', url: tr808Sd, }, { name: '808 Clap', url: tr808Clap, }, { name: '808 Hi-hat Closed', url: tr808Ch, }, { name: '808 Hi-hat Open', url: tr808Oh, }, { name: '808 Cymbal', url: tr808Cym, }, { name: '808 Clave', url: tr808Clav, }, { name: '808 Rimshot', url: tr808Rs, }, { name: '808 High Tom', url: tr808Ht, }, { name: '808 Mid Tom', url: tr808Mt, }, { name: '808 Low Tom', url: tr808Lt, }, { name: 'Ace Bass', url: AcetoneBd, }, { name: 'Ace Snare (Short)', url: AcetoneSd1, }, { name: 'Ace Snare (Long)', url: AcetoneSd2, }, { name: 'Ace Hi-hat Closed', url: AcetoneCh, }, { name: 'Ace Hi-hat Open', url: AcetoneOh, }, { name: 'Ace Percussion (Low)', url: AcetonePerc1, }, { name: 'Ace Percussion (High)', url: AcetonePerc2, }, { name: 'LDrum Bass', url: LinnBd, }, { name: 'LDrum Snare', url: LinnSd, }, { name: 'LDrum Hi-hat Closed', url: LinnCh, }, { name: 'LDrum Hi-hat Pressed', url: LinnPh, }, { name: 'LDrum Clap', url: LinnClap, }, { name: 'LDrum Tambourine', url: LinnTamb, }, { name: 'LDrum Cowbell', url: LinnCowbell, }, { name: 'LDrum High Tom', url: LinnHt, }, { name: 'LDrum Mid Tom', url: LinnMt, }, { name: 'LDrum Low Tom', url: LinnLt, }, { name: 'LDrum Rimshot', url: LinnRim, }, { name: 'Hip Hop Bass 1', url: HipHopBd1, }, { name: 'Hip Hop Bass 2', url: HipHopBd2, }, { name: 'Hip Hop Snare 1', url: HipHopSd1, }, { name: 'Hip Hop Snare 2', url: HipHopSd2, }, { name: 'Hip Hop Hi-hat Closed 1', url: HipHopCh1, }, { name: 'Hip Hop Hi-hat Closed 2', url: HipHopCh2, }, { name: 'Hip Hop Hi-hat Open', url: HipHopOh, }, ]; export default sampleOptions; ================================================ FILE: src/services/__mocks__/audioContext.js ================================================ export const getAudioContext = () => ({ currentTime: 1, }); export const playNote = () => new AudioBufferSourceNode(); ================================================ FILE: src/services/__mocks__/audioRouter.js ================================================ export const playNote = () => {}; ================================================ FILE: src/services/__mocks__/featureChecks.js ================================================ export const detuneSupported = true; ================================================ FILE: src/services/animations.js ================================================ import { getCurrentBeat } from './audioContext'; import { swing } from './swing'; const draw = (store) => { // Get some data from redux store const state = store.getState(); const { bpm, swing: swingAmount } = state.tempo; const { playing, startTime } = state.playbackSession; const currentBeat = getCurrentBeat(bpm, startTime); // Grab all the toggles and animate them const toggles = document.getElementsByClassName('wds-beat-marker'); for (let i = 0; i < toggles.length; i += 1) { const toggle = toggles[i]; const { beat, active } = toggle.dataset; const beatNum = parseFloat(beat); const swingBeat = swing(beatNum, swingAmount); const isActive = active === 'true'; if ( playing && isActive && currentBeat - swingBeat < 0.25 && currentBeat - swingBeat > 0 ) { toggle.style.transition = 'all 0s'; toggle.style.opacity = '0.8'; toggle.style.transform = 'scale(1.3)'; } else { toggle.style.transition = `all ${120 / bpm}s`; toggle.style.opacity = 0; toggle.style.transform = 'scale(1)'; } } window.requestAnimationFrame(() => { draw(store); }); }; export const startAnimations = (store) => { window.requestAnimationFrame(() => { draw(store); }); }; ================================================ FILE: src/services/audioAnalyzer.js ================================================ import { analyserNode } from './audioRouter'; const pcmData = new Float32Array(analyserNode.fftSize); export function getVolume() { analyserNode.getFloatTimeDomainData(pcmData); let peak = 0; for (const amplitude of pcmData) { if (amplitude > peak) { peak = amplitude; } } return peak; } ================================================ FILE: src/services/audioContext.js ================================================ let audioCtx; export const getAudioContext = () => { if (typeof audioCtx === 'undefined') { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } return audioCtx; }; export const getCurrentBeat = (bpm, startTime, currentTime) => { const safeCurrentTime = typeof currentTime === 'undefined' ? audioCtx.currentTime : currentTime; const beatLengthSeconds = bpm / 60; const currentBeat = (safeCurrentTime - startTime) * beatLengthSeconds; return (currentBeat % 4) + 1; }; ================================================ FILE: src/services/audioContext.test.js ================================================ import { getCurrentBeat, } from './audioContext'; jest.mock('./featureChecks'); describe('getCurrentBeat', () => { test('should return beat 1 if startTime is the same as currentTime', () => { expect(getCurrentBeat(60, 1, 1)).toBe(1); }); test('should return beat 2 if currentTime is one second ahead of startTime and bpm = 60', () => { expect(getCurrentBeat(60, 0, 1)).toBe(2); }); test('should return beat 3 if currentTime is one second ahead of startTime and bpm = 120', () => { expect(getCurrentBeat(120, 0, 1)).toBe(3); }); test('should return beat 2.5 if currentTime is one second ahead of startTime and bpm = 90', () => { expect(getCurrentBeat(90, 0, 1)).toBe(2.5); }); test('should return beat 1 if currentTime is one second ahead of startTime and bpm = 240', () => { expect(getCurrentBeat(240, 0, 1)).toBe(1); }); test('should return beat 2 if currentTime is one second ahead of startTime and bpm = 300', () => { expect(getCurrentBeat(300, 0, 1)).toBe(2); }); }); ================================================ FILE: src/services/audioEngine.config.js ================================================ export const LOOKAHEAD = 0.2; // seconds export const INTERVAL = 50; // milliseconds ================================================ FILE: src/services/audioLoop.js ================================================ import { getAudioContext, getCurrentBeat } from './audioContext'; import { updateChannelNodes } from './audioRouter'; import { scheduleNotes } from './audioScheduler'; import { setStartTime } from '../common'; import { INTERVAL } from './audioEngine.config'; export const initializeAudio = (store) => { const audioCtx = getAudioContext(); // Start the clock setInterval(() => { const { playbackSession, tempo, channels, notes, master } = store.getState(); updateChannelNodes(channels); if (playbackSession.playing) { let sT = playbackSession.startTime; // Loop if we reached the end of the bar const barLength = (4 * 60) / tempo.bpm; if (audioCtx.currentTime > playbackSession.startTime + barLength) { store.dispatch(setStartTime(playbackSession.startTime + barLength)); sT = playbackSession.startTime + barLength; } scheduleNotes({ notes, channels, startTime: sT, tempo, pattern: master.pattern, currentBeat: getCurrentBeat(tempo.bpm, playbackSession.startTime), }); } }, INTERVAL); }; ================================================ FILE: src/services/audioRouter.js ================================================ import { detuneSupported, stereoPannerSupported } from './featureChecks'; import { getAudioContext } from './audioContext'; import { loadImpulseResponse } from './reverb'; import impulseResponse from '../assets/impulse-responses/ruby-room.mp3'; const audioCtx = getAudioContext(); const masterOut = audioCtx.createGain(); masterOut.connect(audioCtx.destination); export const analyserNode = audioCtx.createAnalyser(); analyserNode.smoothingTimeConstant = 0; masterOut.connect(analyserNode); const reverbNode = audioCtx.createConvolver(); reverbNode.connect(masterOut); loadImpulseResponse(impulseResponse).then((impulseResponseArrayBuffer) => { reverbNode.buffer = impulseResponseArrayBuffer; }); /** * The channel routing is: * * Drum Sample * -> Gain node * -> Reverb node * -> Master out * -> Analyser node * -> Pan node * -> Master out * -> Analyser node */ const channelGainNodes = {}; const channelPanNodes = {}; const channelReverbNodes = {}; const calculateGain = (channel, soloEnabled) => { if (channel.muted) { return 0; } if (soloEnabled && !channel.solo) { return 0; } if (channel.gain === 'undefined') { return 1; } return channel.gain; }; const updateGainNode = (channel, soloEnabled) => { if (typeof channelGainNodes[channel.id] === 'undefined') { // Set up a GainNode to control note volume channelGainNodes[channel.id] = audioCtx.createGain(); channelGainNodes[channel.id].connect(channelPanNodes[channel.id]); // Also route to reverb channelGainNodes[channel.id].connect(channelReverbNodes[channel.id]); } channelGainNodes[channel.id].gain.setValueAtTime( calculateGain(channel, soloEnabled), audioCtx.currentTime, ); }; const updatePanNode = (channel) => { if (stereoPannerSupported) { if (typeof channelPanNodes[channel.id] === 'undefined') { channelPanNodes[channel.id] = audioCtx.createStereoPanner(); channelPanNodes[channel.id].connect(masterOut); } channelPanNodes[channel.id].pan.setValueAtTime( typeof channel.pan === 'undefined' ? 0 : channel.pan, audioCtx.currentTime, ); } else { if (typeof channelPanNodes[channel.id] === 'undefined') { channelPanNodes[channel.id] = audioCtx.createPanner(); channelPanNodes[channel.id].panningModel = 'equalpower'; channelPanNodes[channel.id].connect(masterOut); } const pan = typeof channel.pan === 'undefined' ? 0 : channel.pan; channelPanNodes[channel.id].setPosition(pan, 0, 1 - Math.abs(pan)); } }; const updateReverbNode = (channel) => { if (typeof channelReverbNodes[channel.id] === 'undefined') { // Set up a GainNode to control the send volume to reverb channelReverbNodes[channel.id] = audioCtx.createGain(); channelReverbNodes[channel.id].connect(reverbNode); } channelReverbNodes[channel.id].gain.setValueAtTime( typeof channel.reverb === 'undefined' ? 0 : channel.reverb, audioCtx.currentTime, ); }; const checkSoloEnabled = (channels) => { for (let i = 0; i < channels.length; i += 1) { if (channels[i].solo) { return true; } } return false; }; export const updateChannelNodes = (channels) => { channels.forEach((channel) => { updateReverbNode(channel); updatePanNode(channel); updateGainNode(channel, checkSoloEnabled(channels)); }); }; export const playNote = (noteTime, buffer, channelID, notePitch = 0) => { // Set up the AudioBufferSourceNode const source = audioCtx.createBufferSource(); source.buffer = buffer; // Detune if available if (detuneSupported) { source.detune.value = notePitch; } // Route to channel gain node source.connect(channelGainNodes[channelID]); // Connect and start source.start(noteTime); return source; }; ================================================ FILE: src/services/audioScheduler.js ================================================ import { LOOKAHEAD } from './audioEngine.config'; import { playNote } from './audioRouter'; import { sampleStore } from './sampleStore'; import { swing } from './swing'; // schedule is a lookup table of all the notes currently scheduled to be played const schedule = {}; export const pitchToCents = ({ pitchCoarse = 0, pitchFine = 0 }) => Math.round(pitchCoarse * 100 + pitchFine); export const playNoteNow = (noteChannel) => { const pitch = pitchToCents(noteChannel); playNote(null, sampleStore[noteChannel.sample], noteChannel.id, pitch); }; export const scheduleNote = (noteID, noteTime, noteChannel) => { if (typeof schedule[noteID] === 'undefined') { const pitch = pitchToCents(noteChannel); schedule[noteID] = playNote( noteTime, sampleStore[noteChannel.sample], noteChannel.id, pitch, ); } }; export const isBetween = (query, a, b) => query >= a && query < b; export const getScheduledNotes = ({ channelNotes, channel, startTime, tempo, currentBeat, }) => channelNotes.map((note) => { const lookaheadBeats = LOOKAHEAD * (tempo.bpm / 60); const swingAmount = typeof tempo.swing === 'undefined' ? 0 : tempo.swing; const swingBeat = swing(note.beat, swingAmount); const noteTime = startTime + (swingBeat - 1) * (60 / tempo.bpm); if (isBetween(note.beat, currentBeat, currentBeat + lookaheadBeats)) { return { id: note.id, time: noteTime, channel, }; } // If nearing the end of the bar, schedule notes at the start of the bar too if ( isBetween(note.beat, currentBeat - 4, currentBeat + lookaheadBeats - 4) ) { return { id: note.id, time: startTime + ((note.beat + 3) * 60) / tempo.bpm, channel, }; } // Return note objects with time: null that should not be scheduled return { id: note.id, time: null, channel, }; }); export const scheduleNotes = ({ notes, channels, startTime, pattern, tempo, currentBeat, }) => { // Determine which notes need to be scheduled const notesToSchedule = channels.reduce( (accumulator, channel) => [ ...accumulator, ...getScheduledNotes({ channelNotes: notes[channel.id][pattern], // Play the current pattern channel, startTime, tempo, currentBeat, }), ], [], ); // Schedule the notes notesToSchedule.forEach((note) => { if (note.time !== null) { scheduleNote(note.id, note.time, note.channel); } else { delete schedule[note.id]; } }); }; ================================================ FILE: src/services/audioScheduler.test.js ================================================ import { isBetween, getScheduledNotes, } from './audioScheduler'; jest.mock('./featureChecks'); jest.mock('./audioContext'); jest.mock('./audioRouter'); describe('isBetween', () => { test('should return true if query is between a and b', () => { expect(isBetween(2, 1, 3)).toBe(true); }); test('should return false if query is note between a and b', () => { expect(isBetween(4, 1, 3)).toBe(false); }); }); describe('getScheduledNotes', () => { const testNotes = [ { beat: 1, id: 'foo', }, { beat: 2.5, id: 'bar', }, { beat: 4.25, id: 'bam', }, ]; const scheduledNotes = getScheduledNotes({ channel: { sample: { url: '/whatever.wav', }, }, channelNotes: testNotes, tempo: { bpm: 60, swing: 0.2, }, startTime: 0, currentBeat: 1, }); test('should return same number of notes', () => { expect(scheduledNotes.length).toBe(testNotes.length); }); test('should calculate noteTime correctly for notes in the lookahead period', () => { expect(scheduledNotes[0].time).toBe(0); }); test('should set noteTime to null if note should not be scheduled', () => { expect(scheduledNotes[1].time).toBeNull(); }); }); ================================================ FILE: src/services/database.js ================================================ const DB_NAME = 'wds-1'; const DB_VERSION = 1; const USER_SAMPLES = 'USER_SAMPLES'; let db; export const initializeDB = () => new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = (event) => { reject(event); }; request.onupgradeneeded = (event) => { // Create an objectStore for this database db = event.target.result; db.createObjectStore(USER_SAMPLES); }; request.onsuccess = (event) => { db = event.target.result; resolve(); }; }); export const saveToDB = (myArrayBuffer, myKey) => new Promise((resolve, reject) => { const trans = db.transaction([USER_SAMPLES], 'readwrite'); trans.objectStore(USER_SAMPLES).put(myArrayBuffer, myKey); trans.onerror = (event) => { reject(event); }; trans.onsuccess = () => { resolve(myKey); }; }); export const getFromDB = (myKey) => new Promise((resolve, reject) => { const trans = db.transaction([USER_SAMPLES], 'readwrite'); const request = trans.objectStore(USER_SAMPLES).get(myKey); request.onerror = (event) => { reject(event); }; request.onsuccess = () => { if (request.result) { resolve(request.result); } reject(); }; }); ================================================ FILE: src/services/featureChecks.js ================================================ const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const source = audioCtx.createBufferSource(); export const detuneSupported = typeof source.detune !== 'undefined'; export const stereoPannerSupported = typeof audioCtx.createStereoPanner !== 'undefined'; ================================================ FILE: src/services/fileUtils.js ================================================ import { getAudioContext } from './audioContext'; export const fetchFile = (url) => new Promise( (resolve, reject) => { fetch(url).then((response) => { if (response.ok) { resolve(response.blob()); } reject(new Error('Network response was not ok.')); }); }, ); export const decodeFile = (sampleBlob) => new Promise( (resolve) => { const fileReader = new FileReader(); fileReader.readAsArrayBuffer(sampleBlob); fileReader.onloadend = () => { resolve(fileReader.result); }; }, ); export const decodeAudio = (audioArrayBuffer) => new Promise( (resolve, reject) => { getAudioContext().decodeAudioData(audioArrayBuffer, resolve, reject); }, ); ================================================ FILE: src/services/pwaInstall.js ================================================ import { setCanInstall } from '../common'; let deferredPrompt; export const initializePwaInstall = (store) => { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; store.dispatch(setCanInstall(true)); }); window.addEventListener('appinstalled', () => { store.dispatch(setCanInstall(false)); }); }; export const promptToInstall = () => { if (typeof deferredPrompt !== 'undefined') { deferredPrompt.prompt(); } }; ================================================ FILE: src/services/reverb.js ================================================ import { fetchFile, decodeFile, decodeAudio } from './fileUtils'; const impulseResponses = {}; export const loadImpulseResponse = (fileName) => { if (typeof impulseResponses[fileName] !== 'undefined') { return Promise.resolve(impulseResponses[fileName]); } return fetchFile(fileName) .then(decodeFile) .then(decodeAudio); }; ================================================ FILE: src/services/sampleStore.js ================================================ import { fetchFile, decodeFile, decodeAudio } from './fileUtils'; import { saveToDB, getFromDB } from './database'; export const sampleStore = {}; export const loadSample = (url) => { if (typeof sampleStore[url] !== 'undefined') { return Promise.resolve(true); } return getFromDB(url) .then(decodeAudio) .then((drumBuffer) => { sampleStore[url] = drumBuffer; return true; }) .catch(() => fetchFile(url) .then(decodeFile) .then(decodeAudio) .then((drumBuffer) => { sampleStore[url] = drumBuffer; return true; }) .catch(() => false)); }; export const saveToSampleStore = (file) => { const id = file.name; return decodeFile(file) .then((myArrayBuffer) => { saveToDB(myArrayBuffer, id); return decodeAudio(myArrayBuffer); }) .then((drumBuffer) => { sampleStore[id] = drumBuffer; return id; }); }; ================================================ FILE: src/services/swing.js ================================================ export const swing = (beatTime, swingAmount) => { const SWING_TIMING = 0.5; // eight note cycles const MAX_SWING = 0.95; const beatCyclePos = beatTime % SWING_TIMING; const beatCyclePercentage = beatCyclePos / SWING_TIMING; const fx = (beatCyclePercentage ** (1 - swingAmount)) * MAX_SWING; // Exponential function const offset = (fx - beatCyclePercentage) * beatCyclePos; return beatTime + offset; }; ================================================ FILE: src/services/unmute.js ================================================ import silence from '../assets/silence.mp3'; export const unmute = () => { var el = document.createElement('audio'); el.src = silence; el.play(); }; ================================================ FILE: src/services/uuid.js ================================================ import uuidv4 from 'uuid/v4'; export const uuid = uuidv4; ================================================ FILE: src/store.js ================================================ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import { persistStore, persistReducer, createMigrate } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import reducer from './reducer'; const migrations = { 1: () => ({}), 2: () => ({}), 3: () => ({}), }; export const configureStore = (callback) => { const persistConfig = { key: 'root-v1.0.0', storage, version: 3, blacklist: ['playbackSession', 'window'], migrate: createMigrate(migrations, { debug: import.meta.env.DEV }), // eslint-disable-line }; const persistedReducer = persistReducer(persistConfig, reducer); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // eslint-disable-line const store = createStore( persistedReducer, composeEnhancers(applyMiddleware(thunk)), ); const persistor = persistStore(store, null, callback); return { store, persistor, }; }; ================================================ FILE: src/styles/globalStyles.js ================================================ import { injectGlobal } from 'styled-components'; import theme from './theme'; import jostMediumWoff2 from '../assets/fonts/jost-medium-webfont.woff2'; import jostMediumWoff from '../assets/fonts/jost-medium-webfont.woff'; import jostBoldWoff2 from '../assets/fonts/jost-bold-webfont.woff2'; import jostBoldWoff from '../assets/fonts/jost-bold-webfont.woff'; import jostSemiboldWoff2 from '../assets/fonts/jost-semi-webfont.woff2'; import jostSemiboldWoff from '../assets/fonts/jost-semi-webfont.woff'; export default () => injectGlobal` @font-face { font-family: 'Jost'; font-style: normal; font-weight: 400; src: local('Jost Medium'), local('Jost-Medium'), url(${jostMediumWoff2}) format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ url(${jostMediumWoff}) format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } @font-face { font-family: 'Jost'; font-style: normal; font-weight: 600; src: local('Jost SemiBold'), local('Jost-SemiBold'), url(${jostSemiboldWoff2}) format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ url(${jostSemiboldWoff}) format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } @font-face { font-family: 'Josts'; font-style: normal; font-weight: 700; src: local('Jost Bold'), local('Jost-Bold'), url(${jostBoldWoff2}) format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ url(${jostBoldWoff}) format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } html { background-color: ${theme.colors.nearBlack}; } * { font-family: "Jost", "Futura", sans-serif; font-display: swap; } body { min-width: 750px; box-sizing: border-box; } .bpm-text-input { -moz-appearance:textfield; } /* Webkit browsers like Safari and Chrome */ .bpm-text-input::-webkit-inner-spin-button, .bpm-text-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } `; ================================================ FILE: src/styles/theme.js ================================================ const colors = { nearWhite: '#F2F2F8', lightGray: '#C0C3C7', gray: '#909599', steel: '#606469', darkGray: '#404449', nearBlack: '#202427', black80: 'rgba(0,0,0,0.8)', green: '#58A291', lightGreen: '#68B2A1', darkGreen: '#1B806D', red: '#CD545B', lightRed: '#DD646B', darkRed: '#633231', brightRed: 'rgb(244, 83, 58)', brightRed30: 'rgba(244, 83, 58, 0.3)', gold: '#E6A65D', yellow: 'rgb(255, 224, 71)', yellow30: 'rgba(255, 224, 71, 0.3)', primary: 'rgba(213,255,169,1)', primaryDark: 'rgba(180,215,129,1)', secondary: 'rgba(152,255,193,1)', blue: '#2f85c6', darkBlue: '#196096', }; export default { fontSizes: [ 11, 13, 14, 24, 32, 48, 64, 96, 128, ], space: [ // margin and padding 0, 4, 8, 16, 32, 64, 128, 256, ], breakpoints: ['640px', '720px', '769px', '820px', '900px', '1024px', '1200px', '1400px'], colors, fancyButtons: { green: { color: 'white', backgroundColor: colors.green, boxShadow: `0 0.3em ${colors.darkGreen}`, '&:hover': { backgroundColor: colors.lightGreen, }, '&:active': { backgroundColor: colors.lightGreen, boxShadow: `0 0 ${colors.darkGreen}`, transform: 'translateY(0.3em)', }, }, red: { color: 'white', backgroundColor: colors.red, boxShadow: `0 0.3em ${colors.darkRed}`, '&:hover': { backgroundColor: colors.lightRed, }, '&:active': { backgroundColor: colors.lightRed, boxShadow: `0 0 ${colors.darkRed}`, transform: 'translateY(0.3em)', }, }, }, }; ================================================ FILE: vite.config.js ================================================ import { defineConfig } from 'vite'; import reactRefresh from '@vitejs/plugin-react-refresh'; export default defineConfig({ plugins: [reactRefresh()], });