├── .editorconfig ├── .gitignore ├── .graphqlconfig ├── .storybook ├── addons.js ├── config.js ├── decorators.tsx └── tsconfig.json ├── README.md ├── docs └── snapshot.png ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── sounds │ ├── 13bc898b-16ca-4b2c-b90c-06a7894a78fd.wav │ ├── 19b606f5-52b5-49f5-a3b6-566c245e0407.wav │ ├── 1e95d1b8-440a-41f3-ab0a-3a48587bf6f7.wav │ ├── 241d3fec-5a90-4ec1-b57a-635009ffd167.wav │ ├── 39a5cf0d-8def-43fa-ab3d-1638b9213cb3.wav │ ├── 3c911ed3-8862-473b-8b48-2aeb00eeecb5.wav │ ├── 53a20b19-712e-4a43-b718-98b7ff897880.wav │ ├── 5d394665-a0bb-4d49-8a61-5ffe9912df6c.wav │ ├── 6eeff2d5-6c90-43c2-91f6-3a68f0911483.wav │ ├── 7129a80a-5698-40ef-a5c1-2fb1d4b62c42.wav │ ├── 74c74b98-7e16-4c81-aff8-b21efe16ddb1.wav │ ├── 7f9a144d-64b5-43e0-a3ca-3878085ce582.wav │ ├── 7ff6ffa7-9768-4bfc-b6c8-b99a70be556b.wav │ ├── 8710fea6-56d0-440e-920f-0da2576bf3d7.wav │ ├── 899bc068-e687-4928-ba4c-9082c0163304.wav │ ├── 8cf86f2f-0b50-42bb-81d8-22731d462161.wav │ ├── 901cfa41-c230-4c26-903b-22f99ee13deb.wav │ ├── 939d89c3-4abd-4312-84b8-d388cd84fcc6.wav │ ├── 97eb4ae4-afe0-408f-88c6-736233409ec9.wav │ ├── 9dce9279-194e-4d6f-9f07-d7968eb13f63.wav │ ├── acc4ea8c-cd40-44f2-b553-0642f411a144.wav │ ├── bfac1667-5115-49cd-82dc-f294f54cb447.wav │ ├── e3a06bcb-ff48-492b-b55c-b9a8a8479aac.wav │ ├── eaacf8f7-0d57-4e8c-a872-bb51315659b3.wav │ ├── f0cb1d42-7052-432f-95df-4320e5d42cb0.wav │ ├── f57d9727-d7f0-4027-b9cd-fb7b56a79df4.wav │ └── fc897b72-744c-434b-9018-6e860da11edb.wav ├── src ├── audio │ ├── context │ │ └── createContext.ts │ ├── processor │ │ └── processor.ts │ └── utils │ │ └── Volume │ │ ├── Volume.test.ts │ │ └── Volume.ts ├── components │ ├── App │ │ ├── App.css │ │ ├── App.module.css │ │ └── App.tsx │ ├── AudioEngine │ │ └── AudioEngine.tsx │ ├── MasterPanel │ │ ├── MasterGainController │ │ │ ├── MasterGainController.stories.tsx │ │ │ └── MasterGainController.tsx │ │ ├── MasterPanel.tsx │ │ ├── ModeSwitch │ │ │ ├── ModeSwitch.module.css │ │ │ └── ModeSwitch.tsx │ │ ├── TempoController │ │ │ ├── TempoController.stories.tsx │ │ │ └── TempoController.tsx │ │ └── Transport │ │ │ ├── Transport.stories.tsx │ │ │ └── Transport.tsx │ ├── Menu │ │ ├── Menu.module.css │ │ ├── Menu.stories.tsx │ │ └── Menu.tsx │ ├── Root │ │ └── Root.tsx │ ├── Sequencer │ │ ├── AddTrack │ │ │ ├── AddTrack.tsx │ │ │ ├── AddTrackButton.tsx │ │ │ └── AddTrackModal.tsx │ │ ├── Sequencer.tsx │ │ └── Track │ │ │ ├── CellRow │ │ │ ├── Cell │ │ │ │ ├── Cell.stories.tsx │ │ │ │ └── Cell.tsx │ │ │ ├── CellRow.stories.tsx │ │ │ └── CellRow.tsx │ │ │ ├── Track.stories.tsx │ │ │ ├── Track.tsx │ │ │ ├── TrackHeader │ │ │ ├── MuteButton.tsx │ │ │ ├── SoloButton.tsx │ │ │ ├── TrackHeader.stories.tsx │ │ │ ├── TrackHeader.tsx │ │ │ └── TrackLabel.tsx │ │ │ └── TrackPanel │ │ │ ├── CellSettings │ │ │ ├── CellSettings.stories.tsx │ │ │ ├── CellSettings.tsx │ │ │ ├── GainKnob │ │ │ │ └── GainKnob.tsx │ │ │ └── NoteSelector │ │ │ │ ├── Key.tsx │ │ │ │ ├── NoteSelector.stories.tsx │ │ │ │ └── NoteSelector.tsx │ │ │ ├── TrackPanel.stories.tsx │ │ │ ├── TrackPanel.tsx │ │ │ └── TrackSettings │ │ │ ├── Fader │ │ │ └── Fader.tsx │ │ │ ├── ResolutionSwitch │ │ │ ├── ResolutionSwitch.stories.tsx │ │ │ └── ResolutionSwitch.tsx │ │ │ ├── TrackSettings.stories.tsx │ │ │ └── TrackSettings.tsx │ ├── context │ │ └── sequencer-prefs.tsx │ ├── controllers │ │ ├── Fader │ │ │ ├── Fader.module.css │ │ │ ├── Fader.stories.tsx │ │ │ └── Fader.tsx │ │ ├── Knob │ │ │ ├── Knob.stories.tsx │ │ │ └── Knob.tsx │ │ ├── ValueController │ │ │ ├── ValueController.module.css │ │ │ ├── ValueController.stories.tsx │ │ │ └── ValueController.tsx │ │ └── VerticalFader │ │ │ ├── VerticalFader.stories.tsx │ │ │ └── VerticalFader.tsx │ └── pages │ │ ├── HomePage │ │ └── HomePage.tsx │ │ └── SessionPage │ │ └── SessionPage.tsx ├── graphql │ └── types │ │ ├── color.graphql │ │ ├── instrument.graphql │ │ ├── processing.graphql │ │ ├── root.graphql │ │ ├── sample.graphql │ │ ├── session.graphql │ │ └── track.graphql ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── redux │ ├── actions │ │ ├── audio │ │ │ ├── creators.ts │ │ │ └── interfaces.ts │ │ └── session │ │ │ ├── creators.ts │ │ │ └── interfaces.ts │ ├── middlewares │ │ └── logger.ts │ ├── reducers │ │ ├── audio.ts │ │ ├── index.ts │ │ ├── instruments.ts │ │ ├── samples.ts │ │ └── session.ts │ └── store │ │ ├── audio │ │ ├── initialState.ts │ │ └── interfaces.ts │ │ ├── configureStore.ts │ │ ├── instrument │ │ ├── initialState.ts │ │ └── interfaces.ts │ │ ├── sample │ │ ├── initialState.ts │ │ └── interfaces.ts │ │ └── session │ │ ├── initialState.ts │ │ └── interfaces.ts ├── serviceWorker.tsx ├── services │ ├── cell.test.ts │ └── cell.ts └── utils │ ├── audio │ └── MidiConverter.ts │ ├── color │ ├── colorLibrary.ts │ └── colorLuminance.ts │ ├── env.ts │ ├── trigo │ ├── polar.test.ts │ └── polar.ts │ └── uuid │ └── uuid.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # Change these settings to your own preference 9 | indent_style = space 10 | indent_size = 4 11 | 12 | # We recommend you to keep these unchanged 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [*.feature] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [*.js] 23 | indent_style = space 24 | indent_size = 2 25 | 26 | [*.json] 27 | indent_style = space 28 | indent_size = 2 29 | 30 | [*.md] 31 | trim_trailing_whitespace = false 32 | 33 | [*.php] 34 | indent_style = space 35 | indent_size = 4 36 | 37 | [*.sh] 38 | indent_style = tab 39 | indent_size = 4 40 | 41 | [*.xml] 42 | indent_style = space 43 | indent_size = 4 44 | 45 | [*.{yaml,yml}] 46 | indent_style = space 47 | indent_size = 4 48 | trim_trailing_whitespace = false 49 | 50 | [.gitmodules] 51 | indent_style = tab 52 | indent_size = 4 53 | 54 | [.php_cs{,.dist}] 55 | indent_style = space 56 | indent_size = 4 57 | 58 | [.travis.yml] 59 | indent_style = space 60 | indent_size = 2 61 | 62 | [composer.json] 63 | indent_style = space 64 | indent_size = 4 65 | 66 | [docker-compose{,.override}.{yaml,yml}] 67 | indent_style = space 68 | indent_size = 2 69 | 70 | [Dockerfile] 71 | indent_style = tab 72 | indent_size = 4 73 | 74 | [package.json] 75 | indent_style = space 76 | indent_size = 2 77 | 78 | [phpunit.xml{,.dist}] 79 | indent_style = space 80 | indent_size = 4 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .idea -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GraphQL Schema", 3 | "schemaPath": "schema.graphql", 4 | "extensions": { 5 | "endpoints": { 6 | "Default GraphQL Endpoint": { 7 | "url": "http://localhost:8080/graphql", 8 | "headers": { 9 | "user-agent": "JS GraphQL" 10 | }, 11 | "introspect": false 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register" 2 | import "@storybook/addon-links/register" 3 | import "@storybook/addon-knobs/register" 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from "@storybook/react" 2 | import { withOptions } from "@storybook/addon-options" 3 | import { withInfo } from "@storybook/addon-info" 4 | 5 | import "../src/index.css" 6 | 7 | addDecorator(withInfo) 8 | addDecorator( 9 | withOptions({ 10 | name: "Sequencer — Components", 11 | url: "https://glimberger.github.io/react-redux-sequencer/" 12 | }) 13 | ) 14 | 15 | const req = require.context("../src/components", true, /\.stories\.tsx$/) 16 | 17 | function loadStories() { 18 | req.keys().forEach(filename => req(filename)) 19 | } 20 | 21 | configure(loadStories, module) 22 | -------------------------------------------------------------------------------- /.storybook/decorators.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Provider } from "react-redux" 3 | 4 | import { PrefsProvider } from "../src/components/context/sequencer-prefs" 5 | import configureStore, { IAppState } from "../src/redux/store/configureStore" 6 | import { RenderFunction } from "@storybook/react" 7 | import colorLibrary from "../src/utils/color/colorLibrary" 8 | 9 | export const withContainer = (story: RenderFunction) => ( 10 |
18 | {story()} 19 |
20 | ) 21 | 22 | export const withPrefsProvider = (story: RenderFunction) => ( 23 | {story()} 24 | ) 25 | 26 | export const withReduxProvider = (state: IAppState) => { 27 | const store = configureStore(state) 28 | return (story: RenderFunction) => {story()} 29 | } 30 | 31 | const matrix = Array.from(Array(64).keys()).map(beat => ({ 32 | scheduled: beat % 3 === 0, 33 | midi: 69, 34 | processing: { gain: { gain: 1 } } 35 | })) 36 | 37 | export const stateFixture: IAppState = { 38 | audio: { 39 | currentBeat: 0, 40 | ready: true, 41 | playing: false, 42 | mode: "EDIT", 43 | events: [] 44 | }, 45 | session: { 46 | tempo: 120, 47 | masterGain: 1, 48 | activeTrackID: "1", 49 | activeCellBeat: 12, 50 | trackOrder: ["1", "2", "3", "4"], 51 | tracks: { 52 | "1": { 53 | id: "1", 54 | label: "Track 1", 55 | muted: false, 56 | soloed: false, 57 | instrumentID: "1", 58 | color: colorLibrary.RED, 59 | noteResolution: 1, 60 | processing: { gain: { gain: 0.7 } } 61 | }, 62 | "2": { 63 | id: "2", 64 | label: "Track 2", 65 | muted: false, 66 | soloed: false, 67 | instrumentID: "1", 68 | color: colorLibrary.ORANGE, 69 | noteResolution: 2, 70 | processing: { gain: { gain: 0.7 } } 71 | }, 72 | "3": { 73 | id: "3", 74 | label: "Track 3", 75 | muted: false, 76 | soloed: false, 77 | instrumentID: "1", 78 | color: colorLibrary.INDIGO, 79 | noteResolution: 4, 80 | processing: { gain: { gain: 0.7 } } 81 | }, 82 | "4": { 83 | id: "4", 84 | label: "Track 4", 85 | muted: false, 86 | soloed: false, 87 | instrumentID: "1", 88 | color: colorLibrary.GREEN, 89 | noteResolution: 1, 90 | processing: { gain: { gain: 0.7 } } 91 | } 92 | }, 93 | instruments: { 94 | "1": { 95 | id: "1", 96 | label: "Instrument 1", 97 | group: "group", 98 | sampleIDs: ["1"], 99 | mapping: { 100 | M67: { 101 | midi: 67, 102 | sampleID: "1", 103 | detune: -100 104 | }, 105 | M68: { 106 | midi: 68, 107 | sampleID: "1", 108 | detune: 0 109 | }, 110 | M69: { 111 | midi: 69, 112 | sampleID: "1", 113 | detune: 100 114 | } 115 | } 116 | } 117 | }, 118 | matrix: { 119 | "1": matrix, 120 | "2": matrix, 121 | "3": matrix, 122 | "4": matrix 123 | }, 124 | samples: { 125 | "1": { 126 | id: "1", 127 | filename: "sample", 128 | label: "Sample", 129 | url: "url", 130 | type: "type" 131 | } 132 | } 133 | }, 134 | instruments: { 135 | "1": { 136 | id: "1", 137 | label: "Instrument 1", 138 | group: "group", 139 | sampleIDs: ["1"], 140 | mapping: { 141 | M69: { 142 | midi: 69, 143 | sampleID: "1", 144 | detune: 100 145 | } 146 | } 147 | } 148 | }, 149 | samples: { 150 | "1": { 151 | id: "1", 152 | filename: "sample", 153 | label: "Sample", 154 | url: "url", 155 | type: "type" 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "allowSyntheticDefaultImports": true, 5 | "module": "es2015", 6 | "target": "es5", 7 | "lib": ["es6", "dom"], 8 | "sourceMap": true, 9 | "allowJs": false, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "rootDir": "../", 13 | "outDir": "dist", 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "declaration": true 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "build", 26 | "dist", 27 | "scripts", 28 | "acceptance-tests", 29 | "webpack", 30 | "jest", 31 | "**/*/*.test.ts" 32 | ] 33 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Redux sequencer 2 | [![Netlify Status](https://api.netlify.com/api/v1/badges/7bb31b6f-a429-452b-b3b4-ee0df050086d/deploy-status)](https://app.netlify.com/sites/vigilant-goldberg-a80afb/deploys) 3 | 4 | What it looks like: 5 | ![snapshot](https://github.com/glimberger/react-redux-sequencer/blob/master/docs/snapshot.png) 6 | 7 | Here's a [demo on Github Pages](https://glimberger.github.io/react-redux-sequencer/session) 🥁 8 | 9 | and a [demo on Netlify](https://vigilant-goldberg-a80afb.netlify.com/session) 🎹 10 | 11 | To start the project: 12 | ```javascript 13 | yarn start 14 | ``` 15 | 16 | 17 | To run stories: 18 | ```javascript 19 | yarn storybook 20 | ``` 21 | 22 | Tested with Chrome 63 23 | -------------------------------------------------------------------------------- /docs/snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/docs/snapshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-sequencer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject", 10 | "storybook": "start-storybook -p 9009 -s public", 11 | "build-storybook": "build-storybook -s public" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome-svg-core": "^1.2.16", 15 | "@fortawesome/free-solid-svg-icons": "^5.8.0", 16 | "@fortawesome/react-fontawesome": "^0.1.4", 17 | "@types/jest": "^24.0.15", 18 | "@types/node": "^12.0.10", 19 | "@types/react": "^16.8.22", 20 | "@types/react-dom": "^16.8.4", 21 | "@types/react-fontawesome": "^1.6.4", 22 | "@types/react-modal": "^3.8.2", 23 | "@types/react-redux": "^7.1.1", 24 | "@types/react-router": "^5.0.3", 25 | "@types/react-router-dom": "^4.3.4", 26 | "@types/redux": "^3.6.0", 27 | "@types/styled-components": "^4.1.16", 28 | "@types/uuid": "^3.4.5", 29 | "@types/webaudioapi": "^0.0.27", 30 | "apollo-boost": "^0.3.1", 31 | "apollo-client": "^2.5.1", 32 | "classnames": "^2.2.6", 33 | "graphql": "^14.1.1", 34 | "lodash": "^4.17.11", 35 | "normalize.css": "^8.0.1", 36 | "react": "^16.8.4", 37 | "react-apollo": "^2.5.2", 38 | "react-dom": "^16.8.4", 39 | "react-modal": "^3.8.1", 40 | "react-redux": "^5.1.1", 41 | "react-router": "^5.0.0", 42 | "react-router-dom": "^5.0.0", 43 | "react-scripts": "2.1.8", 44 | "react-transition-group": "^2.6.1", 45 | "redux": "^4.0.1", 46 | "styled-components": "^4.2.0", 47 | "typescript": "^3.5.2", 48 | "typescript-plugin-css-modules": "^1.2.0", 49 | "uuid": "^3.3.2" 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "^7.3.4", 53 | "@storybook/addon-actions": "^5.0.3", 54 | "@storybook/addon-info": "^5.1.9", 55 | "@storybook/addon-knobs": "^5.1.9", 56 | "@storybook/addon-links": "^5.0.3", 57 | "@storybook/addon-options": "^5.1.9", 58 | "@storybook/addons": "^5.0.3", 59 | "@storybook/react": "^5.0.3", 60 | "@types/storybook-addon-jsx": "^5.4.3", 61 | "@types/storybook__addon-info": "^4.1.2", 62 | "@types/storybook__addon-knobs": "^5.0.2", 63 | "babel-loader": "8.0.5", 64 | "prettier": "^1.16.4", 65 | "redux-devtools-extension": "^2.13.8", 66 | "storybook-addon-jsx": "^7.1.2", 67 | "tslint": "^5.18.0", 68 | "tslint-config-prettier": "^1.18.0" 69 | }, 70 | "eslintConfig": { 71 | "extends": "react-app" 72 | }, 73 | "browserslist": [ 74 | ">0.2%", 75 | "not dead", 76 | "not ie <= 11", 77 | "not op_mini all" 78 | ], 79 | "prettier": { 80 | "semi": false, 81 | "tabWidth": 2 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | JAMS App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Sequencer", 3 | "name": "React Redux audio sequencer", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/sounds/13bc898b-16ca-4b2c-b90c-06a7894a78fd.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/13bc898b-16ca-4b2c-b90c-06a7894a78fd.wav -------------------------------------------------------------------------------- /public/sounds/19b606f5-52b5-49f5-a3b6-566c245e0407.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/19b606f5-52b5-49f5-a3b6-566c245e0407.wav -------------------------------------------------------------------------------- /public/sounds/1e95d1b8-440a-41f3-ab0a-3a48587bf6f7.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/1e95d1b8-440a-41f3-ab0a-3a48587bf6f7.wav -------------------------------------------------------------------------------- /public/sounds/241d3fec-5a90-4ec1-b57a-635009ffd167.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/241d3fec-5a90-4ec1-b57a-635009ffd167.wav -------------------------------------------------------------------------------- /public/sounds/39a5cf0d-8def-43fa-ab3d-1638b9213cb3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/39a5cf0d-8def-43fa-ab3d-1638b9213cb3.wav -------------------------------------------------------------------------------- /public/sounds/3c911ed3-8862-473b-8b48-2aeb00eeecb5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/3c911ed3-8862-473b-8b48-2aeb00eeecb5.wav -------------------------------------------------------------------------------- /public/sounds/53a20b19-712e-4a43-b718-98b7ff897880.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/53a20b19-712e-4a43-b718-98b7ff897880.wav -------------------------------------------------------------------------------- /public/sounds/5d394665-a0bb-4d49-8a61-5ffe9912df6c.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/5d394665-a0bb-4d49-8a61-5ffe9912df6c.wav -------------------------------------------------------------------------------- /public/sounds/6eeff2d5-6c90-43c2-91f6-3a68f0911483.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/6eeff2d5-6c90-43c2-91f6-3a68f0911483.wav -------------------------------------------------------------------------------- /public/sounds/7129a80a-5698-40ef-a5c1-2fb1d4b62c42.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/7129a80a-5698-40ef-a5c1-2fb1d4b62c42.wav -------------------------------------------------------------------------------- /public/sounds/74c74b98-7e16-4c81-aff8-b21efe16ddb1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/74c74b98-7e16-4c81-aff8-b21efe16ddb1.wav -------------------------------------------------------------------------------- /public/sounds/7f9a144d-64b5-43e0-a3ca-3878085ce582.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/7f9a144d-64b5-43e0-a3ca-3878085ce582.wav -------------------------------------------------------------------------------- /public/sounds/7ff6ffa7-9768-4bfc-b6c8-b99a70be556b.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/7ff6ffa7-9768-4bfc-b6c8-b99a70be556b.wav -------------------------------------------------------------------------------- /public/sounds/8710fea6-56d0-440e-920f-0da2576bf3d7.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/8710fea6-56d0-440e-920f-0da2576bf3d7.wav -------------------------------------------------------------------------------- /public/sounds/899bc068-e687-4928-ba4c-9082c0163304.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/899bc068-e687-4928-ba4c-9082c0163304.wav -------------------------------------------------------------------------------- /public/sounds/8cf86f2f-0b50-42bb-81d8-22731d462161.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/8cf86f2f-0b50-42bb-81d8-22731d462161.wav -------------------------------------------------------------------------------- /public/sounds/901cfa41-c230-4c26-903b-22f99ee13deb.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/901cfa41-c230-4c26-903b-22f99ee13deb.wav -------------------------------------------------------------------------------- /public/sounds/939d89c3-4abd-4312-84b8-d388cd84fcc6.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/939d89c3-4abd-4312-84b8-d388cd84fcc6.wav -------------------------------------------------------------------------------- /public/sounds/97eb4ae4-afe0-408f-88c6-736233409ec9.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/97eb4ae4-afe0-408f-88c6-736233409ec9.wav -------------------------------------------------------------------------------- /public/sounds/9dce9279-194e-4d6f-9f07-d7968eb13f63.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/9dce9279-194e-4d6f-9f07-d7968eb13f63.wav -------------------------------------------------------------------------------- /public/sounds/acc4ea8c-cd40-44f2-b553-0642f411a144.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/acc4ea8c-cd40-44f2-b553-0642f411a144.wav -------------------------------------------------------------------------------- /public/sounds/bfac1667-5115-49cd-82dc-f294f54cb447.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/bfac1667-5115-49cd-82dc-f294f54cb447.wav -------------------------------------------------------------------------------- /public/sounds/e3a06bcb-ff48-492b-b55c-b9a8a8479aac.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/e3a06bcb-ff48-492b-b55c-b9a8a8479aac.wav -------------------------------------------------------------------------------- /public/sounds/eaacf8f7-0d57-4e8c-a872-bb51315659b3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/eaacf8f7-0d57-4e8c-a872-bb51315659b3.wav -------------------------------------------------------------------------------- /public/sounds/f0cb1d42-7052-432f-95df-4320e5d42cb0.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/f0cb1d42-7052-432f-95df-4320e5d42cb0.wav -------------------------------------------------------------------------------- /public/sounds/f57d9727-d7f0-4027-b9cd-fb7b56a79df4.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/f57d9727-d7f0-4027-b9cd-fb7b56a79df4.wav -------------------------------------------------------------------------------- /public/sounds/fc897b72-744c-434b-9018-6e860da11edb.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glimberger/react-redux-sequencer/76af1308ee1f4f534f527bb5fb55a275048fff07/public/sounds/fc897b72-744c-434b-9018-6e860da11edb.wav -------------------------------------------------------------------------------- /src/audio/context/createContext.ts: -------------------------------------------------------------------------------- 1 | export type LatencyHint = "balanced" | "interactive" | "playback" 2 | export interface IAudioContextOptions { 3 | latencyHint: LatencyHint 4 | sampleRate: number 5 | } 6 | 7 | const defaultOptions: IAudioContextOptions = { 8 | latencyHint: "playback", 9 | sampleRate: 44100 10 | } 11 | 12 | const createContext = (options: IAudioContextOptions = defaultOptions) => 13 | new AudioContext(options) 14 | 15 | export default createContext 16 | -------------------------------------------------------------------------------- /src/audio/processor/processor.ts: -------------------------------------------------------------------------------- 1 | import createContext, { IAudioContextOptions } from "../context/createContext" 2 | import { IN_DEV } from "../../utils/env" 3 | 4 | import { ISamples } from "../../redux/store/sample/interfaces" 5 | import { ICell, ITrack, ITracks } from "../../redux/store/session/interfaces" 6 | import { 7 | IInstrument, 8 | IInstruments 9 | } from "../../redux/store/instrument/interfaces" 10 | 11 | type SampleAudioBufferMap = Map 12 | type AudioNodeMap = Map 13 | 14 | type TrackAudioNodeMap = Map 15 | 16 | // tslint:disable:no-console no-unused-expression 17 | /** 18 | * Performs any audio processing 19 | */ 20 | class AudioProcessor { 21 | ctx: AudioContext 22 | 23 | masterGainNode: GainNode 24 | 25 | sampleAudioBufferMap: SampleAudioBufferMap 26 | trackAudioNodeMap: TrackAudioNodeMap 27 | 28 | constructor(options?: IAudioContextOptions) { 29 | this.ctx = createContext(options) 30 | IN_DEV && 31 | console.debug( 32 | "[AudioProcessor] audio context created — state = %s", 33 | this.ctx.state 34 | ) 35 | 36 | this.masterGainNode = this.ctx.createGain() 37 | IN_DEV && console.debug("[AudioProcessor] master gain node created") 38 | this.masterGainNode.connect(this.ctx.destination) 39 | IN_DEV && 40 | console.debug( 41 | "[AudioProcessor] master gain node connected to destination node" 42 | ) 43 | 44 | this.sampleAudioBufferMap = new Map() 45 | this.trackAudioNodeMap = new Map() 46 | } 47 | 48 | setMasterGainNodeValue = (gain: number) => { 49 | const time = this.ctx.currentTime 50 | this.masterGainNode.gain.setValueAtTime(gain, time) 51 | console.debug("[AudioProcessor] master gain set to %d at %d s", gain, time) 52 | } 53 | 54 | setTrackGainNodeValue = (trackID: string, gain: number) => { 55 | const time = this.ctx.currentTime 56 | const audioNodes = this.trackAudioNodeMap.get(trackID) 57 | 58 | console.groupCollapsed( 59 | "[AudioProcessor] setTrackGainNodeValue", 60 | trackID, 61 | gain 62 | ) 63 | 64 | console.assert( 65 | audioNodes instanceof Map, 66 | "audioNodes for track %s should be defined", 67 | trackID 68 | ) 69 | 70 | if (!audioNodes) { 71 | return 72 | } 73 | 74 | const gainNode = audioNodes.get("gain") 75 | 76 | console.assert( 77 | gainNode instanceof GainNode, 78 | "gainNode for track %s should be defined", 79 | trackID 80 | ) 81 | 82 | if (gainNode instanceof GainNode) { 83 | gainNode.gain.setValueAtTime(gain, time) 84 | console.debug("[AudioProcessor] track gain set to %d at %d s", gain, time) 85 | } 86 | 87 | console.groupEnd() 88 | } 89 | 90 | storeSampleAudioBuffers = async ( 91 | samples: ISamples, 92 | fetchBuffer: (url: string) => Promise 93 | ): Promise => { 94 | const sampleIDs = Object.keys(samples).map(sampleID => sampleID) 95 | 96 | const audioBuffers = await Promise.all( 97 | sampleIDs.map(sampleID => { 98 | const { url } = samples[sampleID] 99 | 100 | return fetchBuffer(url) 101 | .then(arrayBuffer => this.ctx.decodeAudioData(arrayBuffer)) 102 | .catch(error => console.error(error)) 103 | }) 104 | ) 105 | 106 | audioBuffers.forEach((audioBuffer, idx) => { 107 | const sampleID = sampleIDs[idx] 108 | IN_DEV && 109 | console.debug( 110 | "[AudioProcessor] audioBuffer for sample %s: %o", 111 | sampleID, 112 | audioBuffer 113 | ) 114 | console.assert( 115 | audioBuffer instanceof AudioBuffer, 116 | "audioBuffer for sample %s should be defined", 117 | sampleID 118 | ) 119 | 120 | if (audioBuffer) { 121 | this.sampleAudioBufferMap.set(sampleID, audioBuffer) 122 | } 123 | }) 124 | } 125 | 126 | setTrackGainNode = (trackID: string, gain: number) => { 127 | IN_DEV && 128 | console.groupCollapsed("[AudioProcessor] setTrackGainNode %s", trackID) 129 | 130 | const gainNode = this.ctx.createGain() 131 | IN_DEV && console.debug("[AudioProcessor] gain node created") 132 | 133 | gainNode.connect(this.masterGainNode) 134 | IN_DEV && console.debug("[AudioProcessor] gain node --> master gain node") 135 | 136 | const time = this.ctx.currentTime 137 | gainNode.gain.setValueAtTime(gain, time) 138 | IN_DEV && 139 | console.debug("[AudioProcessor] gain node set to %d at %d", gain, time) 140 | 141 | const audioNodes = 142 | this.trackAudioNodeMap.get(trackID) || new Map() 143 | audioNodes.set("gain", gainNode) 144 | 145 | this.trackAudioNodeMap.set(trackID, audioNodes) 146 | IN_DEV && console.debug("[AudioProcessor] gain node stored") 147 | 148 | IN_DEV && console.groupEnd() 149 | } 150 | 151 | playSample( 152 | beat: number, 153 | gain: number, 154 | note: number | null, 155 | trackID: string, 156 | tracks: { [p: string]: ITrack }, 157 | instruments: { [p: string]: IInstrument }, 158 | matrix: { [p: string]: ICell[] } 159 | ) { 160 | const { instrumentID } = tracks[trackID] 161 | const { mapping } = instruments[instrumentID] 162 | const midi = note === null ? matrix[trackID][beat].midi : note 163 | 164 | if (!mapping["M" + midi]) { 165 | IN_DEV && console.debug("No mapping found for note %d - aborting", midi) 166 | return 167 | } 168 | const { sampleID, detune } = mapping["M" + midi] 169 | 170 | const audioBuffer = this.sampleAudioBufferMap.get(sampleID) 171 | 172 | IN_DEV && console.groupCollapsed("[AudioProcessor] playSample %s", sampleID) 173 | 174 | console.assert( 175 | audioBuffer instanceof AudioBuffer, 176 | "[AudioProcessor] audioBuffer for sample %s should be defined", 177 | sampleID 178 | ) 179 | 180 | if (!audioBuffer) { 181 | return 182 | } 183 | 184 | const gainNode = this.ctx.createGain() 185 | IN_DEV && console.debug("[AudioProcessor] gain node created") 186 | 187 | gainNode.gain.value = gain 188 | IN_DEV && console.debug("[AudioProcessor] gain node set to %d", gain) 189 | 190 | const source = this.ctx.createBufferSource() 191 | IN_DEV && console.debug("[AudioProcessor] source node created") 192 | 193 | source.buffer = audioBuffer 194 | IN_DEV && 195 | console.debug("[AudioProcessor] audioBuffer set to %o", audioBuffer) 196 | 197 | source.detune.value = detune 198 | IN_DEV && console.debug("[AudioProcessor] detune value set to %d", detune) 199 | 200 | source.connect(gainNode) 201 | IN_DEV && console.debug("[AudioProcessor] source node --> gain node") 202 | 203 | gainNode.connect(this.masterGainNode) 204 | IN_DEV && console.debug("[AudioProcessor] gain node --> master gain node") 205 | 206 | source.start(this.ctx.currentTime) 207 | console.debug("[AudioProcessor] source node started") 208 | 209 | IN_DEV && console.groupEnd() 210 | } 211 | 212 | scheduleNoteForTrack = ( 213 | beatNumber: number, 214 | time: number, 215 | trackID: string, 216 | tracks: ITracks, 217 | instruments: IInstruments, 218 | matrix: { [trackID: string]: ICell[] }, 219 | isSoloActive: boolean, 220 | solos: { [trzckID: string]: boolean }, 221 | mutes: { [trackID: string]: boolean }, 222 | tempo: number 223 | ) => { 224 | const { instrumentID, noteResolution } = tracks[trackID] 225 | const { mapping } = instruments[instrumentID] 226 | const { scheduled, midi, processing } = matrix[trackID][beatNumber] 227 | 228 | // check note resolution 229 | if (beatNumber % noteResolution) { 230 | return 231 | } 232 | 233 | // check solo 234 | if (isSoloActive && !solos[trackID]) { 235 | return 236 | } 237 | 238 | // check mute 239 | if (mutes[trackID]) { 240 | return 241 | } 242 | 243 | // check if scheduled 244 | if (!scheduled) { 245 | return 246 | } 247 | 248 | const { detune, sampleID } = mapping["M" + midi] 249 | const audioBuffer = this.sampleAudioBufferMap.get(sampleID) 250 | 251 | console.assert( 252 | audioBuffer instanceof AudioBuffer, 253 | "'audioBuffer' should be an instance of AudioBuffer." 254 | ) 255 | 256 | if (!audioBuffer) { 257 | return 258 | } 259 | 260 | const audioNodes = this.trackAudioNodeMap.get(trackID) 261 | 262 | console.assert( 263 | audioNodes instanceof Map, 264 | "ITrack audio node map entry for track %s should be defined", 265 | trackID 266 | ) 267 | 268 | if (!audioNodes) { 269 | return 270 | } 271 | 272 | const trackGainNode = audioNodes.get("gain") 273 | 274 | console.assert( 275 | trackGainNode instanceof GainNode, 276 | "Gain node for track %s should be defined", 277 | trackID 278 | ) 279 | if (!trackGainNode) { 280 | return 281 | } 282 | 283 | const cellGainNode: GainNode = this.ctx.createGain() 284 | cellGainNode.gain.setValueAtTime(processing.gain.gain, time) 285 | 286 | // check for note off - fade 287 | const secondsPerBeat = 60.0 / tempo 288 | const nextCellAtResolution = 289 | matrix[trackID][(beatNumber + noteResolution) % 64] 290 | if (nextCellAtResolution.scheduled && midi === nextCellAtResolution.midi) { 291 | cellGainNode.gain.setTargetAtTime( 292 | 0, 293 | time + 0.25 * secondsPerBeat * (noteResolution - 0.2), 294 | 0.05 295 | ) 296 | } 297 | 298 | const source: AudioBufferSourceNode = this.ctx.createBufferSource() 299 | source.detune.value = detune 300 | source.buffer = audioBuffer 301 | source.connect(cellGainNode) 302 | cellGainNode.connect(trackGainNode) 303 | 304 | source.start(time) 305 | } 306 | 307 | close = () => this.ctx.close() 308 | } 309 | 310 | export default AudioProcessor 311 | -------------------------------------------------------------------------------- /src/audio/utils/Volume/Volume.test.ts: -------------------------------------------------------------------------------- 1 | import Volume from "./Volume" 2 | 3 | describe("Volume", () => { 4 | describe("toGain", () => { 5 | it("should return a gain of 1 when input is 0", () => { 6 | expect(Volume.toGain(0.0)).toBeCloseTo(1.0, 2) 7 | }) 8 | 9 | it("should return a gain of 0.5 when input is -6", () => { 10 | expect(Volume.toGain(-6.0)).toBeCloseTo(0.5, 2) 11 | }) 12 | 13 | it("should return a gain of 0.25 when input is -12", () => { 14 | expect(Volume.toGain(-12.0)).toBeCloseTo(0.25, 2) 15 | }) 16 | 17 | it("should return a gain of 0.125 when input is -18", () => { 18 | expect(Volume.toGain(-18.0)).toBeCloseTo(0.125, 2) 19 | }) 20 | 21 | it("should return a gain of 0.0 when input is -70+", () => { 22 | expect(Volume.toGain(-70.0)).toEqual(0) 23 | expect(Volume.toGain(-100.0)).toEqual(0) 24 | }) 25 | }) 26 | 27 | describe("toDB", () => { 28 | it("should return ~0 when the input gain is 1", () => { 29 | expect(Volume.toDB(1.0)).toBeCloseTo(0, 1) 30 | }) 31 | 32 | it("should return ~-6 when th e input gain is 0.5", () => { 33 | expect(Volume.toDB(0.5)).toBeCloseTo(-6, 1) 34 | }) 35 | 36 | it("should return ~-12 when th e input gain is 0.25", () => { 37 | expect(Volume.toDB(0.25)).toBeCloseTo(-12, 1) 38 | }) 39 | 40 | it("should return ~-18 when th e input gain is 0.125", () => { 41 | expect(Volume.toDB(0.125)).toBeCloseTo(-18, 0) 42 | }) 43 | 44 | it("should return -100 when the input gain is 0", () => { 45 | expect(Volume.toDB(0)).toBe(-100) 46 | }) 47 | }) 48 | 49 | describe("toDBString", () => { 50 | it("should return 0dB when the input gain is 1", () => { 51 | expect(Volume.toDB(1.0)).toBeCloseTo(0, 1) 52 | }) 53 | 54 | it("should return ~-6dB when th e input gain is 0.5", () => { 55 | expect(Volume.toDB(0.5)).toBeCloseTo(-6, 1) 56 | }) 57 | 58 | it("should return -12dB when th e input gain is 0.25", () => { 59 | expect(Volume.toDB(0.25)).toBeCloseTo(-12, 1) 60 | }) 61 | 62 | it("should return -18dB when th e input gain is 0.125", () => { 63 | expect(Volume.toDB(0.125)).toBeCloseTo(-18, 0) 64 | }) 65 | 66 | it("should return -∞ when the input gain is 0", () => { 67 | expect(Volume.toDB(0)).toBe(-100) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /src/audio/utils/Volume/Volume.ts: -------------------------------------------------------------------------------- 1 | class Volume { 2 | static toGain(db: number) { 3 | if (db <= -70.0) { 4 | return 0 5 | } 6 | 7 | return Math.pow(10, db / 20.0) 8 | } 9 | 10 | static toDB(gain: number) { 11 | if (gain === 0) { 12 | return -100.0 13 | } 14 | 15 | return 20 * Math.log10(gain) 16 | } 17 | 18 | static toDBString(gain: number) { 19 | const db = Volume.toDB(gain) 20 | 21 | if (db <= -70) { 22 | return "-∞ dB" 23 | } 24 | 25 | return db.toFixed(1) + " dB" 26 | } 27 | } 28 | 29 | export default Volume 30 | -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .App-header { 2 | background-color: #282c34; 3 | min-height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | font-size: calc(10px + 2vmin); 9 | color: white; 10 | } 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/App/App.module.css: -------------------------------------------------------------------------------- 1 | @value blueGrey_800: #37474f; 2 | @value blueGrey_800Dark: #102027; 3 | @value grey_900: #212121; 4 | 5 | .Base { 6 | 7 | min-height: 100%; 8 | color: white; 9 | } 10 | 11 | .Sequencer { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import { Route, Switch } from "react-router-dom" 4 | // tslint:disable-next-line:no-submodule-imports 5 | import { library } from "@fortawesome/fontawesome-svg-core" 6 | // tslint:disable-next-line:no-submodule-imports 7 | import { faPlay, faPause } from "@fortawesome/free-solid-svg-icons" 8 | 9 | import Menu from "../Menu/Menu" 10 | import SessionPage from "../pages/SessionPage/SessionPage" 11 | import HomePage from "../pages/HomePage/HomePage" 12 | 13 | library.add(faPlay, faPause) 14 | 15 | class App extends React.Component<{}> { 16 | render() { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | } 28 | 29 | export default App 30 | -------------------------------------------------------------------------------- /src/components/MasterPanel/MasterGainController/MasterGainController.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | 5 | import { MasterGainController } from "./MasterGainController" 6 | import { withContainer } from "../../../../.storybook/decorators" 7 | import { number, withKnobs } from "@storybook/addon-knobs" 8 | 9 | storiesOf("MasterGainController", module) 10 | .addParameters({ 11 | info: { 12 | inline: true, 13 | header: false 14 | } 15 | }) 16 | .addDecorator(withKnobs) 17 | .addDecorator(withContainer) 18 | .add("default — use knobs", () => { 19 | const gain = number("Gain - min : 0", 1) 20 | 21 | return ( 22 | 27 | ) 28 | }) 29 | .add("gain 0dB", () => ( 30 | 35 | )) 36 | .add("gain -6dB", () => ( 37 | 42 | )) 43 | -------------------------------------------------------------------------------- /src/components/MasterPanel/MasterGainController/MasterGainController.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | import { connect } from "react-redux" 5 | 6 | import Controller from "../../controllers/Fader/Fader" 7 | import Volume from "../../../audio/utils/Volume/Volume" 8 | import Color, { MaterialColor } from "../../../utils/color/colorLibrary" 9 | import { changeMasterGain } from "../../../redux/actions/session/creators" 10 | import { IAppState } from "../../../redux/store/configureStore" 11 | 12 | interface IOwnProps { 13 | color: MaterialColor 14 | } 15 | 16 | interface IProps extends IOwnProps { 17 | gain: number 18 | changeMasterGain: (gain: number) => void 19 | } 20 | 21 | const StyledContainer = styled.div` 22 | display: flex; 23 | flex-flow: row nowrap; 24 | align-items: center; 25 | ` 26 | 27 | const StyledIndicator = styled.span<{ color: MaterialColor }>` 28 | margin-left: 0.5rem; 29 | width: 4rem; 30 | text-align: end; 31 | user-select: none; 32 | color: ${({ color }) => Color.get50(color)}; 33 | ` 34 | 35 | export function MasterGainController(props: IProps) { 36 | const handleGainChange = (e: React.ChangeEvent) => { 37 | props.changeMasterGain(parseFloat(e.target.value)) 38 | } 39 | 40 | return ( 41 | 42 | 52 | 53 | {Volume.toDBString(props.gain)} 54 | 55 | 56 | ) 57 | } 58 | 59 | const mapStateToProps = (state: IAppState) => ({ 60 | gain: state.session.masterGain 61 | }) 62 | 63 | const MasterGainControllerConnected = connect( 64 | mapStateToProps, 65 | { changeMasterGain } 66 | )(MasterGainController) 67 | 68 | export default MasterGainControllerConnected 69 | -------------------------------------------------------------------------------- /src/components/MasterPanel/MasterPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import TempoController from "./TempoController/TempoController" 6 | import Color, { MaterialColor } from "../../utils/color/colorLibrary" 7 | import Transport from "./Transport/Transport" 8 | import MasterGainController from "./MasterGainController/MasterGainController" 9 | 10 | const StyledContainer = styled.div<{ color: MaterialColor }>` 11 | height: 4rem; 12 | display: flex; 13 | background-color: ${({ color }) => Color.get600(color)}; 14 | ` 15 | 16 | const StyledItem = styled.div` 17 | margin: auto 1rem; 18 | ` 19 | 20 | function MasterPanel() { 21 | return ( 22 |
23 | 24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | ) 36 | } 37 | 38 | export default MasterPanel 39 | -------------------------------------------------------------------------------- /src/components/MasterPanel/ModeSwitch/ModeSwitch.module.css: -------------------------------------------------------------------------------- 1 | .Container { 2 | margin: auto; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/MasterPanel/ModeSwitch/ModeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import Color, { MaterialColor } from "../../../utils/color/colorLibrary" 4 | 5 | interface IProps { 6 | mode: string 7 | color: MaterialColor 8 | setPlayMode: () => void 9 | setEditMode: () => void 10 | } 11 | 12 | class ModeSwitch extends React.Component { 13 | handleKeyUp = (e: KeyboardEvent) => { 14 | const code = e.which 15 | 16 | // 'e' 17 | if (code === 69) { 18 | this.props.setEditMode() 19 | } 20 | 21 | // 'p' 22 | if (code === 80) { 23 | this.props.setPlayMode() 24 | } 25 | } 26 | 27 | componentDidMount(): void { 28 | window.addEventListener("keyup", this.handleKeyUp) 29 | } 30 | 31 | componentWillUnmount(): void { 32 | window.removeEventListener("keyup", this.handleKeyUp) 33 | } 34 | 35 | render() { 36 | const { mode, color, setPlayMode, setEditMode } = this.props 37 | const containerStyles: React.CSSProperties = { 38 | height: "2.5rem", 39 | width: "6rem", 40 | display: "flex", 41 | color: Color.get100(color), 42 | border: `solid 3px ${Color.get100(color)}`, 43 | borderRadius: "3px", 44 | cursor: "pointer" 45 | } 46 | 47 | const textStyles: React.CSSProperties = { 48 | margin: "auto", 49 | fontWeight: 700, 50 | userSelect: "none" 51 | } 52 | 53 | return ( 54 |
{ 57 | if (mode === "PLAY") { 58 | setEditMode() 59 | } 60 | if (mode === "EDIT") { 61 | setPlayMode() 62 | } 63 | }} 64 | > 65 | {mode} 66 |
67 | ) 68 | } 69 | } 70 | 71 | export default ModeSwitch 72 | -------------------------------------------------------------------------------- /src/components/MasterPanel/TempoController/TempoController.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | import { TempoController } from "./TempoController" 5 | import { withContainer } from "../../../../.storybook/decorators" 6 | import { 7 | withKnobs, 8 | number, 9 | } from "@storybook/addon-knobs" 10 | 11 | storiesOf("TempoController", module) 12 | .addParameters({ 13 | info: { 14 | inline: true, 15 | header: false 16 | } 17 | }) 18 | .addDecorator(withKnobs) 19 | .addDecorator(story => withContainer(story)) 20 | .add("default — use knobs", () => { 21 | const tempo = number('Tempo', 120) 22 | return ( 23 | 28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/MasterPanel/TempoController/TempoController.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { connect } from "react-redux" 3 | // tslint:disable-next-line:no-submodule-imports 4 | import styled from "styled-components/macro" 5 | 6 | import ValueController from "../../controllers/ValueController/ValueController" 7 | import { changeTempo } from "../../../redux/actions/session/creators" 8 | 9 | import { MaterialColor } from "../../../utils/color/colorLibrary" 10 | import { Dispatch } from "redux" 11 | import { Action } from "../../../redux/actions/session/interfaces" 12 | import { IAppState } from "../../../redux/store/configureStore" 13 | 14 | interface IOwnProps { 15 | color: MaterialColor 16 | } 17 | 18 | interface IProps extends IOwnProps { 19 | tempo: number 20 | onChange: (value: number) => void 21 | } 22 | 23 | const StyledContainer = styled.div` 24 | display: flex; 25 | flex-flow: row nowrap; 26 | justify-content: space-between; 27 | align-items: center; 28 | ` 29 | 30 | export function TempoController({ color, tempo, onChange }: IProps) { 31 | return ( 32 | 33 | 41 | 42 | ) 43 | } 44 | 45 | const mapStateToProps = (state: IAppState) => ({ tempo: state.session.tempo }) 46 | 47 | const mapDispatchToProps = (dispatch: Dispatch) => { 48 | return { 49 | onChange(value: number) { 50 | dispatch(changeTempo(value)) 51 | } 52 | } 53 | } 54 | 55 | const TempoControllerConnected = connect( 56 | mapStateToProps, 57 | mapDispatchToProps 58 | )(TempoController) 59 | 60 | export default TempoControllerConnected 61 | -------------------------------------------------------------------------------- /src/components/MasterPanel/Transport/Transport.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | import { library } from "@fortawesome/fontawesome-svg-core" 5 | import { faPlay, faPause } from "@fortawesome/free-solid-svg-icons" 6 | 7 | import { Transport } from "./Transport" 8 | import { withContainer } from "../../../../.storybook/decorators" 9 | import { withKnobs, boolean } from "@storybook/addon-knobs" 10 | 11 | library.add(faPlay, faPause) 12 | 13 | storiesOf("Transport", module) 14 | .addParameters({ 15 | info: { 16 | inline: true, 17 | header: false 18 | } 19 | }) 20 | .addDecorator(withKnobs) 21 | .addDecorator(withContainer) 22 | .add("default - use knobs", () => { 23 | const playing = boolean("Played", true) 24 | return ( 25 | 30 | ) 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/MasterPanel/Transport/Transport.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" 4 | // tslint:disable-next-line:no-submodule-imports 5 | import styled from "styled-components/macro" 6 | import { connect } from "react-redux" 7 | 8 | import { togglePlay } from "../../../redux/actions/audio/creators" 9 | import Color, { MaterialColor } from "../../../utils/color/colorLibrary" 10 | import { IAppState } from "../../../redux/store/configureStore" 11 | 12 | interface IOwnProps { 13 | color: MaterialColor 14 | } 15 | 16 | interface IProps extends IOwnProps { 17 | playing: boolean, 18 | togglePlay: () => void 19 | } 20 | 21 | const StyledButton = styled.button<{color: MaterialColor}>` 22 | height: 100%; 23 | width: 4rem; 24 | font-size: 1.5rem; 25 | cursor: pointer; 26 | border: none; 27 | color: ${({ color }) => Color.get50(color)}; 28 | background-color: ${({ color }) => Color.get800(color)}; 29 | 30 | &:hover { 31 | background-color: ${({ color }) => Color.get700(color)}; 32 | } 33 | ` 34 | 35 | export function Transport(props: IProps) { 36 | const playButtonRef = React.useRef(null) 37 | const pauseButtonRef = React.useRef(null) 38 | 39 | const handlePlayClick = () => { 40 | // tslint:disable-next-line:no-unused-expression 41 | playButtonRef.current && playButtonRef.current.blur() 42 | props.togglePlay() 43 | } 44 | 45 | const handlePauseClick = () => { 46 | // tslint:disable-next-line:no-unused-expression 47 | pauseButtonRef.current && pauseButtonRef.current.blur() 48 | props.togglePlay() 49 | } 50 | 51 | const handleKeyUp = (e: KeyboardEvent) => { 52 | const code = e.which 53 | 54 | // place 55 | if (code === 32) { 56 | props.togglePlay() 57 | } 58 | } 59 | 60 | React.useEffect(() => { 61 | window.addEventListener("keyup", handleKeyUp) 62 | 63 | return () => window.removeEventListener("keyup", handleKeyUp) 64 | }, []) 65 | 66 | return props.playing ? ( 67 | 72 | 73 | 74 | ) : ( 75 | 80 | 81 | 82 | ) 83 | } 84 | 85 | const mapStateToProps = (state: IAppState) => ({ 86 | playing: state.audio.playing 87 | }) 88 | 89 | const ConnectedTransport = connect( 90 | mapStateToProps, 91 | { togglePlay } 92 | )(Transport) 93 | 94 | export default ConnectedTransport 95 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.module.css: -------------------------------------------------------------------------------- 1 | @value deepOrange_A700: #dd2c00; 2 | @value blueGrey_900: #263238; 3 | @value blueGrey_300: #90a4ae; 4 | @value grey_800: #424242; 5 | @value grey_100: #f5f5f5; 6 | 7 | .Base { 8 | position: absolute; 9 | display: flex; 10 | width: 100%; 11 | flex-flow: row nowrap; 12 | /*justify-content: space-between;*/ 13 | align-items: center; 14 | height: 3rem; 15 | background: grey_800; 16 | box-shadow: 0px 0px 16px 0px rgba(0,0,0,1); 17 | } 18 | 19 | .Brand { 20 | padding: 1rem; 21 | font-weight: 700; 22 | user-select: none; 23 | } 24 | 25 | .Link { 26 | text-decoration: none; 27 | opacity: 0.8; 28 | color: grey_100; 29 | } 30 | 31 | .Link:hover { 32 | opacity: 1; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { MemoryRouter } from "react-router-dom" 4 | 5 | import Menu from "./Menu" 6 | 7 | storiesOf("Menu", module) 8 | .addParameters({ 9 | info: { 10 | inline: true, 11 | header: false 12 | } 13 | }) 14 | .addDecorator(story => ( 15 |
{story()}
16 | )) 17 | .add("default", () => ( 18 | 19 | 20 | 21 | )) 22 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Link, NavLink } from "react-router-dom" 3 | 4 | import styles from "./Menu.module.css" 5 | 6 | function Menu() { 7 | return ( 8 |
9 |
10 | 11 | JAMS 12 | 13 |
14 |
15 | 16 | Session 17 | 18 |
19 |
20 | ) 21 | } 22 | 23 | export default Menu 24 | -------------------------------------------------------------------------------- /src/components/Root/Root.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Provider } from "react-redux" 3 | import { BrowserRouter } from "react-router-dom" 4 | 5 | import configureStore from "../../redux/store/configureStore" 6 | import App from "../App/App" 7 | import audio from "../../redux/store/audio/initialState" 8 | import session from "../../redux/store/session/initialState" 9 | import { instruments } from "../../redux/store/instrument/initialState" 10 | import { samples } from "../../redux/store/sample/initialState" 11 | 12 | const preloadState = { 13 | audio, 14 | session, 15 | instruments, 16 | samples 17 | } 18 | const store = configureStore(preloadState) 19 | 20 | function Root() { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default Root 31 | -------------------------------------------------------------------------------- /src/components/Sequencer/AddTrack/AddTrack.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { connect } from "react-redux" 3 | import { v4 as uuid } from "uuid" 4 | 5 | import AddTrackButton from "./AddTrackButton" 6 | import AddTrackModal from "./AddTrackModal" 7 | import { addTrack } from "../../../redux/actions/session/creators" 8 | import { getInstrumentListIndexedByGroup } from "../../../redux/reducers/instruments" 9 | 10 | import { MaterialColor } from "../../../utils/color/colorLibrary" 11 | import { 12 | IInstrument, 13 | IInstruments 14 | } from "../../../redux/store/instrument/interfaces" 15 | import { ISamples } from "../../../redux/store/sample/interfaces" 16 | import { IAppState } from "../../../redux/store/configureStore" 17 | 18 | interface IOwnProps { 19 | color: MaterialColor 20 | } 21 | 22 | interface IProps extends IOwnProps { 23 | instruments: IInstruments 24 | samples: ISamples 25 | addTrack: ( 26 | trackID: string, 27 | instrument: IInstrument, 28 | samples: ISamples 29 | ) => void 30 | } 31 | 32 | export function AddTrack(props: IProps) { 33 | const [modalOpen, setModalOpen] = React.useState(false) 34 | 35 | const afterOpenModal = () => { 36 | // references are now sync'd and can be accessed. 37 | } 38 | 39 | const handleNewTrack = (instrument: IInstrument) => { 40 | setModalOpen(false) 41 | 42 | const instrumentSamples: ISamples = {} 43 | instrument.sampleIDs.forEach( 44 | sampleID => (instrumentSamples[sampleID] = props.samples[sampleID]) 45 | ) 46 | 47 | props.addTrack(uuid(), instrument, instrumentSamples) 48 | } 49 | 50 | return ( 51 |
52 | setModalOpen(true)} /> 53 | 54 | setModalOpen(false)} 58 | onAfterOpen={afterOpenModal} 59 | onInstrumentSelect={handleNewTrack} 60 | instrumentList={getInstrumentListIndexedByGroup(props.instruments)} 61 | /> 62 |
63 | ) 64 | } 65 | 66 | const mapStateToProps = (state: IAppState) => ({ 67 | instruments: state.instruments, 68 | samples: state.samples 69 | }) 70 | 71 | const AddTrackConnected = connect( 72 | mapStateToProps, 73 | { addTrack } 74 | )(AddTrack) 75 | 76 | export default AddTrackConnected 77 | -------------------------------------------------------------------------------- /src/components/Sequencer/AddTrack/AddTrackButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import { usePrefs } from "../../context/sequencer-prefs" 6 | import Color, { MaterialColor } from "../../../utils/color/colorLibrary" 7 | 8 | interface IProps { 9 | color: MaterialColor 10 | onClick: () => void 11 | } 12 | 13 | const StyledButton = styled.div<{ 14 | width: number 15 | height: number 16 | color: MaterialColor 17 | }>` 18 | cursor: pointer; 19 | display: flex; 20 | justify-content: flex-start; 21 | align-items: center; 22 | width: ${({ width }) => width}px; 23 | height: ${({ height }) => height}px; 24 | background: ${({ color }) => Color.get800(color)}; 25 | border-radius: 3px; 26 | color: ${({ color }) => Color.get500(color)}; 27 | 28 | &:hover { 29 | background: ${({ color }) => Color.get700(color)}; 30 | color: ${({ color }) => Color.get100(color)}; 31 | } 32 | ` 33 | 34 | const StyledButtonLabel = styled.div<{ gutter: number }>` 35 | margin-left: ${({ gutter }) => gutter}px; 36 | font-size: 16px; 37 | ` 38 | 39 | function AddTrackButton({ color, onClick }: IProps) { 40 | const { panelWidth, cellSize, gutter } = usePrefs() 41 | 42 | return ( 43 | 49 | Add... 50 | 51 | ) 52 | } 53 | 54 | export default AddTrackButton 55 | -------------------------------------------------------------------------------- /src/components/Sequencer/AddTrack/AddTrackModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import Modal from "react-modal" 3 | // tslint:disable-next-line:no-submodule-imports 4 | import styled from "styled-components/macro" 5 | 6 | import { usePrefs } from "../../context/sequencer-prefs" 7 | 8 | import Color, { MaterialColor } from "../../../utils/color/colorLibrary" 9 | import { IInstrument } from "../../../redux/store/instrument/interfaces" 10 | 11 | interface IProps { 12 | color: MaterialColor 13 | isOpen: boolean 14 | onAfterOpen: () => void 15 | onClose: () => void 16 | onInstrumentSelect: (instrument: IInstrument) => void 17 | instrumentList: { [group: string]: { [instrumentID: string]: IInstrument } } 18 | } 19 | 20 | const StyledModal = styled.div<{ gutter: number; color: MaterialColor }>` 21 | display: flex; 22 | flex-direction: column; 23 | margin: ${({ gutter }) => gutter * 2}px; 24 | color: ${({ color }) => Color.get100(color)}; 25 | ` 26 | 27 | const StyledModalHeader = styled.div` 28 | display: flex; 29 | 30 | & h2 { 31 | margin-top: 0; 32 | } 33 | ` 34 | 35 | const StyledModalBody = styled.div` 36 | display: flex; 37 | flex-direction: column; 38 | ` 39 | 40 | const StyledModalParagraph = styled.div<{ color: MaterialColor }>` 41 | color: ${({ color }) => Color.get300(color)}; 42 | font-weight: 300; 43 | ` 44 | 45 | const StyledList = styled.div` 46 | display: flex; 47 | flex-direction: column; 48 | ` 49 | 50 | const StyledListItem = styled.div`` 51 | 52 | const StyledListButtonItem = styled.button<{ 53 | gutter: number 54 | color: MaterialColor 55 | }>` 56 | flex: 1 0 auto; 57 | display: flex; 58 | justify-content: space-between; 59 | align-items: center; 60 | cursor: pointer; 61 | margin-bottom: ${({ gutter }) => gutter}px; 62 | padding: ${({ gutter }) => gutter * 2}px; 63 | background-color: ${({ color }) => Color.get700(color)}; 64 | font-size: 14px; 65 | color: ${({ color }) => Color.get50(color)}; 66 | line-height: 1; 67 | border: none; 68 | border-radius: 3px; 69 | 70 | &:last-of-type { 71 | margin-bottom: 0; 72 | } 73 | 74 | &:hover { 75 | background-color: ${({ color }) => Color.get600(color)}; 76 | color: white; 77 | } 78 | 79 | & .infos { 80 | //font-size: 0.9em; 81 | font-weight: 300; 82 | } 83 | ` 84 | 85 | const modalStyles = { 86 | overlay: { 87 | position: "fixed", 88 | top: 0, 89 | left: 0, 90 | right: 0, 91 | bottom: 0, 92 | backgroundColor: "rgba(0, 0, 0, 0.7)" 93 | }, 94 | content: { 95 | position: "absolute", 96 | top: "10%", 97 | left: "30%", 98 | right: "30%", 99 | bottom: "10%", 100 | border: "none", 101 | background: Color.get900(Color.BLUE_GREY), 102 | overflow: "auto", 103 | WebkitOverflowScrolling: "touch", 104 | borderRadius: "3px", 105 | outline: "none" 106 | } 107 | } 108 | 109 | Modal.setAppElement("#root") 110 | 111 | function AddTrackModal(props: IProps) { 112 | const { gutter } = usePrefs() 113 | 114 | return ( 115 | 122 | 123 | 124 |

New track

125 |
126 | 127 | 128 | Pick an instrument in the list 129 | 130 | 131 | {Object.keys(props.instrumentList).map(group => ( 132 | 133 |

{group}

134 | 135 | {Object.keys(props.instrumentList[group]).map( 136 | instrumentID => ( 137 | 142 | props.onInstrumentSelect( 143 | props.instrumentList[group][instrumentID] 144 | ) 145 | } 146 | > 147 | 148 | {props.instrumentList[group][instrumentID].label} 149 | 150 | 151 | { 152 | props.instrumentList[group][instrumentID].sampleIDs 153 | .length 154 | }{" "} 155 | sample 156 | {props.instrumentList[group][instrumentID].sampleIDs 157 | .length > 1 && "s"} 158 | 159 | 160 | ) 161 | )} 162 | 163 |
164 | ))} 165 |
166 |
167 |
168 |
169 | ) 170 | } 171 | 172 | export default AddTrackModal 173 | -------------------------------------------------------------------------------- /src/components/Sequencer/Sequencer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import { usePrefs } from "../context/sequencer-prefs" 6 | import AddTrack from "./AddTrack/AddTrack" 7 | import Track from "./Track/Track" 8 | import Color from "../../utils/color/colorLibrary" 9 | import { connect } from "react-redux" 10 | import { IAppState } from "../../redux/store/configureStore" 11 | 12 | interface IProps { 13 | trackOrder: string[] 14 | } 15 | 16 | const StyledSequencer = styled.div<{ 17 | panelWidth: number 18 | cellSize: number 19 | gutter: number 20 | }>` 21 | background-color: transparent; 22 | background-image: ${({ panelWidth, cellSize, gutter }) => `linear-gradient( 23 | 90deg, 24 | transparent ${panelWidth + gutter / 2.0 - 1}px, 25 | rgba(255, 255, 255, 0.2) ${panelWidth + gutter / 2.0 - 1}px, 26 | rgba(255, 255, 255, 0.2) ${panelWidth + gutter / 2.0 + 1}px, 27 | transparent ${panelWidth + gutter / 2.0 + 1}px 28 | ), 29 | linear-gradient( 30 | 90deg, 31 | transparent ${panelWidth + 32 | gutter / 2.0 - 33 | 1 + 34 | (8 * cellSize + 8 * gutter)}px, 35 | rgba(255, 255, 255, 0.2) ${panelWidth + 36 | gutter / 2.0 - 37 | 1 + 38 | (8 * cellSize + 8 * gutter)}px, 39 | rgba(255, 255, 255, 0.2) ${panelWidth + 40 | gutter / 2.0 + 41 | 1 + 42 | (8 * cellSize + 8 * gutter)}px, 43 | transparent ${panelWidth + gutter / 2.0 + 1 + (8 * cellSize + 8 * gutter)}px 44 | ), 45 | linear-gradient( 46 | 90deg, 47 | transparent ${panelWidth + 48 | gutter / 2.0 - 49 | 1 + 50 | (8 * cellSize + 8 * gutter) * 2}px, 51 | rgba(255, 255, 255, 0.2) ${panelWidth + 52 | gutter / 2.0 - 53 | 1 + 54 | (8 * cellSize + 8 * gutter) * 2}px, 55 | rgba(255, 255, 255, 0.2) ${panelWidth + 56 | gutter / 2.0 + 57 | 1 + 58 | (8 * cellSize + 8 * gutter) * 2}px, 59 | transparent ${panelWidth + 60 | gutter / 2.0 + 61 | 1 + 62 | (8 * cellSize + 8 * gutter) * 2}px 63 | ), 64 | linear-gradient( 65 | 90deg, 66 | transparent ${panelWidth + 67 | gutter / 2.0 - 68 | 1 + 69 | (8 * cellSize + 8 * gutter) * 3}px, 70 | rgba(255, 255, 255, 0.2) ${panelWidth + 71 | gutter / 2.0 - 72 | 1 + 73 | (8 * cellSize + 8 * gutter) * 3}px, 74 | rgba(255, 255, 255, 0.2) ${panelWidth + 75 | gutter / 2.0 + 76 | 1 + 77 | (8 * cellSize + 8 * gutter) * 3}px, 78 | transparent ${panelWidth + 79 | gutter / 2.0 + 80 | 1 + 81 | (8 * cellSize + 8 * gutter) * 3}px 82 | ), 83 | linear-gradient( 84 | 90deg, 85 | transparent ${panelWidth + 86 | gutter / 2.0 - 87 | 1 + 88 | (8 * cellSize + 8 * gutter) * 4}px, 89 | rgba(255, 255, 255, 0.2) ${panelWidth + 90 | gutter / 2.0 - 91 | 1 + 92 | (8 * cellSize + 8 * gutter) * 4}px, 93 | rgba(255, 255, 255, 0.2) ${panelWidth + 94 | gutter / 2.0 + 95 | 1 + 96 | (8 * cellSize + 8 * gutter) * 4}px, 97 | transparent ${panelWidth + 98 | gutter / 2.0 + 99 | 1 + 100 | (8 * cellSize + 8 * gutter) * 4}px 101 | ) 102 | `}; 103 | 104 | background-size: 100%; 105 | ` 106 | 107 | const Row = styled.div<{ first: boolean; gutter: number }>` 108 | margin-top: ${({ first, gutter }) => (first ? 0 : gutter)}px; 109 | ` 110 | 111 | const AddTrackWrapper = styled.div` 112 | display: flex; 113 | justify-content: flex-start; 114 | ` 115 | 116 | export function Sequencer({ trackOrder }: IProps) { 117 | const { panelWidth, cellSize, gutter } = usePrefs() 118 | 119 | return ( 120 |
121 | 126 | {trackOrder.map((trackID, idx) => ( 127 | 128 | 129 | 130 | ))} 131 | 132 | 133 | 134 | 135 | 136 | 137 |
138 | ) 139 | } 140 | 141 | const mapStateToProps = (state: IAppState) => ({ 142 | trackOrder: state.session.trackOrder 143 | }) 144 | 145 | const SequencerConnected = connect(mapStateToProps)(Sequencer) 146 | 147 | export default SequencerConnected 148 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/CellRow/Cell/Cell.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | import { withKnobs, select, number, boolean } from "@storybook/addon-knobs" 5 | 6 | import { Cell } from "./Cell" 7 | import { 8 | withContainer, 9 | withPrefsProvider 10 | } from "../../../../../../.storybook/decorators" 11 | import Color, { trackColors } from "../../../../../utils/color/colorLibrary" 12 | 13 | storiesOf("Cell", module) 14 | .addDecorator(withKnobs) 15 | .addDecorator(withPrefsProvider) 16 | .addDecorator(withContainer) 17 | .addParameters({ 18 | info: { 19 | inline: true, 20 | header: false 21 | } 22 | }) 23 | .add("default — use knobs", () => { 24 | const c = select("Color", trackColors, Color.DEEP_PURPLE) 25 | const rendered = boolean("Rendered", true) 26 | const played = boolean("Played", true) 27 | const scheduled = boolean("Scheduled", true) 28 | const edited = boolean("Edited", false) 29 | const gain = number("Gain - min: 0", 0.7) 30 | 31 | return ( 32 |
33 | 47 | 61 | 75 |
76 | ) 77 | }) 78 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/CellRow/Cell/Cell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | import { connect } from "react-redux" 5 | 6 | import Color, { 7 | hexToRgb, 8 | MaterialColor 9 | } from "../../../../../utils/color/colorLibrary" 10 | import colorLuminance from "../../../../../utils/color/colorLuminance" 11 | import { AnyAction, getCell, getTrack } from "../../../../../redux/reducers" 12 | import { isCellPlayed } from "../../../../../services/cell" 13 | import { toggleTrackCell } from "../../../../../redux/actions/session/creators" 14 | import { usePrefs } from "../../../../context/sequencer-prefs" 15 | import { IGainProcessing, ISession, ITrack, NoteResolution } from "../../../../../redux/store/session/interfaces" 16 | import { IAppState } from "../../../../../redux/store/configureStore" 17 | import { Dispatch } from "redux" 18 | 19 | interface IOwnProps { 20 | trackID: string 21 | beatNumber: number 22 | gutter: number 23 | } 24 | 25 | interface IProps extends IOwnProps { 26 | activeTrackID: ISession['activeTrackID'] 27 | color: ITrack["color"] 28 | noteResolution: ITrack["noteResolution"] 29 | gain: IGainProcessing['gain'] 30 | played: boolean 31 | scheduled: boolean 32 | edited: boolean 33 | rendered: boolean 34 | onClick: (beat: number, trackID: string) => void 35 | } 36 | 37 | const cellWidth = ( 38 | resolution: NoteResolution, 39 | size: number, 40 | margin: number 41 | ): number => { 42 | if (resolution === 1) { 43 | return (size + margin) / 2.0 - margin 44 | } 45 | if (resolution === 2) { 46 | return size 47 | } 48 | if (resolution === 4) { 49 | return (size + margin) * 2.0 - margin 50 | } 51 | 52 | return size 53 | } 54 | 55 | const borderColor = ( 56 | color: MaterialColor, 57 | edited: boolean, 58 | played: boolean, 59 | scheduled: boolean, 60 | hover: boolean 61 | ): string => { 62 | if (edited) { 63 | return hover ? Color.get50(color) : Color.get100(color) 64 | } 65 | 66 | if (played) { 67 | return hover ? Color.get100(color) : Color.getA100(color) 68 | } 69 | 70 | if (scheduled) { 71 | return hover ? Color.getA100(color) : Color.getA700(color) 72 | } 73 | 74 | return hover 75 | ? Color.getA100(color) 76 | : colorLuminance(Color.getA700(color), -0.4) 77 | } 78 | 79 | const StyledCell = styled.button<{ 80 | size: number 81 | noteResolution: NoteResolution 82 | gutter: number 83 | edited: boolean 84 | played: boolean 85 | scheduled: boolean 86 | color: MaterialColor 87 | gain: number 88 | }>` 89 | cursor: pointer; 90 | height: ${({ size }) => size}px; 91 | width: ${({ noteResolution, size, gutter }) => 92 | cellWidth(noteResolution, size, gutter)}px; 93 | margin-right: ${({ gutter }) => gutter}px; 94 | padding: 0; 95 | border: 3px solid 96 | ${({ color, edited, played, scheduled }) => 97 | borderColor(color, edited, played, scheduled, false)}; 98 | border-radius: 3px; 99 | background-color: ${({ scheduled, color, gain }) => { 100 | const rgb = hexToRgb(Color.getA700(color)) 101 | return scheduled 102 | ? `rgba(${rgb ? rgb.r : 255}, ${rgb ? rgb.g : 255}, ${ 103 | rgb ? rgb.b : 255 104 | }, ${gain})` 105 | : "transparent" 106 | }}; 107 | 108 | &:hover { 109 | border-color: ${({ color, edited, played, scheduled }) => 110 | borderColor(color, edited, played, scheduled, true)}; 111 | background-color: ${({ color, scheduled }) => { 112 | return scheduled ? Color.getA400(color) : "transparent" 113 | }}; 114 | } 115 | ` 116 | 117 | Cell.defaultProps = { 118 | rendered: true, 119 | edited: false, 120 | gutter: 0 121 | } 122 | 123 | export function Cell(props: IProps) { 124 | if (!props.rendered) { 125 | return
126 | } 127 | 128 | const { cellSize } = usePrefs() 129 | const buttonRef = React.createRef() 130 | 131 | const handleClick = () => { 132 | props.onClick(props.beatNumber, props.trackID) 133 | // tslint:disable-next-line:no-unused-expression 134 | buttonRef.current && buttonRef.current.blur() 135 | } 136 | 137 | return ( 138 | 150 | {" "} 151 | 152 | ) 153 | } 154 | 155 | const MemoizedCell = React.memo(Cell) 156 | 157 | const mapStateToProps = (state: IAppState, ownProps: IOwnProps) => { 158 | const track = getTrack(state, ownProps.trackID) 159 | const cell = getCell(state, ownProps.trackID, ownProps.beatNumber) 160 | 161 | return { 162 | activeTrackID: state.session.activeTrackID, 163 | color: track.color, 164 | noteResolution: track.noteResolution, 165 | gain: cell.processing.gain.gain, 166 | played: isCellPlayed( 167 | track.noteResolution, 168 | ownProps.beatNumber, 169 | state.audio.currentBeat 170 | ), 171 | scheduled: cell.scheduled, 172 | edited: 173 | ownProps.trackID === state.session.activeTrackID && 174 | ownProps.beatNumber === state.session.activeCellBeat, 175 | // we don't want to keep cells out of note resolution 176 | rendered: ownProps.beatNumber % track.noteResolution === 0 177 | } 178 | } 179 | 180 | const mapDispatchToProps = ( 181 | dispatch: Dispatch, 182 | ownProps: IOwnProps 183 | ) => ({ 184 | onClick: () => 185 | dispatch(toggleTrackCell(ownProps.beatNumber, ownProps.trackID)) 186 | }) 187 | 188 | const CellWithConnect = connect( 189 | mapStateToProps, 190 | mapDispatchToProps 191 | )(MemoizedCell) 192 | 193 | export default CellWithConnect 194 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/CellRow/CellRow.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | 4 | import CellRow from "./CellRow" 5 | import { 6 | withContainer, 7 | withPrefsProvider, 8 | withReduxProvider, 9 | stateFixture 10 | } from "../../../../../.storybook/decorators" 11 | import { IAppState } from "../../../../redux/store/configureStore" 12 | 13 | const state: IAppState = { 14 | ...stateFixture, 15 | audio: { 16 | ...stateFixture.audio, 17 | currentBeat: 0 18 | }, 19 | session: { 20 | ...stateFixture.session, 21 | activeTrackID: "4", 22 | activeCellBeat: 12 23 | } 24 | } 25 | 26 | storiesOf("CellRow", module) 27 | .addParameters({ 28 | info: { 29 | inline: true, 30 | header: false, 31 | } 32 | }) 33 | .addDecorator(withReduxProvider(state)) 34 | .addDecorator(withPrefsProvider) 35 | .addDecorator(withContainer) 36 | .add("sixteenth notes", () => , { 37 | info: { 38 | text: ` 39 | ##### Track settings: 40 | * __sixteenth__ notes display 41 | * not active 42 | * active beat : 12 43 | * current beat : 0 44 | ` 45 | } 46 | }) 47 | .add("eighth notes", () => , { 48 | info: { 49 | text: ` 50 | ##### Track settings: 51 | * __eighth notes__ display 52 | * not active 53 | * active beat : 12 54 | * current beat : 0 55 | ` 56 | } 57 | }) 58 | .add("quarter notes", () => , { 59 | info: { 60 | text: ` 61 | ##### Track settings: 62 | * __quarter notes__ display 63 | * not active 64 | * current beat : 0 65 | ` 66 | } 67 | }) 68 | .add("sixteenth notes — active track", () => , { 69 | info: { 70 | text: ` 71 | ##### Track settings: 72 | * __sixteenth notes__ display 73 | * __active__ track 74 | * active beat : 12 75 | * current beat : 0 76 | ` 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/CellRow/CellRow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import Cell from "./Cell/Cell" 6 | import { usePrefs } from "../../../context/sequencer-prefs" 7 | 8 | interface IProps { 9 | trackID: string 10 | } 11 | 12 | const StyledCellRow = styled.div` 13 | display: flex; 14 | ` 15 | 16 | const CellRowMemo = React.memo(function CellRow({ trackID }: IProps) { 17 | const { gutter } = usePrefs() 18 | 19 | return ( 20 | 21 | {Array.from(Array(64).keys()).map(beat => { 22 | return ( 23 | 29 | ) 30 | })} 31 | 32 | ) 33 | }) 34 | 35 | export default CellRowMemo 36 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/Track.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | 4 | import Track from "./Track" 5 | import { 6 | withContainer, 7 | withPrefsProvider, 8 | withReduxProvider, 9 | stateFixture 10 | } from "../../../../.storybook/decorators" 11 | 12 | storiesOf("Track", module) 13 | .addDecorator(withReduxProvider(stateFixture)) 14 | .addDecorator(withPrefsProvider) 15 | .addDecorator(withContainer) 16 | .addParameters({ 17 | info: { 18 | inline: true, 19 | header: false 20 | } 21 | }) 22 | .add("active", () => ) 23 | .add("not active", () => ) 24 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/Track.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | import { connect } from "react-redux" 5 | 6 | import TrackHeader from "./TrackHeader/TrackHeader" 7 | import CellRow from "./CellRow/CellRow" 8 | import TrackPanel from "./TrackPanel/TrackPanel" 9 | import { usePrefs } from "../../context/sequencer-prefs" 10 | 11 | import { IAppState } from "../../../redux/store/configureStore" 12 | 13 | interface IOwnProps { 14 | trackID: string 15 | } 16 | 17 | interface IProps extends IOwnProps { 18 | activeTrackID: string | null 19 | } 20 | 21 | const HeaderContainer = styled.div` 22 | display: flex; 23 | ` 24 | 25 | const PanelContainer = styled.div<{ gutter: number }>` 26 | margin-top: ${({ gutter }) => gutter}px; 27 | ` 28 | 29 | const Gutter = styled.div<{ gutter: number }>` 30 | margin-left: ${({ gutter }) => gutter}px; 31 | ` 32 | 33 | export function Track(props: IProps) { 34 | const { gutter } = usePrefs() 35 | 36 | return ( 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | {props.trackID === props.activeTrackID && ( 45 | 46 | 47 | 48 | )} 49 |
50 | ) 51 | } 52 | 53 | const TrackMemoized = React.memo(Track) 54 | 55 | const mapStateToProps = (state: IAppState) => { 56 | return { 57 | activeTrackID: state.session.activeTrackID 58 | } 59 | } 60 | 61 | const TrackWithConnect = connect(mapStateToProps)(TrackMemoized) 62 | 63 | export default TrackWithConnect 64 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackHeader/MuteButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import Color, { MaterialColor } from "../../../../utils/color/colorLibrary" 6 | 7 | interface IProps { 8 | color: MaterialColor 9 | width: number 10 | muted: boolean 11 | onClick: () => void 12 | } 13 | 14 | const StyledButton = styled.button<{ 15 | width: number 16 | color: MaterialColor 17 | muted: boolean 18 | }>` 19 | cursor: pointer; 20 | user-select: none; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | width: ${({ width }) => width}px; 25 | margin-left: 0.5rem; 26 | border: none; 27 | border-radius: 3px; 28 | background-color: ${({ color, muted }) => 29 | muted ? Color.get50(color) : Color.get400(color)}; 30 | text-align: center; 31 | line-height: 0; 32 | font-size: 13px; 33 | font-weight: 700; 34 | color: ${({ color, muted }) => 35 | muted ? Color.get900(color) : Color.get800(color)}; 36 | 37 | &:hover { 38 | background-color: ${({ color, muted }) => 39 | muted ? "white" : Color.get300(color)}; 40 | } 41 | ` 42 | 43 | export const MuteButtonMemo = React.memo(function MuteButton( 44 | props: IProps 45 | ) { 46 | const buttonRef = React.createRef() 47 | 48 | return ( 49 | { 55 | event.stopPropagation() 56 | props.onClick() 57 | // tslint:disable-next-line:no-unused-expression 58 | buttonRef.current && buttonRef.current.blur() 59 | }} 60 | > 61 | M 62 | 63 | ) 64 | }) 65 | 66 | export default MuteButtonMemo 67 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackHeader/SoloButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import Color, { MaterialColor } from "../../../../utils/color/colorLibrary" 6 | 7 | interface IProps { 8 | color: MaterialColor 9 | width: number 10 | soloed: boolean 11 | onClick: () => void 12 | } 13 | 14 | const StyledButton = styled.button<{ 15 | width: number 16 | color: MaterialColor 17 | soloed: boolean 18 | }>` 19 | cursor: pointer; 20 | user-select: none; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | width: ${({ width }) => width}px; 25 | margin-left: 0.5rem; 26 | border: none; 27 | border-radius: 3px; 28 | background-color: ${({ color, soloed }) => 29 | soloed ? Color.get50(color) : Color.get400(color)}; 30 | text-align: center; 31 | line-height: 0; 32 | font-size: 13px; 33 | font-weight: 700; 34 | color: ${({ color, soloed }) => 35 | soloed ? Color.get900(color) : Color.get800(color)}; 36 | 37 | &:hover { 38 | background-color: ${({ color, soloed }) => 39 | soloed ? "white" : Color.get300(color)}; 40 | } 41 | ` 42 | 43 | export const SoloButtonMemo = React.memo(function SoloButton( 44 | props: IProps 45 | ) { 46 | const buttonRef = React.createRef() 47 | 48 | return ( 49 | { 55 | event.stopPropagation() 56 | props.onClick() 57 | // tslint:disable-next-line:no-unused-expression 58 | buttonRef.current && buttonRef.current.blur() 59 | }} 60 | > 61 | S 62 | 63 | ) 64 | }) 65 | 66 | export default SoloButtonMemo 67 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackHeader/TrackHeader.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | import { 5 | withKnobs, 6 | text, 7 | select, 8 | number, 9 | boolean 10 | } from "@storybook/addon-knobs" 11 | 12 | import { TrackHeader } from "./TrackHeader" 13 | import { 14 | withContainer, 15 | withPrefsProvider 16 | } from "../../../../../.storybook/decorators" 17 | import Color, { trackColors } from "../../../../utils/color/colorLibrary" 18 | 19 | storiesOf("TrackHeader", module) 20 | .addParameters({ 21 | info: { 22 | inline: true, 23 | header: false 24 | } 25 | }) 26 | .addDecorator(withKnobs) 27 | .addDecorator(withPrefsProvider) 28 | .addDecorator(withContainer) 29 | .add("default", () => { 30 | const label = text("Label", "Track number one") 31 | const gain = number("Gain", 0.7) 32 | const color = select("Color", trackColors, Color.RED) 33 | const soloed = boolean("Solo", false) 34 | const muted = boolean("Mute", false) 35 | 36 | return ( 37 | 49 | ) 50 | }) 51 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackHeader/TrackHeader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | import { connect } from "react-redux" 5 | 6 | import Volume from "../../../../audio/utils/Volume/Volume" 7 | import MuteButton from "./MuteButton" 8 | import SoloButton from "./SoloButton" 9 | import TrackLabel from "./TrackLabel" 10 | import { usePrefs } from "../../../context/sequencer-prefs" 11 | import Color, { MaterialColor } from "../../../../utils/color/colorLibrary" 12 | import { IAppState } from "../../../../redux/store/configureStore" 13 | import { 14 | changeTrackLabel, 15 | toggleActiveTrack, 16 | toggleMuteTrack, 17 | toggleSoloTrack 18 | } from "../../../../redux/actions/session/creators" 19 | import { Dispatch } from "redux" 20 | import { AnyAction } from "../../../../redux/reducers" 21 | 22 | interface IOwnProps { 23 | trackID: string 24 | } 25 | 26 | interface IProps extends IOwnProps { 27 | color: MaterialColor 28 | muted: boolean 29 | soloed: boolean 30 | gain: number 31 | label: string 32 | onMuteClick: () => void 33 | onSoloClick: () => void 34 | onTitleClick: () => void 35 | changeTrackLabel: (label: string) => void 36 | } 37 | 38 | const Container = styled.div` 39 | cursor: pointer; 40 | ` 41 | 42 | const StyledTrackHeader = styled.div<{ 43 | width: number 44 | height: number 45 | gutter: number 46 | color: MaterialColor 47 | }>` 48 | flex-shrink: 0; 49 | display: flex; 50 | justify-content: flex-start; 51 | align-items: stretch; 52 | width: ${({ width }) => width}px; 53 | height: ${({ height }) => height}px; 54 | padding: ${({ gutter }) => gutter}px; 55 | border-radius: 3px; 56 | background-color: ${({ color }) => Color.get800(color)}; 57 | color: ${({ color }) => Color.get100(color)}; 58 | 59 | &:hover { 60 | background-color: ${({ color }) => Color.get600(color)}; 61 | color: ${({ color }) => Color.get50(color)}; 62 | } 63 | ` 64 | 65 | const StyledControls = styled.div` 66 | display: flex; 67 | ` 68 | 69 | const GainIndicator = styled.div` 70 | user-select: none; 71 | margin-left: 0.5rem; 72 | text-align: center; 73 | display: flex; 74 | justify-content: center; 75 | align-items: center; 76 | line-height: 0; 77 | font-size: 13px; 78 | ` 79 | 80 | const StyledLabelForm = styled.form<{ gutter: number; color: MaterialColor }>` 81 | flex: 1; 82 | display: flex; 83 | justify-content: flex-start; 84 | 85 | & input { 86 | padding: ${({ gutter }) => gutter}px; 87 | border: 1px solid ${({ color }) => Color.get300(color)}; 88 | border-radius: 3px; 89 | background-color: ${({ color }) => Color.get400(color)}; 90 | color: ${({ color }) => Color.get900(color)}; 91 | font-size: 13px; 92 | line-height: 0; 93 | } 94 | ` 95 | 96 | // https://css-tricks.com/snippets/javascript/bind-different-events-to-click-and-double-click/#comment-1671033 97 | let timer: number 98 | const delay = 200 99 | 100 | export function TrackHeader(props: IProps) { 101 | const [labelEdited, setLabelEdited] = React.useState(false) 102 | const [clicked, setClicked] = React.useState(false) 103 | 104 | const { panelWidth, cellSize, gutter } = usePrefs() 105 | 106 | const doubleClickAction = () => { 107 | setLabelEdited(true) 108 | } 109 | 110 | const singleClickAction = () => { 111 | props.onTitleClick() 112 | } 113 | 114 | const handleClick = () => { 115 | if (labelEdited) { 116 | return 117 | } 118 | 119 | if (clicked) { 120 | clearTimeout(timer) 121 | doubleClickAction() 122 | setClicked(false) 123 | } else { 124 | setClicked(true) 125 | 126 | timer = setTimeout(() => { 127 | setLabelEdited(false) 128 | singleClickAction() 129 | setClicked(false) 130 | }, delay) 131 | } 132 | } 133 | 134 | const handleSubmit = (event: React.FormEvent) => { 135 | event.preventDefault() 136 | const formData = new FormData(event.currentTarget) 137 | 138 | const submittedLabel = formData.get("label") 139 | 140 | if (typeof submittedLabel === "string") { 141 | props.changeTrackLabel(submittedLabel) 142 | setLabelEdited(false) 143 | } 144 | } 145 | 146 | return ( 147 | 148 | 154 | {labelEdited ? ( 155 | 160 | 161 | 162 | ) : ( 163 | 164 | 165 | 166 | {Volume.toDBString(props.gain)} 167 | props.onSoloClick()} 172 | /> 173 | props.onMuteClick()} 178 | /> 179 | 180 | 181 | )} 182 | 183 | 184 | ) 185 | } 186 | 187 | const mapStateToProps = (state: IAppState, ownProps: IOwnProps) => { 188 | const track = state.session.tracks[ownProps.trackID] 189 | 190 | return { 191 | label: track.label, 192 | color: track.color, 193 | gain: track.processing.gain.gain, 194 | muted: track.muted, 195 | soloed: track.soloed 196 | } 197 | } 198 | 199 | const TrackHeaderMemoized = React.memo(TrackHeader) 200 | 201 | const mapDispatchToProps = ( 202 | dispatch: Dispatch, 203 | ownProps: IOwnProps 204 | ) => ({ 205 | onMuteClick: () => dispatch(toggleMuteTrack(ownProps.trackID)), 206 | onSoloClick: () => dispatch(toggleSoloTrack(ownProps.trackID)), 207 | onTitleClick: () => dispatch(toggleActiveTrack(ownProps.trackID)), 208 | changeTrackLabel: (label: string) => 209 | dispatch(changeTrackLabel(label, ownProps.trackID)) 210 | }) 211 | 212 | const TrackHeaderWithConnect = connect( 213 | mapStateToProps, 214 | mapDispatchToProps 215 | )(TrackHeaderMemoized) 216 | 217 | export default TrackHeaderWithConnect 218 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackHeader/TrackLabel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import Color, { MaterialColor } from "../../../../utils/color/colorLibrary" 6 | 7 | interface IProps { 8 | label: string 9 | color: MaterialColor 10 | } 11 | 12 | const StyledLabel = styled.div<{ color: MaterialColor }>` 13 | flex: 1; 14 | user-select: none; 15 | display: flex; 16 | justify-content: flex-start; 17 | align-items: center; 18 | font-size: 16px; 19 | line-height: 0; 20 | color: ${({ color }) => Color.get100(color)}; 21 | 22 | &:hover { 23 | color: ${({ color }) => Color.get50(color)}; 24 | } 25 | ` 26 | 27 | function TrackLabel(props: IProps) { 28 | return {props.label} 29 | } 30 | 31 | export default TrackLabel 32 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/CellSettings/CellSettings.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | 4 | import CellSettings from "./CellSettings" 5 | import { 6 | withContainer, 7 | withPrefsProvider, 8 | withReduxProvider, 9 | stateFixture 10 | } from "../../../../../../.storybook/decorators" 11 | 12 | storiesOf("CellSettings", module) 13 | .addParameters({ 14 | info: { 15 | inline: true, 16 | header: false 17 | } 18 | }) 19 | .addDecorator(withReduxProvider(stateFixture)) 20 | .addDecorator(withPrefsProvider) 21 | .addDecorator(withContainer) 22 | .add("default", () => ) 23 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/CellSettings/CellSettings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | import { connect } from "react-redux" 5 | 6 | import MidiConverter from "../../../../../utils/audio/MidiConverter" 7 | import GainKnob from "./GainKnob/GainKnob" 8 | import { usePrefs } from "../../../../context/sequencer-prefs" 9 | import { Cell } from "../../CellRow/Cell/Cell" 10 | import NoteSelector from "./NoteSelector/NoteSelector" 11 | import { 12 | getActiveCell, 13 | getActiveTrack, 14 | getInstrumentMapping 15 | } from "../../../../../redux/reducers" 16 | import { 17 | changeCellGain, 18 | scheduleTrackCell 19 | } from "../../../../../redux/actions/session/creators" 20 | import Color, { MaterialColor } from "../../../../../utils/color/colorLibrary" 21 | import { 22 | ICell, 23 | NoteResolution 24 | } from "../../../../../redux/store/session/interfaces" 25 | import { IAppState } from "../../../../../redux/store/configureStore" 26 | import { IInstrumentMapping } from "../../../../../redux/store/instrument/interfaces" 27 | 28 | interface IProps { 29 | activeTrackID: string | null 30 | activeCellBeat: number | null 31 | color: MaterialColor 32 | noteResolution: NoteResolution 33 | cell: ICell | null 34 | mapping: (note: number) => IInstrumentMapping | null 35 | scheduleTrackCell: (beat: number, trackID: string) => void 36 | changeCellGain: (gain: number, beat: number, trackID: string) => void 37 | } 38 | 39 | const StyledSettings = styled.div<{ 40 | cellSize: number 41 | gutter: number 42 | height: number 43 | color: MaterialColor 44 | }>` 45 | flex-shrink: 0; 46 | border-radius: 3px; 47 | width: ${({ cellSize, gutter }) => cellSize * 32 + gutter * 31}px; 48 | height: ${({ height }) => height}px; 49 | margin-right: ${({ gutter }) => gutter}px; 50 | background-color: ${({ color }) => Color.get800Dark(color)}; 51 | color: ${({ color }) => Color.get100(color)}; 52 | ` 53 | 54 | const StyledNoteSection = styled.section<{ gutter: number }>` 55 | display: flex; 56 | justify-content: space-between; 57 | padding: ${({ gutter }) => gutter * 2}px; 58 | overflow: auto; 59 | ` 60 | 61 | const CellInfo = styled.div` 62 | display: flex; 63 | flex-direction: column; 64 | justify-content: space-between; 65 | font-size: 14px; 66 | ` 67 | 68 | const StyledGainSection = styled.section` 69 | display: flex; 70 | justify-content: space-between; 71 | ` 72 | 73 | export function CellSettings(props: IProps) { 74 | if (props.activeTrackID === null) { return
} 75 | 76 | const { panelHeight, cellSize, gutter } = usePrefs() 77 | 78 | if (props.activeCellBeat === null || props.cell === null) { 79 | return ( 80 | 86 | {" "} 87 | 88 | ) 89 | } 90 | 91 | const detune = (cell: ICell | null): string => { 92 | if (cell == null) { 93 | return "" 94 | } 95 | 96 | const mapping = props.mapping(cell.midi) 97 | 98 | if (mapping === null) { return "-" } 99 | 100 | return mapping.detune + "" 101 | } 102 | 103 | return ( 104 | 110 | 111 | 112 | { 124 | if ( 125 | props.activeCellBeat === null || 126 | props.activeTrackID === null 127 | ) { 128 | return 129 | } 130 | 131 | props.scheduleTrackCell(props.activeCellBeat, props.activeTrackID) 132 | }} 133 | /> 134 |
135 |
136 | BEAT{" "} 137 | {props.activeCellBeat} 138 |
139 |
140 | NOTE{" "} 141 | 142 | {MidiConverter.toNote(props.cell.midi)} 143 | {" "} 144 | ({props.cell ? props.cell.midi : 0}) 145 |
146 |
147 | detune{" "} 148 | {detune(props.cell)} cent 149 |
150 |
151 |
152 |
153 | 154 |
155 |
156 |
157 | 158 | 164 | props.changeCellGain( 165 | value, 166 | props.activeCellBeat || 0, 167 | props.activeTrackID || "" 168 | ) 169 | } 170 | /> 171 | 172 |
173 |
174 | ) 175 | } 176 | 177 | const mapStateToProps = (state: IAppState) => { 178 | const activeTrack = getActiveTrack(state) 179 | const activeCell = getActiveCell(state) 180 | 181 | return { 182 | color: activeTrack ? activeTrack.color : Color.GREY, 183 | noteResolution: activeTrack ? activeTrack.noteResolution : 1, 184 | cell: activeCell, 185 | activeCellBeat: state.session.activeCellBeat, 186 | activeTrackID: state.session.activeTrackID, 187 | mapping: (note: number) => 188 | getInstrumentMapping(state, state.session.activeTrackID, note) 189 | } 190 | } 191 | 192 | const CellSettingsWithConnect = connect( 193 | mapStateToProps, 194 | { scheduleTrackCell, changeCellGain } 195 | )(CellSettings) 196 | 197 | export default CellSettingsWithConnect 198 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/CellSettings/GainKnob/GainKnob.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import Knob from "../../../../../controllers/Knob/Knob" 6 | import Volume from "../../../../../../audio/utils/Volume/Volume" 7 | import { MaterialColor } from "../../../../../../utils/color/colorLibrary" 8 | 9 | interface IProps { 10 | // ownProps 11 | color: MaterialColor 12 | gutter: number 13 | size: number 14 | // stateProps 15 | gain: number 16 | onChange: (value: number) => void 17 | } 18 | 19 | const StyledWrapper = styled.div` 20 | display: flex; 21 | flex-direction: column; 22 | ` 23 | 24 | const StyledKnob = styled.div<{ size: number }>` 25 | height: ${({ size }) => size}px; 26 | ` 27 | 28 | const GainIndicator = styled.div<{ gutter: number }>` 29 | user-select: none; 30 | margin-top: ${({ gutter }) => gutter * 2}px; 31 | text-align: center; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | line-height: 0; 36 | font-size: 13px; 37 | ` 38 | 39 | function GainKnob(props: IProps) { 40 | const knobPrefs = { color: props.color, size: props.size } 41 | 42 | return ( 43 | 44 | 45 | 53 | 54 | 55 | {Volume.toDBString(props.gain)} 56 | 57 | 58 | ) 59 | } 60 | 61 | export default GainKnob 62 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/CellSettings/NoteSelector/Key.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import Color, { 6 | MaterialColor 7 | } from "../../../../../../utils/color/colorLibrary" 8 | 9 | interface IProps { 10 | width: number 11 | color: MaterialColor 12 | black: boolean 13 | midiNote: number 14 | active: boolean 15 | disabled: boolean 16 | onClick: () => void 17 | onHoverStart: () => void 18 | onHoverStop: () => void 19 | } 20 | 21 | const backgroundColor = ( 22 | color: MaterialColor, 23 | black: boolean, 24 | active: boolean, 25 | hover: boolean, 26 | disabled: boolean 27 | ) => { 28 | if (disabled) { 29 | return black ? Color.get900Dark(Color.GREY) : Color.get700(Color.GREY) 30 | } 31 | 32 | if (active) { 33 | return black ? Color.get800(color) : "white" 34 | } 35 | 36 | if (black) { 37 | return hover ? Color.get800(color) : Color.get900Dark(color) 38 | } 39 | 40 | return hover ? "white" : Color.get100(color) 41 | } 42 | 43 | const border = (color: MaterialColor, disabled: boolean, black: boolean) => { 44 | if (disabled) { 45 | return Color.get800(Color.GREY) 46 | } 47 | 48 | return black ? Color.get900(color) : Color.get200(color) 49 | } 50 | 51 | const KeyStyled = styled.button<{ 52 | width: number 53 | black: boolean 54 | active: boolean 55 | disabled: boolean 56 | color: MaterialColor 57 | }>` 58 | display: inline-block; 59 | height: 100%; 60 | width: ${({ width }) => width}px; 61 | background-color: ${({ color, black, active, disabled }) => 62 | backgroundColor(color, black, active, false, disabled)}; 63 | padding: 0; 64 | border: 1px solid ${props => border(props.color, props.disabled, props.black)}; 65 | border-bottom-left-radius: 3px; 66 | border-bottom-right-radius: 3px; 67 | cursor: ${props => (props.disabled ? "default" : "pointer")}; 68 | 69 | &:hover { 70 | background-color: ${({ color, black, active, disabled }) => 71 | backgroundColor(color, black, active, true, disabled)}; 72 | } 73 | ` 74 | 75 | const KeyMemo = React.memo(function Key(props: IProps) { 76 | const buttonRef = React.createRef() 77 | 78 | const handleClick = () => { 79 | props.onClick() 80 | // tslint:disable-next-line:no-unused-expression 81 | buttonRef.current && buttonRef.current.blur() 82 | } 83 | 84 | return ( 85 | !props.disabled && props.onHoverStart()} 94 | onMouseLeave={() => !props.disabled && props.onHoverStop()} 95 | > 96 | {" "} 97 | 98 | ) 99 | }) 100 | 101 | export default KeyMemo 102 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/CellSettings/NoteSelector/NoteSelector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | 5 | import { NoteSelector } from "./NoteSelector" 6 | import { withContainer } from "../../../../../../../.storybook/decorators" 7 | import { select, number, withKnobs } from "@storybook/addon-knobs" 8 | import Color, { trackColors } from "../../../../../../utils/color/colorLibrary" 9 | 10 | storiesOf("NoteSelector", module) 11 | .addParameters({ 12 | info: { 13 | inline: true, 14 | header: false 15 | } 16 | }) 17 | .addDecorator(withKnobs) 18 | .addDecorator(withContainer) 19 | .add("default — use knobs", () => { 20 | const color = select("Color", trackColors, Color.DEEP_PURPLE) 21 | const activeNote = number("Active note", 69) 22 | 23 | return ( 24 | { 32 | if (note < 21 || note > 98) { 33 | return null 34 | } 35 | return { midi: note, sampleID: "foo", detune: -200 } 36 | }} 37 | sample={() => ({ 38 | id: "1", 39 | filename: "filename", 40 | url: "", 41 | type: "", 42 | label: "sample label", 43 | group: "sample group" 44 | })} 45 | changeCellNote={action("changeCellNote")} 46 | listenCellNote={action("listenCellNote")} 47 | /> 48 | ) 49 | }) 50 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/CellSettings/NoteSelector/NoteSelector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | import { connect } from "react-redux" 5 | 6 | import Key from "./Key" 7 | import MidiConverter from "../../../../../../utils/audio/MidiConverter" 8 | import Color, { 9 | MaterialColor 10 | } from "../../../../../../utils/color/colorLibrary" 11 | import { 12 | getActiveCell, 13 | getActiveTrack, 14 | getInstrumentMapping, 15 | getSample 16 | } from "../../../../../../redux/reducers" 17 | import { changeCellNote } from "../../../../../../redux/actions/session/creators" 18 | import { listenCellNote } from "../../../../../../redux/actions/audio/creators" 19 | 20 | import { ISample } from "../../../../../../redux/store/sample/interfaces" 21 | import { IInstrumentMapping } from "../../../../../../redux/store/instrument/interfaces" 22 | import { IAppState } from "../../../../../../redux/store/configureStore" 23 | 24 | interface IOwnProps { 25 | height: number 26 | keyWidth: number 27 | } 28 | 29 | interface IProps extends IOwnProps { 30 | color: MaterialColor 31 | activeNote: number 32 | activeTrackID: string | null 33 | activeCellBeat: number | null 34 | mapping: (note: number) => IInstrumentMapping | null 35 | sample: (note: number) => ISample | null 36 | changeCellNote: (note: number, beat: number, TrackID: string) => void 37 | listenCellNote: (note: number, beat: number, trackID: string) => void 38 | } 39 | 40 | const StyledSelector = styled.div<{ color: MaterialColor }>` 41 | background-color: ${({ color }) => Color.get800Dark(color)}; 42 | color: ${({ color }) => Color.get100(color)}; 43 | font-size: 13px; 44 | ` 45 | 46 | const Info = styled.div<{ keyWidth: number }>` 47 | display: flex; 48 | margin-bottom: ${({ keyWidth }) => keyWidth}px; 49 | ` 50 | 51 | const InfoItem = styled.div<{ itemWidth: string; keyWidth: number }>` 52 | width: ${({ itemWidth }) => itemWidth || "10%"}; 53 | margin-left: ${({ keyWidth }) => keyWidth || 0}px; 54 | ` 55 | 56 | const Keys = styled.div<{ height: number }>` 57 | display: flex; 58 | height: ${({ height }) => height}px; 59 | ` 60 | 61 | const KeyWrapper = styled.div<{ 62 | blackKey: boolean 63 | height: number 64 | keyWidth: number 65 | ratio: number 66 | }>` 67 | margin-bottom: ${({ blackKey, height }) => (blackKey ? height * 0.4 : 0)}px; 68 | margin-left: ${({ blackKey, keyWidth, ratio }) => 69 | blackKey ? -(keyWidth * ratio) / 2.0 : 0}px; 70 | margin-right: ${({ blackKey, keyWidth, ratio }) => 71 | blackKey ? -(keyWidth * ratio) / 2.0 : 0}px; 72 | z-index: ${({ blackKey }) => (blackKey ? 10 : 1)}; 73 | ` 74 | 75 | // used to compute the width of a black key 76 | const widthRatio = 0.75 77 | 78 | export function NoteSelector(props: IProps) { 79 | const [noteOnHover, setNoteOnHover] = React.useState(null) 80 | 81 | if (props.activeTrackID === null || props.activeCellBeat === null) { 82 | return
83 | } 84 | 85 | const fileName = (note: number | null): string => { 86 | if (note == null) { 87 | return "" 88 | } 89 | 90 | const sample = props.sample(note) 91 | 92 | if (sample === null) { 93 | return "-" 94 | } 95 | 96 | return sample.label 97 | } 98 | 99 | const detune = (note: number | null): string => { 100 | if (note == null) { 101 | return "" 102 | } 103 | 104 | const mapping = props.mapping(note) 105 | 106 | if (mapping === null) { 107 | return "-" 108 | } 109 | 110 | return mapping.detune + " cent" 111 | } 112 | 113 | return ( 114 | 115 | {noteOnHover !== null ? ( 116 | 117 | 118 |
119 | NOTE{" "} 120 | {MidiConverter.toNote(noteOnHover)} ({noteOnHover}) 121 |
122 |
123 | 124 | DETUNE{" "} 125 | {detune(noteOnHover)} 126 | 127 | 128 | SAMPLE{" "} 129 | {fileName(noteOnHover)} 130 | 131 |
132 | ) : ( 133 | 134 | 135 | 🎹 136 | 137 | 138 | )} 139 | 140 | 141 | {[...Array(128).keys()].map(midiNote => { 142 | const blackKey = [1, 3, 6, 8, 10].includes(midiNote % 12) 143 | const disabled = props.mapping(midiNote) === null 144 | 145 | return ( 146 | 153 | { 161 | if ( 162 | props.activeTrackID === null || 163 | props.activeCellBeat === null || 164 | disabled 165 | ) { 166 | return 167 | } 168 | 169 | props.changeCellNote( 170 | midiNote, 171 | props.activeCellBeat, 172 | props.activeTrackID 173 | ) 174 | }} 175 | onHoverStart={() => { 176 | if ( 177 | props.activeTrackID === null || 178 | props.activeCellBeat === null 179 | ) { 180 | return 181 | } 182 | setNoteOnHover(midiNote) 183 | }} 184 | onHoverStop={() => setNoteOnHover(null)} 185 | /> 186 | 187 | ) 188 | })} 189 | 190 |
191 | ) 192 | } 193 | 194 | export const NoteSelectorMemoized = React.memo(NoteSelector) 195 | 196 | const mapStateToProps = (state: IAppState) => { 197 | const track = getActiveTrack(state) 198 | const cell = getActiveCell(state) 199 | 200 | return { 201 | color: track ? track.color : Color.GREY, 202 | activeNote: cell ? cell.midi : 0, 203 | activeTrackID: state.session.activeTrackID, 204 | activeCellBeat: state.session.activeCellBeat, 205 | mapping: (note: number) => 206 | getInstrumentMapping(state, state.session.activeTrackID, note), 207 | sample: (note: number) => 208 | getSample(state, state.session.activeTrackID, note) 209 | } 210 | } 211 | 212 | const NoteSelectorWithConnect = connect( 213 | mapStateToProps, 214 | { changeCellNote, listenCellNote } 215 | )(NoteSelectorMemoized) 216 | 217 | export default NoteSelectorWithConnect 218 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/TrackPanel.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | 4 | import { 5 | withContainer, 6 | withPrefsProvider, 7 | withReduxProvider, 8 | stateFixture 9 | } from "../../../../../.storybook/decorators" 10 | 11 | import TrackPanel from "./TrackPanel" 12 | 13 | const state: any = { 14 | ...stateFixture, 15 | session: { 16 | ...stateFixture.session, 17 | activeTrackID: "2", 18 | activeCellBeat: 6 19 | } 20 | } 21 | 22 | storiesOf("TrackPanel", module) 23 | .addParameters({ 24 | info: { 25 | inline: true, 26 | header: false 27 | } 28 | }) 29 | .addDecorator(withReduxProvider(state)) 30 | .addDecorator(withPrefsProvider) 31 | .addDecorator(withContainer) 32 | .add("default", () => ) 33 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/TrackPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import CellSettings from "./CellSettings/CellSettings" 6 | import { usePrefs } from "../../../context/sequencer-prefs" 7 | import TrackSettings from "./TrackSettings/TrackSettings" 8 | 9 | const StyledTrackPanel = styled.div<{ gutter: number }>` 10 | display: flex; 11 | margin-bottom: ${({ gutter }) => gutter}px; 12 | ` 13 | 14 | const Gutter = styled.div<{ gutter: number }>` 15 | margin-left: ${({ gutter }) => gutter}px; 16 | ` 17 | 18 | const TrackPanelMemo = React.memo(function TrackPanel() { 19 | const { gutter } = usePrefs() 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | }) 29 | 30 | export default TrackPanelMemo 31 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/TrackSettings/Fader/Fader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { connect } from "react-redux" 3 | 4 | import { getActiveTrack } from "../../../../../../redux/reducers" 5 | import { changeTrackGain } from "../../../../../../redux/actions/session/creators" 6 | import VerticalFader from "../../../../../controllers/VerticalFader/VerticalFader" 7 | import { MaterialColor } from "../../../../../../utils/color/colorLibrary" 8 | import { IAppState } from "../../../../../../redux/store/configureStore" 9 | 10 | interface IOwnProps { 11 | height: number 12 | width: number 13 | color: MaterialColor 14 | fontSize: number 15 | } 16 | 17 | interface IProps extends IOwnProps { 18 | value: number 19 | activeTrackID: string | null 20 | changeTrackGain: (trackId: string, gain: number) => void 21 | } 22 | 23 | export function Fader(props: IProps) { 24 | return ( 25 | { 32 | // tslint:disable-next-line:no-unused-expression 33 | if (!props.activeTrackID) { 34 | return 35 | } 36 | 37 | changeTrackGain(props.activeTrackID, value) 38 | }} 39 | /> 40 | ) 41 | } 42 | 43 | const mapStateToProps = (state: IAppState) => { 44 | const track = getActiveTrack(state) 45 | 46 | return { 47 | value: track ? track.processing.gain.gain : 1, 48 | activeTrackID: state.session.activeTrackID 49 | } 50 | } 51 | 52 | const VerticalFaderWithConnect = connect( 53 | mapStateToProps, 54 | { changeTrackGain } 55 | )(Fader) 56 | 57 | export default VerticalFaderWithConnect 58 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/TrackSettings/ResolutionSwitch/ResolutionSwitch.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | 5 | import { ResolutionSwitch } from "./ResolutionSwitch" 6 | import { 7 | withContainer, 8 | withPrefsProvider 9 | } from "../../../../../../../.storybook/decorators" 10 | import { withKnobs, select } from "@storybook/addon-knobs" 11 | import Color, { trackColors } from "../../../../../../utils/color/colorLibrary" 12 | import { NoteResolution } from "../../../../../../redux/store/session/interfaces" 13 | 14 | storiesOf("ResolutionSwitch", module) 15 | .addParameters({ 16 | info: { 17 | inline: true, 18 | header: false 19 | } 20 | }) 21 | .addDecorator(withKnobs) 22 | .addDecorator(withPrefsProvider) 23 | .addDecorator(withContainer) 24 | .add("sixteenth note - use knobs", () => { 25 | const color = select("Color", trackColors, Color.PINK) 26 | const noteResolution = select( 27 | "Note resolution", 28 | { 29 | "sixteenth note": 1 as NoteResolution, 30 | "eighth note": 2 as NoteResolution, 31 | "quarter note": 4 as NoteResolution 32 | }, 33 | 1 34 | ) 35 | 36 | return ( 37 | 43 | ) 44 | }) 45 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/TrackSettings/ResolutionSwitch/ResolutionSwitch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | import { connect } from "react-redux" 5 | 6 | import Color, { 7 | MaterialColor 8 | } from "../../../../../../utils/color/colorLibrary" 9 | import { getActiveTrack } from "../../../../../../redux/reducers" 10 | import { changeNoteResolution } from "../../../../../../redux/actions/session/creators" 11 | import { usePrefs } from "../../../../../context/sequencer-prefs" 12 | import { NoteResolution } from "../../../../../../redux/store/session/interfaces" 13 | import { IAppState } from "../../../../../../redux/store/configureStore" 14 | 15 | interface IProps { 16 | color: MaterialColor 17 | noteResolution: NoteResolution 18 | activeTrackID: string | null 19 | changeNoteResolution: ( 20 | noteResolution: NoteResolution, 21 | trackID: string 22 | ) => void 23 | } 24 | 25 | const StyledSwitch = styled.div<{ 26 | width: number 27 | height: number 28 | gutter: number 29 | color: MaterialColor 30 | }>` 31 | display: flex; 32 | align-items: stretch; 33 | border-radius: 3px; 34 | user-select: none; 35 | z-index: 999; 36 | width: ${({ width }) => width}px; 37 | height: ${({ height }) => height}px; 38 | padding: ${({ gutter }) => gutter}px; 39 | background-color: ${({ color }) => Color.get700(color)}; 40 | ` 41 | 42 | const ResolutionButton = styled.button<{ 43 | noteResolution: NoteResolution 44 | color: MaterialColor 45 | buttonResolution: number 46 | }>` 47 | flex: 1 1 auto; 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | cursor: ${({ noteResolution, buttonResolution }) => 52 | noteResolution === buttonResolution ? "default" : "pointer"}; 53 | padding: 0; 54 | border-radius: 3px; 55 | border: none; 56 | font-size: 12px; 57 | background-color: ${({ color, noteResolution, buttonResolution }) => 58 | noteResolution === buttonResolution 59 | ? Color.get50(color) 60 | : Color.get400(color)}; 61 | 62 | &:hover { 63 | background-color: ${({ color, noteResolution, buttonResolution }) => 64 | noteResolution === buttonResolution 65 | ? Color.get50(color) 66 | : Color.get200(color)}; 67 | } 68 | ` 69 | 70 | const Gutter = styled.div<{ gutter: number }>` 71 | margin-left: ${({ gutter }) => gutter}px; 72 | ` 73 | 74 | const prefs = { height: 36, width: 130 } 75 | 76 | const button1Ref = React.createRef() 77 | const button2Ref = React.createRef() 78 | const button4Ref = React.createRef() 79 | 80 | export function ResolutionSwitch(props: IProps) { 81 | if (props.activeTrackID === null) { 82 | return
83 | } 84 | 85 | const { gutter } = usePrefs() 86 | 87 | return ( 88 | 94 | { 105 | if (props.activeTrackID && props.noteResolution !== 1) { 106 | props.changeNoteResolution(1, props.activeTrackID) 107 | // tslint:disable-next-line:no-unused-expression 108 | button1Ref.current && button1Ref.current.blur() 109 | } 110 | }} 111 | > 112 | ♬ 113 | 114 | 115 | { 124 | if (props.activeTrackID && props.noteResolution !== 2) { 125 | props.changeNoteResolution(2, props.activeTrackID) 126 | // tslint:disable-next-line:no-unused-expression 127 | button2Ref.current && button2Ref.current.blur() 128 | } 129 | }} 130 | > 131 | ♫ 132 | 133 | 134 | { 145 | if (props.activeTrackID && props.noteResolution !== 4) { 146 | props.changeNoteResolution(4, props.activeTrackID) 147 | // tslint:disable-next-line:no-unused-expression 148 | button4Ref.current && button4Ref.current.blur() 149 | } 150 | }} 151 | > 152 | ♩ 153 | 154 | 155 | ) 156 | } 157 | 158 | const ResolutionSwitchMemoized = React.memo(ResolutionSwitch) 159 | 160 | const mapStateToProps = (state: IAppState) => { 161 | const track = getActiveTrack(state) 162 | 163 | return { 164 | color: track ? track.color : "grey", 165 | noteResolution: track ? track.noteResolution : 1, 166 | activeTrackID: state.session.activeTrackID 167 | } 168 | } 169 | 170 | const ResolutionSwitchWithConnect = connect( 171 | mapStateToProps, 172 | { changeNoteResolution } 173 | )(ResolutionSwitchMemoized) 174 | 175 | export default ResolutionSwitchWithConnect 176 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/TrackSettings/TrackSettings.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | 4 | import { 5 | withContainer, 6 | withPrefsProvider, 7 | withReduxProvider, 8 | stateFixture 9 | } from "../../../../../../.storybook/decorators" 10 | import { TrackSettings } from "./TrackSettings" 11 | import { withKnobs, boolean } from "@storybook/addon-knobs" 12 | 13 | storiesOf("TrackSettings", module) 14 | .addParameters({ 15 | info: { 16 | inline: true, 17 | header: false 18 | } 19 | }) 20 | .addDecorator(withKnobs) 21 | .addDecorator(withReduxProvider(stateFixture)) 22 | .addDecorator(withPrefsProvider) 23 | .addDecorator(withContainer) 24 | .add("active track - use knobs", () => { 25 | const active = boolean("Track active", true) 26 | 27 | return 28 | }) 29 | -------------------------------------------------------------------------------- /src/components/Sequencer/Track/TrackPanel/TrackSettings/TrackSettings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | import { connect } from "react-redux" 5 | 6 | import Color, { MaterialColor } from "../../../../../utils/color/colorLibrary" 7 | import ResolutionSwitch from "./ResolutionSwitch/ResolutionSwitch" 8 | import VerticalFaderWithConnect from "./Fader/Fader" 9 | import { usePrefs } from "../../../../context/sequencer-prefs" 10 | import { getActiveTrack } from "../../../../../redux/reducers" 11 | import { IAppState } from "../../../../../redux/store/configureStore" 12 | 13 | interface IProps { 14 | color: MaterialColor 15 | isTrackActive: boolean 16 | } 17 | 18 | const StyledSettings = styled.div<{ 19 | width: number 20 | height: number 21 | color: MaterialColor 22 | }>` 23 | border-radius: 3px; 24 | width: ${({ width }) => width}px; 25 | height: ${({ height }) => height}px; 26 | background-color: ${({ color }) => Color.get800Dark(color)}; 27 | ` 28 | 29 | const InnerWrapper = styled.div` 30 | display: flex; 31 | height: 100%; 32 | ` 33 | 34 | const AsideSection = styled.section<{ gutter: number }>` 35 | padding: ${({ gutter }) => 2 * gutter}px; 36 | ` 37 | 38 | const MainSection = styled.section` 39 | width: 100%; 40 | ` 41 | 42 | const ResolutionSwitchWrapper = styled.div<{ gutter: number }>` 43 | display: flex; 44 | justify-content: flex-end; 45 | padding: ${({ gutter }) => 2 * gutter}px; 46 | ` 47 | 48 | export function TrackSettings({ color, isTrackActive }: IProps) { 49 | if (!isTrackActive) { 50 | return
51 | } 52 | 53 | const { panelWidth, panelHeight, gutter } = usePrefs() 54 | 55 | return ( 56 | 57 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | 72 | 73 | 74 | ) 75 | } 76 | 77 | const mapStateToProps = (state: IAppState) => { 78 | const track = getActiveTrack(state) 79 | 80 | return { 81 | color: track ? track.color : "grey", 82 | isTrackActive: !!track 83 | } 84 | } 85 | 86 | const TrackSettingsWithConnect = connect(mapStateToProps)(TrackSettings) 87 | 88 | export default TrackSettingsWithConnect 89 | -------------------------------------------------------------------------------- /src/components/context/sequencer-prefs.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | 4 | interface IProps { 5 | children: React.ReactNode 6 | } 7 | 8 | interface IPrefs { 9 | cellSize: number, 10 | gutter: number, 11 | panelWidth: number, 12 | panelHeight: number, 13 | transitionDuration: number 14 | } 15 | 16 | const SequencerPrefs = React.createContext({ 17 | cellSize: 0, 18 | gutter: 0, 19 | panelWidth: 0, 20 | panelHeight: 0, 21 | transitionDuration: 0 22 | }) 23 | 24 | const prefs = { 25 | cellSize: 36, 26 | gutter: 6, 27 | panelWidth: 280, 28 | panelHeight: 300, 29 | transitionDuration: 300 30 | } 31 | 32 | function PrefsProvider({ children }: IProps) { 33 | return ({children}) 34 | } 35 | 36 | function usePrefs(): IPrefs { 37 | const preferences = React.useContext(SequencerPrefs) 38 | if (!preferences) { 39 | throw new Error("usePrefs must be used within a PrefsProvider") 40 | } 41 | 42 | return prefs 43 | } 44 | 45 | export { PrefsProvider, SequencerPrefs, usePrefs } 46 | -------------------------------------------------------------------------------- /src/components/controllers/Fader/Fader.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | 5 | import Fader from "./Fader" 6 | import { withKnobs, select, number } from "@storybook/addon-knobs" 7 | import { withContainer } from "../../../../.storybook/decorators" 8 | import Color, { trackColors } from "../../../utils/color/colorLibrary" 9 | 10 | storiesOf("Fader", module) 11 | .addParameters({ 12 | info: { 13 | inline: true, 14 | header: false 15 | } 16 | }) 17 | .addDecorator(withKnobs) 18 | .addDecorator(withContainer) 19 | .add("default - use knobs", () => { 20 | const color = select("Color", trackColors, Color.INDIGO) 21 | const value = number("Value [0,100]", 20) 22 | 23 | return 24 | }) 25 | .add("vertical", () => { 26 | const color = select("Color", trackColors, Color.PURPLE) 27 | const value = number("Value [0,100]", 20) 28 | 29 | return ( 30 |
31 | 38 |
39 | ) 40 | }) 41 | .add("horizontal", () => { 42 | const color = select("Color", trackColors, Color.GREEN) 43 | const value = number("Value [0,100]", 20) 44 | 45 | return ( 46 | 53 | ) 54 | }) 55 | -------------------------------------------------------------------------------- /src/components/controllers/Fader/Fader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import styles from "./Fader.module.css" 4 | import { MaterialColor } from "../../../utils/color/colorLibrary" 5 | 6 | interface IProps { 7 | orientation: "vertical" | "horizontal" 8 | min: number 9 | max: number 10 | step: number 11 | color: MaterialColor 12 | size: number 13 | value: number 14 | onChange: (e: React.ChangeEvent) => void 15 | } 16 | 17 | Fader.defaultProps = { 18 | size: 168, 19 | orientation: "horizontal", 20 | min: 0, 21 | max: 100, 22 | step: 1 23 | } 24 | 25 | function Fader({ 26 | orientation, 27 | min, 28 | max, 29 | step, 30 | color, 31 | size, 32 | value, 33 | onChange 34 | }: IProps) { 35 | // thumb offset (px) 36 | const thumbOffset = 6 37 | const cssStyles: React.CSSProperties = { 38 | width: size < 168 ? "168px" : size + "px", 39 | marginLeft: size < 168 ? "-166px" : thumbOffset - size + "px" 40 | } 41 | 42 | const InputVerticalClass = "InputVertical_" + color 43 | const InputClass = "Input_" + color 44 | 45 | return ( 46 |
52 | 71 |
72 | ) 73 | } 74 | 75 | export default Fader 76 | -------------------------------------------------------------------------------- /src/components/controllers/Knob/Knob.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | 5 | import Knob from "./Knob" 6 | import { withKnobs, number, select } from "@storybook/addon-knobs" 7 | import Color, { trackColors } from "../../../utils/color/colorLibrary" 8 | 9 | storiesOf("Knob", module) 10 | .addParameters({ 11 | info: { 12 | inline: true, 13 | header: false 14 | } 15 | }) 16 | .addDecorator(withKnobs) 17 | .addDecorator(story =>
{story()}
) 18 | .add("default - use knobs", () => { 19 | const value = number("Value [0,100]", 25) 20 | const color = select("Color", trackColors, Color.PINK) 21 | const size = number("Size", 80) 22 | 23 | return ( 24 | 32 | ) 33 | }) 34 | -------------------------------------------------------------------------------- /src/components/controllers/Knob/Knob.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import { coordinates } from "../../../utils/trigo/polar" 6 | import Color, { MaterialColor } from "../../../utils/color/colorLibrary" 7 | 8 | interface IProps { 9 | value: number 10 | onChange: (value: number) => void 11 | min: number 12 | max: number 13 | step: number 14 | prefs: { color: MaterialColor; size: number } 15 | } 16 | 17 | const StyledKnob = styled.div<{ width: number; height: number }>` 18 | position: relative; 19 | width: ${({ width }) => width}px; 20 | height: ${({ height }) => height}px; 21 | ` 22 | 23 | const StyledSvg = styled.svg` 24 | pointer-events: none; 25 | width: 100%; 26 | height: 100%; 27 | ` 28 | 29 | const StyledInput = styled.input` 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | cursor: grab; 34 | width: 100%; 35 | height: 100%; 36 | transform: rotate(-90deg); 37 | opacity: 0; 38 | ` 39 | 40 | function Knob({ value, min, max, step, prefs, onChange }: IProps) { 41 | const angle = (360 / (1.0 * (max - min))) * (value * 1.0 - min) 42 | 43 | const { x, y } = coordinates(35, 35, 35)(angle) 44 | return ( 45 | 46 | 51 | 52 | 56 | 62 | 63 | 64 | 65 | 66 | ) => { 73 | onChange(parseFloat(e.currentTarget.value)) 74 | }} 75 | /> 76 | 77 | ) 78 | } 79 | 80 | export default Knob 81 | -------------------------------------------------------------------------------- /src/components/controllers/ValueController/ValueController.module.css: -------------------------------------------------------------------------------- 1 | .Display { 2 | user-select: none; 3 | } 4 | 5 | .Button { 6 | cursor: pointer; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/controllers/ValueController/ValueController.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | import ValueController from "./ValueController" 5 | import { withKnobs } from "@storybook/addon-knobs" 6 | 7 | storiesOf("ValueController", module) 8 | .addParameters({ 9 | info: { 10 | inline: true, 11 | header: false 12 | } 13 | }) 14 | .addDecorator(withKnobs) 15 | .addDecorator(story =>
{story()}
) 16 | .add("default", () => ( 17 | 25 | )) 26 | -------------------------------------------------------------------------------- /src/components/controllers/ValueController/ValueController.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | // import * as styles from "./ValueController.module.css" 6 | import Color, { MaterialColor } from "../../../utils/color/colorLibrary" 7 | 8 | interface IProps { 9 | value: number 10 | onChange: (value: number) => void 11 | amount: number 12 | min: number 13 | max: number 14 | prefs: { color: MaterialColor } 15 | } 16 | 17 | const StyledDisplay = styled.text` 18 | user-select: none; 19 | ` 20 | 21 | const StyledButtonSVGPath = styled.path` 22 | cursor: pointer; 23 | ` 24 | 25 | function ValueController({ value, onChange, amount, min, max }: IProps) { 26 | const increment = () => { 27 | const nextValue = value + amount > max ? max : value + amount 28 | 29 | return onChange(nextValue) 30 | } 31 | 32 | const decrement = () => { 33 | const nextValue = value - amount < min ? min : value - amount 34 | 35 | return onChange(nextValue) 36 | } 37 | 38 | return ( 39 |
40 | 47 | 48 | 56 | {`${value} BPM`} 57 | 58 | {/**/} 67 | {/* {`${value} BPM`}*/} 68 | {/**/} 69 | 72 | {/**/} 78 | 81 | {/**/} 87 | 88 |
89 | ) 90 | } 91 | 92 | export default ValueController 93 | -------------------------------------------------------------------------------- /src/components/controllers/VerticalFader/VerticalFader.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { storiesOf } from "@storybook/react" 3 | import { action } from "@storybook/addon-actions" 4 | 5 | import VerticalFader from "./VerticalFader" 6 | import Color, { trackColors } from "../../../utils/color/colorLibrary" 7 | import { number, select, withKnobs } from "@storybook/addon-knobs" 8 | 9 | storiesOf("VerticalFader", module) 10 | .addParameters({ 11 | info: { 12 | inline: true, 13 | header: false 14 | } 15 | }) 16 | .addDecorator(withKnobs) 17 | .add("w=70, h=300", () => { 18 | const color = select("Color", trackColors, Color.ORANGE) 19 | const value = number("Value [0,1]", 0.707) 20 | const height = number("Height", 300) 21 | const width = number('Width', 70) 22 | 23 | return ( 24 |
32 | 40 |
41 | ) 42 | }) 43 | .add("w=48, h=400", () => { 44 | const color = select("Color", trackColors, Color.PINK) 45 | const value = number("Value [0,1]", 0.707) 46 | const height = number("Height", 400) 47 | const width = number('Width', 48) 48 | 49 | return ( 50 |
58 | 66 |
67 | ) 68 | }) 69 | -------------------------------------------------------------------------------- /src/components/controllers/VerticalFader/VerticalFader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import Volume from "../../../audio/utils/Volume/Volume" 6 | import Color, { MaterialColor } from "../../../utils/color/colorLibrary" 7 | 8 | interface IProps { 9 | readonly width: number 10 | readonly height: number 11 | readonly color: MaterialColor 12 | readonly fontSize: number 13 | readonly value: number 14 | readonly onValueChange: (value: number) => void 15 | } 16 | 17 | const Wrapper = styled.div<{ 18 | readonly width: number 19 | readonly height: number 20 | readonly color: MaterialColor 21 | }>` 22 | display: flex; 23 | flex-direction: column; 24 | width: ${({ width }) => width}px; 25 | height: ${({ height }) => height}px; 26 | border-radius: 3px; 27 | background: ${({ color }) => Color.get900(color)}; 28 | ` 29 | 30 | const Range = styled.div<{ gutter: number }>` 31 | flex: 1 0 auto; 32 | display: flex; 33 | justify-content: center; 34 | margin: ${({ gutter }) => `${gutter}px ${gutter}px 0 ${gutter}px`}; 35 | ` 36 | 37 | const InputWrapper = styled.div` 38 | display: flex; 39 | transform: rotate(-90deg); 40 | ` 41 | 42 | const Input = styled.input<{ 43 | readonly height: number 44 | readonly gutter: number 45 | readonly fontSize: number 46 | readonly width: number 47 | readonly trackWidth: number 48 | readonly color: MaterialColor 49 | readonly thumbHeight: number 50 | }>` 51 | width: ${({ height, gutter, fontSize }) => 52 | height - gutter * 3 - fontSize}px; 53 | height: ${({ width }) => width}px; 54 | margin: ${({ height, gutter, fontSize, width }) => 55 | (height - gutter * 3 - fontSize - width) / 2.0}px 56 | 0 0; 57 | background: transparent; 58 | border: none transparent; 59 | 60 | &, 61 | &::-webkit-slider-runnable-track, 62 | &::-webkit-slider-thumb { 63 | -webkit-appearance: none; 64 | } 65 | 66 | &::-webkit-slider-runnable-track { 67 | height: ${({ trackWidth }) => trackWidth}px; 68 | background: ${({ color }) => Color.get900Dark(color)}; 69 | border: none; 70 | border-radius: 3px; 71 | } 72 | 73 | &::-moz-range-track { 74 | height: ${({ trackWidth }) => trackWidth}px; 75 | background: ${({ color }) => Color.get900Dark(color)}; 76 | border: none; 77 | border-radius: 3px; 78 | } 79 | 80 | &::-ms-track { 81 | height: ${({ trackWidth }) => trackWidth}px; 82 | background: ${({ color }) => Color.get900Dark(color)}; 83 | border: none; 84 | border-radius: 3px; 85 | color: transparent; 86 | } 87 | 88 | &::-ms-fill-lower { 89 | display: none; 90 | } 91 | 92 | &::-webkit-slider-thumb { 93 | cursor: grab; 94 | width: ${({ thumbHeight }) => thumbHeight}px; 95 | height: ${({ width, gutter }) => width - 2 * gutter}px; 96 | margin-top: ${({ width, trackWidth, gutter }) => 97 | -(width - 2 * gutter - trackWidth) / 2.0}px; 98 | border: none; 99 | border-radius: 3px; 100 | background: ${({ color }) => Color.getA700(color)}; 101 | } 102 | 103 | &::-moz-range-thumb { 104 | cursor: grab; 105 | width: ${({ thumbHeight }) => thumbHeight}px; 106 | height: ${({ width, gutter }) => width - 2 * gutter}px; 107 | border: none; 108 | border-radius: 3px; 109 | background: ${({ color }) => Color.getA700(color)}; 110 | } 111 | 112 | &::-ms-thumb { 113 | cursor: grab; 114 | width: ${({ thumbHeight }) => thumbHeight}px; 115 | height: ${({ width, gutter }) => width - 2 * gutter}px; 116 | border: none; 117 | border-radius: 3px; 118 | background: ${({ color }) => Color.getA700(color)}; 119 | } 120 | 121 | &::-moz-focus-outer { 122 | border: 0; 123 | } 124 | 125 | &:focus { 126 | outline: none; 127 | 128 | &::-webkit-slider-runnable-track { 129 | background: #212121; 130 | border: none; 131 | } 132 | &::-moz-range-track { 133 | background: #212121; 134 | border: none; 135 | } 136 | &::-ms-track { 137 | background: #212121; 138 | border: none; 139 | } 140 | } 141 | 142 | &:hover { 143 | &::-webkit-slider-thumb { 144 | background: ${({ color }: { color: MaterialColor }) => 145 | Color.getA400(color)}; 146 | } 147 | &::-moz-range-thumb { 148 | background: ${({ color }: { color: MaterialColor }) => 149 | Color.getA400(color)}; 150 | } 151 | &::-ms-thumb { 152 | background: ${({ color }: { color: MaterialColor }) => 153 | Color.getA400(color)}; 154 | } 155 | } 156 | ` 157 | 158 | const GainIndicator = styled.div<{ 159 | readonly gutter: number 160 | readonly color: MaterialColor 161 | readonly fontSize: number 162 | }>` 163 | padding: ${({ gutter }) => gutter}px 0; 164 | color: ${({ color }) => Color.get100(color)}; 165 | font-size: ${({ fontSize }) => fontSize}px; 166 | line-height: 1; 167 | text-align: center; 168 | ` 169 | 170 | const MemoizedVerticalFader = React.memo(function VerticalFader( 171 | props: IProps 172 | ) { 173 | const handleChange = (e: React.ChangeEvent) => { 174 | const value = parseFloat(e.currentTarget.value) 175 | 176 | props.onValueChange(value) 177 | } 178 | 179 | const gutter = props.width * 0.2 180 | const thumbHeight = props.width * 0.5 181 | const trackWidth = props.width * 0.25 182 | 183 | return ( 184 | 185 | 186 | 187 | 203 | 204 | 205 | 210 | {Volume.toDBString(props.value)} 211 | 212 | 213 | ) 214 | }) 215 | 216 | export default MemoizedVerticalFader 217 | -------------------------------------------------------------------------------- /src/components/pages/HomePage/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | const StyledContainer = styled.div` 6 | padding-top: 3rem; 7 | ` 8 | 9 | function HomePage() { 10 | return HOME 11 | } 12 | 13 | export default HomePage 14 | -------------------------------------------------------------------------------- /src/components/pages/SessionPage/SessionPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | // tslint:disable-next-line:no-submodule-imports 3 | import styled from "styled-components/macro" 4 | 5 | import MasterPanel from "../../MasterPanel/MasterPanel" 6 | import Sequencer from "../../Sequencer/Sequencer" 7 | import { PrefsProvider } from "../../context/sequencer-prefs" 8 | import AudioEngine from "../../AudioEngine/AudioEngine" 9 | 10 | const StyledContainer = styled.div` 11 | padding-top: 3rem; 12 | ` 13 | 14 | const StyledSection = styled.div` 15 | margin: 1rem; 16 | ` 17 | 18 | function SessionPage() { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default SessionPage 33 | -------------------------------------------------------------------------------- /src/graphql/types/color.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | A set of named colors as described in Material Design 3 | https://material.io/tools/color 4 | """ 5 | enum MaterialColor { 6 | red 7 | pink 8 | purple 9 | deepPurple 10 | indigo 11 | blue 12 | lightBlue 13 | cyan 14 | teal 15 | green 16 | lightGreen 17 | lime 18 | yellow 19 | amber 20 | orange 21 | deepOrange 22 | brown 23 | grey 24 | blueGrey 25 | } 26 | -------------------------------------------------------------------------------- /src/graphql/types/instrument.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | instrumentList: [Instrument!]! 3 | } 4 | 5 | extend type Mutation { 6 | createInstrument(input: InstrumentCreateInput!): InstrumentMutationResponse! 7 | deleteInstrument(id: ID!): InstrumentMutationResponse! 8 | } 9 | 10 | """ 11 | IInstrument used to build a sequencer track 12 | """ 13 | type Instrument { 14 | "primary key (UUIDv4)" 15 | id: ID! 16 | 17 | label: String! 18 | 19 | group: String! 20 | 21 | "IDs of the samples used in mappings" 22 | samples: [Sample!]! 23 | 24 | "MIDI mapping — set of maximum 128 entries" 25 | mapping: [InstrumentMapping!]! 26 | 27 | "Creation date in ISO 8601 Extended Format" 28 | createdAt: DateTime! 29 | 30 | "Update date in ISO 8601 Exteded Format" 31 | updatedAt: DateTime! 32 | } 33 | 34 | """ 35 | Mapping entry for the instrument 36 | """ 37 | type InstrumentMapping { 38 | "Corresponding MIDI note [0, 127]" 39 | note: Int! 40 | 41 | "The associated sample" 42 | sample: Sample! 43 | 44 | "Detuning of the pitch in cents" 45 | detune: Int! 46 | } 47 | 48 | input InstrumentCreateInput { 49 | label: String! 50 | 51 | group: String 52 | 53 | mapping: [InstrumentMappingCreateInput!]! 54 | } 55 | 56 | input InstrumentMappingCreateInput { 57 | note: Int! 58 | 59 | sampleID: String! 60 | 61 | detune: Int! 62 | } 63 | 64 | type InstrumentMutationResponse implements MutationResponse { 65 | code: String! 66 | success: Boolean! 67 | messageTemplate: String! 68 | message: String 69 | instrument: Instrument 70 | error: String 71 | } 72 | -------------------------------------------------------------------------------- /src/graphql/types/processing.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | Audio processing settings 3 | """ 4 | type AudioProcessing { 5 | gain: GainProcessing!, 6 | filter: FilterProcessing, 7 | delay: DelayProcessing, 8 | distorsion: DistorsionProcessing 9 | } 10 | 11 | """ 12 | Gain settings for audio processing 13 | 14 | https://webaudio.github.io/web-audio-api/#gainnode 15 | """ 16 | type GainProcessing { 17 | "Amount of gain" 18 | gain: Float! 19 | } 20 | 21 | """ 22 | Filter settings for audio processing 23 | 24 | https://webaudio.github.io/web-audio-api/#biquadfilternode 25 | """ 26 | type FilterProcessing { 27 | enabled: Boolean! 28 | 29 | "Filter type" 30 | type: FilterType! 31 | 32 | "Filter frequency in Hz" 33 | frequency: Float! 34 | 35 | "Detuning of the frequency in cents" 36 | detune: Int 37 | 38 | "Filter gain" 39 | gain: Float! 40 | 41 | "Filter quality factor" 42 | q: Float 43 | } 44 | 45 | """ 46 | Enumeration of filter type for the filter audio processing 47 | """ 48 | enum FilterType { 49 | lowpass 50 | highpass 51 | bandpass 52 | lowshelf 53 | highshelf 54 | peaking 55 | notch 56 | allpass 57 | } 58 | 59 | """ 60 | Delay settings for audio processing 61 | 62 | https://webaudio.github.io/web-audio-api/#DelayNode 63 | """ 64 | type DelayProcessing { 65 | enabled: Boolean! 66 | 67 | "Amount of delay in s" 68 | delayTime: Float! 69 | } 70 | 71 | """ 72 | Disorsion settings for audio processing 73 | 74 | https://webaudio.github.io/web-audio-api/#waveshapernode 75 | """ 76 | type DistorsionProcessing { 77 | enabled: Boolean! 78 | 79 | "Shaping curve" 80 | curve: [Float!]! 81 | 82 | "Type of oversampling" 83 | oversample: OversamplingType! 84 | } 85 | 86 | """ 87 | Enumeration of oversampling types for distorsion audio processing 88 | """ 89 | enum OversamplingType { 90 | none 91 | twoTimes 92 | fourTimes 93 | } 94 | -------------------------------------------------------------------------------- /src/graphql/types/root.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | # trick to declare an empty type 3 | _: Boolean 4 | } 5 | 6 | type Mutation { 7 | # trick to declare an empty type 8 | _: Boolean 9 | } 10 | 11 | type Subscription { 12 | # trick to declare an empty type 13 | _: Boolean 14 | } 15 | 16 | interface MutationResponse { 17 | code: String! 18 | success: Boolean! 19 | messageTemplate: String! 20 | message: String 21 | error: String 22 | } 23 | 24 | scalar DateTime 25 | 26 | # in Apollo server Upload we need to comment out Upload definition as it's already declared 27 | scalar Upload 28 | -------------------------------------------------------------------------------- /src/graphql/types/sample.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | sampleList: [Sample!]! 3 | } 4 | 5 | extend type Mutation { 6 | createSample(input: SampleCreateInput!): SampleMutationResponse! 7 | } 8 | 9 | extend type Mutation { 10 | updateSample(id: ID!, input: SampleUpdateInput!): SampleMutationResponse! 11 | } 12 | 13 | extend type Mutation { 14 | deleteSample(id: ID!): SampleMutationResponse! 15 | } 16 | 17 | type Sample { 18 | """ 19 | UUID 20 | """ 21 | id: ID! 22 | 23 | """ 24 | ISample file name 25 | """ 26 | filename: String! 27 | 28 | """ 29 | ISample url 30 | """ 31 | url: String! 32 | 33 | """ 34 | Mime type 35 | """ 36 | type: String! 37 | 38 | """ 39 | ISample label 40 | """ 41 | label: String! 42 | 43 | """ 44 | Name of the group which the sample belongs to 45 | """ 46 | group: String 47 | 48 | createdAt: DateTime! 49 | 50 | updatedAt: DateTime! 51 | } 52 | 53 | input SampleCreateInput { 54 | file: Upload! 55 | label: String 56 | group: String 57 | } 58 | 59 | input SampleUpdateInput { 60 | label: String 61 | group: String 62 | } 63 | 64 | type SampleMutationResponse implements MutationResponse { 65 | code: String! 66 | success: Boolean! 67 | messageTemplate: String! 68 | message: String 69 | sample: Sample 70 | error: String 71 | } 72 | -------------------------------------------------------------------------------- /src/graphql/types/session.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | session(id: ID!): Session 3 | } 4 | 5 | extend type Mutation { 6 | createSession(input: SessionCreateInput): SessionMutationResponse! 7 | updateSession(input: SessionUpdateInput): SessionMutationResponse! 8 | } 9 | 10 | type Session { 11 | "Primary key (UUIDv4)" 12 | id: ID! 13 | 14 | "ID of the user who creates the session" 15 | creatorID: ID! 16 | 17 | "Tempo in BPM [20, 200]" 18 | tempo: Int! 19 | 20 | "Master gain [0, 1]" 21 | masterGain: Int! 22 | 23 | "ID of the active track — its panel is visible" 24 | activeTrackID: ID 25 | 26 | "When a track is active, row index (beat) of the currently active cell in the panel" 27 | activeCellBeat: Int 28 | 29 | "ITrack IDs to determine the track order" 30 | trackOrder: [ID]! 31 | 32 | "Sequencer tracks" 33 | tracks: [Track!]! 34 | 35 | "IInstruments used by in tracks" 36 | instruments: [Instrument!]! 37 | 38 | "ISamples played in tracks" 39 | samples: [Sample!]! 40 | 41 | "Creation date in ISO 8601 Extended Format" 42 | createdAt: DateTime! 43 | 44 | "Update date in ISO 8601 Exteded Format" 45 | updatedAt: DateTime! 46 | } 47 | 48 | input SessionCreateInput { 49 | creatorID: ID! 50 | } 51 | 52 | input SessionUpdateInput { 53 | sessionID: ID! 54 | 55 | # instrument for new track 56 | instrumentID: ID 57 | } 58 | 59 | type SessionMutationResponse implements MutationResponse { 60 | code: String! 61 | success: Boolean! 62 | messageTemplate: String! 63 | message: String 64 | session: Session 65 | error: String 66 | } 67 | -------------------------------------------------------------------------------- /src/graphql/types/track.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | A track part of the session sequencer 3 | """ 4 | type Track { 5 | "Primary key (UUIDv4)" 6 | id: ID! 7 | 8 | "ITrack label" 9 | label: String! 10 | 11 | "ITrack color" 12 | color: MaterialColor! 13 | 14 | "ITrack note resolution — 1=16th note, 2=8th note, 4=quarter note" 15 | noteResolution: Int! 16 | 17 | "IInstrument used to build the track" 18 | instrument: Instrument! 19 | 20 | "ITrack mute enabled" 21 | muted: Boolean! 22 | 23 | "ITrack solo enabled" 24 | soloed: Boolean! 25 | 26 | "Row of cells (64) to be clock played — row index as beat number" 27 | cells: [Cell!]! 28 | 29 | "Audio processing settings" 30 | processing: AudioProcessing! 31 | } 32 | 33 | """ 34 | A note to be played 35 | """ 36 | type Cell { 37 | "Note scheduled or not" 38 | scheduled: Boolean! 39 | 40 | "MIDI note [0, 127]" 41 | midi: Int! 42 | 43 | "Audio processing settings" 44 | processing: AudioProcessing! 45 | } 46 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /*@import url('https://fonts.googleapis.com/css?family=Barlow:200,400,700,900');*/ 2 | @import url('https://fonts.googleapis.com/css?family=Lato:300,400,700,900'); 3 | 4 | body { 5 | position: relative; 6 | font-family: 'Lato', sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | background-color: #212121;/*#282c34;*/ 10 | } 11 | 12 | html { 13 | box-sizing: border-box; 14 | } 15 | *, *:before, *:after { 16 | box-sizing: inherit; 17 | } 18 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import ReactDOM from "react-dom" 3 | import * as serviceWorker from "./serviceWorker" 4 | 5 | import Root from "./components/Root/Root" 6 | 7 | import "normalize.css" 8 | import "./index.css" 9 | 10 | ReactDOM.render(, document.getElementById("root")) 11 | 12 | // If you want your app to work offline and load faster, you can change 13 | // unregister() to register() below. Note this comes with some pitfalls. 14 | // Learn more about service workers: https://bit.ly/CRA-PWA 15 | serviceWorker.unregister() 16 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/redux/actions/audio/creators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ANNOUNCE_BEAT, 3 | CLEAR_EVENT_QUEUE, 4 | TOGGLE_PLAY, 5 | SET_AUDIO_ENGINE_READY, 6 | RESET_TRANSPORT, 7 | LISTEN_CELL_NOTE, 8 | IAnnounceBeatAction, 9 | IClearEventQueueAction, 10 | IListenCellNoteAction, 11 | IResetTransportAction, 12 | ISetAudioEngineReady, 13 | ITogglePlayAction 14 | } from "./interfaces" 15 | 16 | export function resetTransport(): IResetTransportAction { 17 | return { type: RESET_TRANSPORT } 18 | } 19 | 20 | export function togglePlay(): ITogglePlayAction { 21 | return { type: TOGGLE_PLAY } 22 | } 23 | 24 | export function announceBeat(beat: number): IAnnounceBeatAction { 25 | return { 26 | type: ANNOUNCE_BEAT, 27 | payload: { beat } 28 | } 29 | } 30 | 31 | export function clearEventQueue(): IClearEventQueueAction { 32 | return { type: CLEAR_EVENT_QUEUE } 33 | } 34 | 35 | export function setAudioEngineReady(): ISetAudioEngineReady { 36 | return { 37 | type: SET_AUDIO_ENGINE_READY 38 | } 39 | } 40 | 41 | export function listenCellNote( 42 | note: number, 43 | beat: number, 44 | trackID: string 45 | ): IListenCellNoteAction { 46 | return { 47 | type: LISTEN_CELL_NOTE, 48 | payload: { note, beat, trackID } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/redux/actions/audio/interfaces.ts: -------------------------------------------------------------------------------- 1 | export const RESET_TRANSPORT = "RESET_TRANSPORT" 2 | export const TOGGLE_PLAY = "TOGGLE_PLAY" 3 | export const ANNOUNCE_BEAT = "ANNOUNCE_BEAT" 4 | export const CLEAR_EVENT_QUEUE = "CLEAR_EVENT_QUEUE" 5 | export const SET_AUDIO_ENGINE_READY = "SET_AUDIO_ENGINE_READY" 6 | export const LISTEN_CELL_NOTE = "LISTEN_CELL_NOTE" 7 | 8 | export interface IAnnounceBeatAction { 9 | type: "ANNOUNCE_BEAT" 10 | payload: { beat: number } 11 | } 12 | 13 | export interface IClearEventQueueAction { 14 | type: "CLEAR_EVENT_QUEUE" 15 | } 16 | 17 | export interface IResetTransportAction { 18 | type: "RESET_TRANSPORT" 19 | } 20 | 21 | export interface ITogglePlayAction { 22 | type: "TOGGLE_PLAY" 23 | } 24 | 25 | export interface ISetAudioEngineReady { 26 | type: "SET_AUDIO_ENGINE_READY" 27 | } 28 | 29 | export interface IListenCellNoteAction { 30 | type: "LISTEN_CELL_NOTE" 31 | payload: { 32 | note: number 33 | beat: number 34 | trackID: string 35 | } 36 | } 37 | 38 | export type Action = 39 | | IAnnounceBeatAction 40 | | IClearEventQueueAction 41 | | IResetTransportAction 42 | | ITogglePlayAction 43 | | ISetAudioEngineReady 44 | | IListenCellNoteAction 45 | -------------------------------------------------------------------------------- /src/redux/actions/session/creators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IChangeTrackGainAction, 3 | IChangeMasterGainAction, 4 | IChangeTempoAction, 5 | IScheduleTrackCellAction, 6 | IChangeNoteResolution, 7 | IToggleTrackMuteAction, 8 | IToggleTrackSoloAction, 9 | ISetActiveCellAction, 10 | IToggleActiveTrackAction, 11 | IChangeCellNoteAction, 12 | IAddTrackAction, 13 | IChangeTrackLabelAction, 14 | IChangeCellGainAction, 15 | IToggleTrackCellAction, 16 | CHANGE_TRACK_GAIN, 17 | CHANGE_MASTER_GAIN, 18 | CHANGE_TEMPO, 19 | SCHEDULE_TRACK_CELL, 20 | CHANGE_NOTE_RESOLUTION, 21 | TOGGLE_TRACK_MUTE, 22 | TOGGLE_TRACK_SOLO, 23 | SET_ACTIVE_CELL, 24 | TOGGLE_ACTIVE_TRACK, 25 | CHANGE_CELL_NOTE, 26 | ADD_TRACK, 27 | CHANGE_TRACK_LABEL, 28 | CHANGE_CELL_GAIN, 29 | TOGGLE_TRACK_CELL 30 | } from "./interfaces" 31 | import { NoteResolution } from "../../store/session/interfaces" 32 | import { IInstrument } from "../../store/instrument/interfaces" 33 | import { ISamples } from "../../store/sample/interfaces" 34 | 35 | export function changeTempo(tempo: number): IChangeTempoAction { 36 | return { 37 | type: CHANGE_TEMPO, 38 | payload: { tempo } 39 | } 40 | } 41 | 42 | export function changeMasterGain(gain: number): IChangeMasterGainAction { 43 | return { 44 | type: CHANGE_MASTER_GAIN, 45 | payload: { gain } 46 | } 47 | } 48 | 49 | export function changeTrackGain( 50 | trackID: string, 51 | gain: number 52 | ): IChangeTrackGainAction { 53 | return { 54 | type: CHANGE_TRACK_GAIN, 55 | payload: { trackID, gain } 56 | } 57 | } 58 | 59 | export function scheduleTrackCell( 60 | beat: number, 61 | trackID: string 62 | ): IScheduleTrackCellAction { 63 | return { 64 | type: SCHEDULE_TRACK_CELL, 65 | payload: { beat, trackID } 66 | } 67 | } 68 | 69 | export function toggleTrackCell( 70 | beat: number, 71 | trackID: string 72 | ): IToggleTrackCellAction { 73 | return { 74 | type: TOGGLE_TRACK_CELL, 75 | payload: { beat, trackID } 76 | } 77 | } 78 | 79 | export function changeNoteResolution( 80 | noteResolution: NoteResolution, 81 | trackID: string 82 | ): IChangeNoteResolution { 83 | return { 84 | type: CHANGE_NOTE_RESOLUTION, 85 | payload: { noteResolution, trackID } 86 | } 87 | } 88 | 89 | export function toggleMuteTrack(trackID: string): IToggleTrackMuteAction { 90 | return { 91 | type: TOGGLE_TRACK_MUTE, 92 | payload: { trackID } 93 | } 94 | } 95 | 96 | export function toggleSoloTrack(trackID: string): IToggleTrackSoloAction { 97 | return { 98 | type: TOGGLE_TRACK_SOLO, 99 | payload: { trackID } 100 | } 101 | } 102 | 103 | export function toggleActiveTrack(trackID: string): IToggleActiveTrackAction { 104 | return { 105 | type: TOGGLE_ACTIVE_TRACK, 106 | payload: { trackID } 107 | } 108 | } 109 | 110 | export function setActiveCell(beat: number): ISetActiveCellAction { 111 | return { 112 | type: SET_ACTIVE_CELL, 113 | payload: { beat } 114 | } 115 | } 116 | 117 | export function changeCellNote( 118 | note: number, 119 | beat: number, 120 | trackID: string 121 | ): IChangeCellNoteAction { 122 | return { 123 | type: CHANGE_CELL_NOTE, 124 | payload: { note, beat, trackID } 125 | } 126 | } 127 | 128 | export function addTrack( 129 | trackID: string, 130 | instrument: IInstrument, 131 | samples: ISamples 132 | ): IAddTrackAction { 133 | return { 134 | type: ADD_TRACK, 135 | payload: { trackID, instrument, samples } 136 | } 137 | } 138 | 139 | export function changeTrackLabel( 140 | label: string, 141 | trackID: string 142 | ): IChangeTrackLabelAction { 143 | return { 144 | type: CHANGE_TRACK_LABEL, 145 | payload: { label, trackID } 146 | } 147 | } 148 | 149 | export function changeCellGain( 150 | gain: number, 151 | beat: number, 152 | trackID: string 153 | ): IChangeCellGainAction { 154 | return { 155 | type: CHANGE_CELL_GAIN, 156 | payload: { gain, beat, trackID } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/redux/actions/session/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { NoteResolution } from "../../store/session/interfaces" 2 | import { IInstrument } from "../../store/instrument/interfaces" 3 | import { ISamples } from "../../store/sample/interfaces" 4 | 5 | export const CHANGE_TEMPO = "CHANGE_TEMPO" 6 | export const CHANGE_MASTER_GAIN = "CHANGE_MASTER_GAIN" 7 | export const SCHEDULE_TRACK_CELL = "SCHEDULE_TRACK_CELL" 8 | export const TOGGLE_TRACK_CELL = "TOGGLE_TRACK_CELL" 9 | export const ADD_TRACK = "ADD_TRACK" 10 | export const CHANGE_TRACK_GAIN = "CHANGE_TRACK_GAIN" 11 | export const CHANGE_NOTE_RESOLUTION = "CHANGE_NOTE_RESOLUTION" 12 | export const TOGGLE_TRACK_MUTE = "TOGGLE_TRACK_MUTE" 13 | export const TOGGLE_TRACK_SOLO = "TOGGLE_TRACK_SOLO" 14 | export const CHANGE_TRACK_LABEL = "CHANGE_TRACK_LABEL" 15 | export const TOGGLE_ACTIVE_TRACK = "TOGGLE_ACTIVE_TRACK" 16 | export const SET_ACTIVE_CELL = "SET_ACTIVE_CELL" 17 | export const CHANGE_CELL_NOTE = "CHANGE_CELL_NOTE" 18 | export const CHANGE_CELL_GAIN = "CHANGE_CELL_GAIN" 19 | 20 | export interface IChangeMasterGainAction { 21 | type: "CHANGE_MASTER_GAIN" 22 | payload: { gain: number } 23 | } 24 | 25 | export interface IChangeTrackGainAction { 26 | type: "CHANGE_TRACK_GAIN" 27 | payload: { trackID: string; gain: number } 28 | } 29 | 30 | export interface IChangeTempoAction { 31 | type: "CHANGE_TEMPO" 32 | payload: { tempo: number } 33 | } 34 | 35 | export interface IScheduleTrackCellAction { 36 | type: "SCHEDULE_TRACK_CELL" 37 | payload: { trackID: string; beat: number } 38 | } 39 | 40 | export interface IToggleTrackCellAction { 41 | type: "TOGGLE_TRACK_CELL" 42 | payload: { trackID: string; beat: number } 43 | } 44 | 45 | export interface IAddTrackAction { 46 | type: "ADD_TRACK" 47 | payload: { trackID: string; instrument: IInstrument; samples: ISamples } 48 | } 49 | 50 | export interface IChangeNoteResolution { 51 | type: "CHANGE_NOTE_RESOLUTION" 52 | payload: { noteResolution: NoteResolution; trackID: string } 53 | } 54 | 55 | export interface IToggleTrackMuteAction { 56 | type: "TOGGLE_TRACK_MUTE" 57 | payload: { trackID: string } 58 | } 59 | 60 | export interface IToggleTrackSoloAction { 61 | type: "TOGGLE_TRACK_SOLO" 62 | payload: { trackID: string } 63 | } 64 | 65 | export interface IToggleActiveTrackAction { 66 | type: "TOGGLE_ACTIVE_TRACK" 67 | payload: { trackID: string } 68 | } 69 | 70 | export interface ISetActiveCellAction { 71 | type: "SET_ACTIVE_CELL" 72 | payload: { beat: number } 73 | } 74 | 75 | export interface IChangeCellNoteAction { 76 | type: "CHANGE_CELL_NOTE" 77 | payload: { note: number; beat: number; trackID: string } 78 | } 79 | 80 | export interface IChangeTrackLabelAction { 81 | type: "CHANGE_TRACK_LABEL" 82 | payload: { label: string; trackID: string } 83 | } 84 | 85 | export interface IChangeCellGainAction { 86 | type: "CHANGE_CELL_GAIN" 87 | payload: { gain: number; beat: number; trackID: string } 88 | } 89 | 90 | export type Action = 91 | | IChangeMasterGainAction 92 | | IChangeTrackGainAction 93 | | IChangeTempoAction 94 | | IScheduleTrackCellAction 95 | | IToggleTrackCellAction 96 | | IAddTrackAction 97 | | IChangeNoteResolution 98 | | IToggleTrackMuteAction 99 | | IToggleTrackSoloAction 100 | | IToggleActiveTrackAction 101 | | ISetActiveCellAction 102 | | IChangeCellNoteAction 103 | | IChangeTrackLabelAction 104 | | IChangeCellGainAction 105 | -------------------------------------------------------------------------------- /src/redux/middlewares/logger.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, MiddlewareAPI } from "redux" 2 | import { AnyAction } from "../reducers" 3 | import { IAppState } from "../store/configureStore" 4 | 5 | // tslint:disable:no-console 6 | const logger = (store: MiddlewareAPI, IAppState>) => ( 7 | next: Dispatch 8 | ) => { 9 | return (action: AnyAction) => { 10 | if (!console.group) { 11 | next(action) 12 | } 13 | 14 | console.groupCollapsed(action.type) 15 | console.debug("%c prev state", "color: grey", store.getState()) 16 | console.debug("%c action", "color: blue", action) 17 | const result = next(action) 18 | console.debug("%c next state", "color: green", store.getState()) 19 | console.groupEnd() 20 | 21 | return result 22 | } 23 | } 24 | 25 | export default logger 26 | -------------------------------------------------------------------------------- /src/redux/reducers/audio.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ANNOUNCE_BEAT, 3 | CLEAR_EVENT_QUEUE, 4 | LISTEN_CELL_NOTE, 5 | RESET_TRANSPORT, 6 | TOGGLE_PLAY, 7 | Action, 8 | IListenCellNoteAction 9 | } from "../actions/audio/interfaces" 10 | import { 11 | ADD_TRACK, 12 | CHANGE_CELL_NOTE, 13 | CHANGE_MASTER_GAIN, 14 | CHANGE_TRACK_GAIN, 15 | SCHEDULE_TRACK_CELL, 16 | TOGGLE_TRACK_CELL, 17 | IAddTrackAction, 18 | IChangeCellNoteAction, 19 | IChangeMasterGainAction, 20 | IChangeTrackGainAction, 21 | IScheduleTrackCellAction, 22 | IToggleTrackCellAction 23 | } from "../actions/session/interfaces" 24 | import { IAudioState } from "../store/audio/interfaces" 25 | 26 | type ReducerAction = 27 | | Action 28 | | IChangeMasterGainAction 29 | | IChangeTrackGainAction 30 | | IScheduleTrackCellAction 31 | | IToggleTrackCellAction 32 | | IListenCellNoteAction 33 | | IChangeCellNoteAction 34 | | IAddTrackAction 35 | 36 | const initialState: IAudioState = { 37 | ready: false, 38 | playing: false, 39 | mode: "PLAY", 40 | currentBeat: 0, 41 | events: [] 42 | } 43 | 44 | const reducer = ( 45 | state: IAudioState = initialState, 46 | action: ReducerAction 47 | ): IAudioState => { 48 | switch (action.type) { 49 | case RESET_TRANSPORT: 50 | return { 51 | ...state, 52 | playing: false 53 | } 54 | 55 | case TOGGLE_PLAY: 56 | return { 57 | ...state, 58 | playing: !state.playing, 59 | events: [...state.events, action] 60 | } 61 | 62 | case ANNOUNCE_BEAT: 63 | return { 64 | ...state, 65 | currentBeat: action.payload.beat 66 | } 67 | 68 | case CLEAR_EVENT_QUEUE: 69 | return { 70 | ...state, 71 | events: [] 72 | } 73 | 74 | case CHANGE_MASTER_GAIN: 75 | case CHANGE_TRACK_GAIN: 76 | case SCHEDULE_TRACK_CELL: 77 | case TOGGLE_TRACK_CELL: 78 | case CHANGE_CELL_NOTE: 79 | case LISTEN_CELL_NOTE: 80 | case ADD_TRACK: 81 | return { 82 | ...state, 83 | events: [...state.events, action] 84 | } 85 | 86 | default: 87 | return state 88 | } 89 | } 90 | 91 | export default reducer 92 | -------------------------------------------------------------------------------- /src/redux/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux" 2 | 3 | import audio from "./audio" 4 | import session, * as fromSession from "./session" 5 | import instruments, * as fromInstruments from "./instruments" 6 | import samples, * as fromSamples from "./samples" 7 | 8 | import { Action as AudioAction } from "../actions/audio/interfaces" 9 | import { Action as SessionAction } from "../actions/session/interfaces" 10 | import { IAudioState } from "../store/audio/interfaces" 11 | import { ISession } from "../store/session/interfaces" 12 | import { IInstrument, IInstruments } from "../store/instrument/interfaces" 13 | import { ISamples } from "../store/sample/interfaces" 14 | 15 | interface IAppState { 16 | audio: IAudioState 17 | session: ISession 18 | instruments: IInstruments 19 | samples: ISamples 20 | } 21 | 22 | export type AnyAction = AudioAction | SessionAction 23 | 24 | export const initialSate: IAppState = { 25 | audio: { 26 | ready: false, 27 | playing: false, 28 | mode: "PLAY", 29 | currentBeat: 0, 30 | events: [] 31 | }, 32 | session: { 33 | tempo: 120, 34 | masterGain: 1, 35 | activeTrackID: null, 36 | activeCellBeat: null, 37 | trackOrder: [], 38 | matrix: {}, 39 | tracks: {}, 40 | instruments: {}, 41 | samples: {} 42 | }, 43 | instruments: {}, 44 | samples: {} 45 | } 46 | 47 | const rootReducer = combineReducers<{}, AnyAction>({ 48 | audio, 49 | session, 50 | instruments, 51 | samples 52 | }) 53 | 54 | export default rootReducer 55 | 56 | export function getOrderedTracks(state: IAppState) { 57 | return fromSession.getOrderedTracks(state.session) 58 | } 59 | 60 | export function getTrack(state: IAppState, trackID: string) { 61 | return fromSession.getTrack(state.session, trackID) 62 | } 63 | 64 | export function getActiveTrack(state: IAppState) { 65 | return fromSession.getActiveTrack(state.session) 66 | } 67 | 68 | export function getCellRow(state: IAppState, trackID: string) { 69 | return fromSession.getCellRow(state.session, trackID) 70 | } 71 | 72 | export function getCell(state: IAppState, trackID: string, beat: number) { 73 | return fromSession.getCell(state.session, trackID, beat) 74 | } 75 | 76 | export function getActiveCell(state: IAppState) { 77 | return fromSession.getActiveCell(state.session) 78 | } 79 | 80 | export function getInstrument(state: IAppState, trackID: string): IInstrument { 81 | return fromSession.getInstrument(state.session, trackID) 82 | } 83 | 84 | export function getInstrumentMapping( 85 | state: IAppState, 86 | trackID: string | null, 87 | note: number 88 | ) { 89 | return fromSession.getInstrumentMapping(state.session, trackID, note) 90 | } 91 | 92 | export function getSample( 93 | state: IAppState, 94 | trackID: string | null, 95 | note: number 96 | ) { 97 | return fromSession.getSample(state.session, trackID, note) 98 | } 99 | 100 | export function getSolos(state: IAppState) { 101 | return fromSession.getSolos(state.session) 102 | } 103 | 104 | export function isSoloActive(state: IAppState) { 105 | return fromSession.isSoloActive(state.session) 106 | } 107 | 108 | export function getMutes(state: IAppState) { 109 | return fromSession.getMutes(state.session) 110 | } 111 | 112 | export function getInstrumentListIndexedByGroup(state: IAppState) { 113 | return fromInstruments.getInstrumentListIndexedByGroup(state.instruments) 114 | } 115 | 116 | export function getSamplesByIDs(state: IAppState, sampleIDs: string[]) { 117 | return fromSamples.getSamplesByIDs(state.samples, sampleIDs) 118 | } 119 | -------------------------------------------------------------------------------- /src/redux/reducers/instruments.ts: -------------------------------------------------------------------------------- 1 | import { IInstrument, IInstruments } from "../store/instrument/interfaces" 2 | 3 | const instrumentsReducer = (state: IInstruments = {}, action: any) => { 4 | switch (action.type) { 5 | default: 6 | return state 7 | } 8 | } 9 | 10 | export default instrumentsReducer 11 | 12 | export function getInstrumentListIndexedByGroup(state: IInstruments) { 13 | let list: { [group: string]: { [instrumentID: string]: IInstrument } } = {} 14 | 15 | Object.keys(state).forEach(instrumentID => { 16 | const instr = state[instrumentID] 17 | const group = instr.group 18 | 19 | list = { 20 | ...list, 21 | [group]: { 22 | ...list[group], 23 | [instrumentID]: instr 24 | } 25 | } 26 | }) 27 | 28 | return list 29 | } 30 | 31 | export function getSampleIDs( 32 | state: IInstruments, 33 | instrumentID: string 34 | ): string[] { 35 | return state[instrumentID].sampleIDs 36 | } 37 | -------------------------------------------------------------------------------- /src/redux/reducers/samples.ts: -------------------------------------------------------------------------------- 1 | import { ISamples } from "../store/sample/interfaces" 2 | 3 | const SamplesReducer = (state: ISamples = {}, action: any) => { 4 | switch (action.type) { 5 | default: 6 | return state 7 | } 8 | } 9 | 10 | export default SamplesReducer 11 | 12 | export function getSamplesByIDs(state: ISamples, IDs: string[]) { 13 | const samples: ISamples = {} 14 | 15 | IDs.forEach(ID => { 16 | samples[ID] = state[ID] 17 | }) 18 | 19 | return samples 20 | } 21 | -------------------------------------------------------------------------------- /src/redux/store/audio/initialState.ts: -------------------------------------------------------------------------------- 1 | import { IAudioState } from "./interfaces" 2 | 3 | const initialState: IAudioState = { 4 | ready: false, 5 | playing: false, 6 | mode: "PLAY", 7 | currentBeat: 0, 8 | // currentTrackPanel: null, 9 | // currentCellPanel: null, 10 | // mutes: { 11 | // "8ebdfbd8-4528-4e5e-932b-987c5405aec5": { enabled: false }, 12 | // "14f2dd71-77ad-4cf6-88f3-64680bf8f007": { enabled: false }, 13 | // "3eef107a-73c2-47d0-8c89-7cfe606dfcbd": { enabled: false }, 14 | // "7f6938d7-56e5-4d6c-90cd-431edad19a94": { enabled: false } 15 | // }, 16 | // solos: { 17 | // "8ebdfbd8-4528-4e5e-932b-987c5405aec5": { enabled: false }, 18 | // "14f2dd71-77ad-4cf6-88f3-64680bf8f007": { enabled: false }, 19 | // "3eef107a-73c2-47d0-8c89-7cfe606dfcbd": { enabled: false }, 20 | // "7f6938d7-56e5-4d6c-90cd-431edad19a94": { enabled: false } 21 | // }, 22 | events: [] 23 | } 24 | 25 | export default initialState 26 | -------------------------------------------------------------------------------- /src/redux/store/audio/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IListenCellNoteAction, 3 | ITogglePlayAction 4 | } from "../../actions/audio/interfaces" 5 | import { 6 | IAddTrackAction, 7 | IChangeCellNoteAction, 8 | IChangeMasterGainAction, 9 | IChangeTrackGainAction, 10 | IScheduleTrackCellAction, 11 | IToggleTrackCellAction 12 | } from "../../actions/session/interfaces" 13 | 14 | export interface IAudioState { 15 | ready: boolean 16 | playing: boolean 17 | mode: "EDIT" | "PLAY" 18 | currentBeat: number 19 | events: Event[] 20 | } 21 | 22 | export type Event = 23 | | ITogglePlayAction 24 | | IChangeMasterGainAction 25 | | IChangeTrackGainAction 26 | | IScheduleTrackCellAction 27 | | IToggleTrackCellAction 28 | | IListenCellNoteAction 29 | | IChangeCellNoteAction 30 | | IAddTrackAction 31 | -------------------------------------------------------------------------------- /src/redux/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, Middleware } from "redux" 2 | import { composeWithDevTools } from "redux-devtools-extension" 3 | 4 | import rootReducer from "../reducers" 5 | import loggerMiddleware from "../middlewares/logger" 6 | import { NOT_IN_PROD } from "../../utils/env" 7 | import { IAudioState } from "./audio/interfaces" 8 | import { ISession } from "./session/interfaces" 9 | import { IInstruments } from "./instrument/interfaces" 10 | import { ISamples } from "./sample/interfaces" 11 | 12 | export interface IAppState { 13 | audio: IAudioState 14 | session: ISession 15 | instruments: IInstruments 16 | samples: ISamples 17 | } 18 | 19 | const configureStore = (preloadState: IAppState) => { 20 | const middlewares: Middleware[] = [] 21 | 22 | if (NOT_IN_PROD) { 23 | middlewares.push(loggerMiddleware) 24 | } 25 | 26 | const middlewareEnhancer = applyMiddleware(...middlewares) 27 | 28 | const enhancers = [middlewareEnhancer] 29 | const composedEnhancers = composeWithDevTools(...enhancers) 30 | 31 | if (NOT_IN_PROD && module.hot) { 32 | module.hot.accept("../reducers", () => store.replaceReducer(rootReducer)) 33 | } 34 | 35 | const store = createStore(rootReducer, preloadState, composedEnhancers) 36 | 37 | return store 38 | } 39 | 40 | export default configureStore 41 | -------------------------------------------------------------------------------- /src/redux/store/instrument/initialState.ts: -------------------------------------------------------------------------------- 1 | import { IInstrument } from "./interfaces" 2 | 3 | export const instruments: { [instrumeID: string]: IInstrument } = { 4 | "a5caf57b-0771-4c56-a600-28a422f0c45d": { 5 | id: "a5caf57b-0771-4c56-a600-28a422f0c45d", 6 | label: "TR808-BD", 7 | group: "TR808", 8 | sampleIDs: ["7ff6ffa7-9768-4bfc-b6c8-b99a70be556b"], 9 | mapping: { 10 | M69: { 11 | midi: 69, 12 | sampleID: "7ff6ffa7-9768-4bfc-b6c8-b99a70be556b", 13 | detune: 0 14 | } 15 | } 16 | }, 17 | "8ba96671-f8b4-45fc-8aa9-6f229154c5db": { 18 | id: "8ba96671-f8b4-45fc-8aa9-6f229154c5db", 19 | label: "TR808-SD", 20 | group: "TR808", 21 | sampleIDs: ["7f9a144d-64b5-43e0-a3ca-3878085ce582"], 22 | mapping: { 23 | M0: { 24 | midi: 0, 25 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 26 | detune: -100 27 | }, 28 | M1: { 29 | midi: 1, 30 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 31 | detune: -75 32 | }, 33 | M2: { 34 | midi: 2, 35 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 36 | detune: -50 37 | }, 38 | M3: { 39 | midi: 3, 40 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 41 | detune: -25 42 | }, 43 | M4: { 44 | midi: 4, 45 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 46 | detune: 0 47 | }, 48 | M5: { 49 | midi: 5, 50 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 51 | detune: 25 52 | }, 53 | M6: { 54 | midi: 6, 55 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 56 | detune: 50 57 | }, 58 | M7: { 59 | midi: 7, 60 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 61 | detune: 75 62 | }, 63 | M8: { 64 | midi: 8, 65 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 66 | detune: 100 67 | }, 68 | M69: { 69 | midi: 69, 70 | sampleID: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 71 | detune: 0 72 | } 73 | } 74 | }, 75 | "eb8ee72b-726d-4238-944f-b220d989a903": { 76 | id: "eb8ee72b-726d-4238-944f-b220d989a903", 77 | label: "TR808-OH", 78 | group: "TR808", 79 | sampleIDs: ["9dce9279-194e-4d6f-9f07-d7968eb13f63"], 80 | mapping: { 81 | M69: { 82 | midi: 69, 83 | sampleID: "9dce9279-194e-4d6f-9f07-d7968eb13f63", 84 | detune: 0 85 | } 86 | } 87 | }, 88 | "3e8329f8-945d-4dde-9307-f14d3542973a": { 89 | id: "3e8329f8-945d-4dde-9307-f14d3542973a", 90 | label: "TR808-MA", 91 | group: "TR808", 92 | sampleIDs: ["19b606f5-52b5-49f5-a3b6-566c245e0407"], 93 | mapping: { 94 | M69: { 95 | midi: 69, 96 | sampleID: "19b606f5-52b5-49f5-a3b6-566c245e0407", 97 | detune: 0 98 | } 99 | } 100 | }, 101 | "77d2c144-2a6c-483a-b94c-8584dcdc2b7c": { 102 | id: "77d2c144-2a6c-483a-b94c-8584dcdc2b7c", 103 | label: "TR808-CP", 104 | group: "TR808", 105 | sampleIDs: ["8cf86f2f-0b50-42bb-81d8-22731d462161"], 106 | mapping: { 107 | M69: { 108 | midi: 69, 109 | sampleID: "8cf86f2f-0b50-42bb-81d8-22731d462161", 110 | detune: 0 111 | } 112 | } 113 | }, 114 | "cfaf931f-2082-4bed-86dd-cf534e2e0c97": { 115 | id: "cfaf931f-2082-4bed-86dd-cf534e2e0c97", 116 | label: "TR808-RS", 117 | group: "TR808", 118 | sampleIDs: ["f57d9727-d7f0-4027-b9cd-fb7b56a79df4"], 119 | mapping: { 120 | M69: { 121 | midi: 69, 122 | sampleID: "f57d9727-d7f0-4027-b9cd-fb7b56a79df4", 123 | detune: 0 124 | } 125 | } 126 | }, 127 | "a025e47b-3e71-4c03-b3b8-de203b3b6f12": { 128 | id: "a025e47b-3e71-4c03-b3b8-de203b3b6f12", 129 | label: "BASS-STACCATO", 130 | group: "BASS", 131 | sampleIDs: [ 132 | "acc4ea8c-cd40-44f2-b553-0642f411a144", 133 | "1e95d1b8-440a-41f3-ab0a-3a48587bf6f7", 134 | "901cfa41-c230-4c26-903b-22f99ee13deb", 135 | "6eeff2d5-6c90-43c2-91f6-3a68f0911483", 136 | "53a20b19-712e-4a43-b718-98b7ff897880", 137 | "fc897b72-744c-434b-9018-6e860da11edb" 138 | ], 139 | mapping: { 140 | M28: { 141 | midi: 28, 142 | sampleID: "acc4ea8c-cd40-44f2-b553-0642f411a144", 143 | detune: 0 144 | }, // E1 145 | M29: { 146 | midi: 29, 147 | sampleID: "acc4ea8c-cd40-44f2-b553-0642f411a144", 148 | detune: 100 149 | }, 150 | M30: { 151 | midi: 30, 152 | sampleID: "acc4ea8c-cd40-44f2-b553-0642f411a144", 153 | detune: 200 154 | }, 155 | M31: { 156 | midi: 31, 157 | sampleID: "1e95d1b8-440a-41f3-ab0a-3a48587bf6f7", 158 | detune: -200 159 | }, 160 | M32: { 161 | midi: 32, 162 | sampleID: "1e95d1b8-440a-41f3-ab0a-3a48587bf6f7", 163 | detune: -100 164 | }, 165 | M33: { 166 | midi: 33, 167 | sampleID: "1e95d1b8-440a-41f3-ab0a-3a48587bf6f7", 168 | detune: 0 169 | }, // A1 170 | M34: { 171 | midi: 34, 172 | sampleID: "1e95d1b8-440a-41f3-ab0a-3a48587bf6f7", 173 | detune: 100 174 | }, 175 | M35: { 176 | midi: 35, 177 | sampleID: "1e95d1b8-440a-41f3-ab0a-3a48587bf6f7", 178 | detune: 200 179 | }, 180 | M36: { 181 | midi: 36, 182 | sampleID: "1e95d1b8-440a-41f3-ab0a-3a48587bf6f7", 183 | detune: 300 184 | }, 185 | M37: { 186 | midi: 37, 187 | sampleID: "901cfa41-c230-4c26-903b-22f99ee13deb", 188 | detune: -300 189 | }, 190 | M38: { 191 | midi: 38, 192 | sampleID: "901cfa41-c230-4c26-903b-22f99ee13deb", 193 | detune: -200 194 | }, 195 | M39: { 196 | midi: 39, 197 | sampleID: "901cfa41-c230-4c26-903b-22f99ee13deb", 198 | detune: -100 199 | }, 200 | M40: { 201 | midi: 40, 202 | sampleID: "901cfa41-c230-4c26-903b-22f99ee13deb", 203 | detune: 0 204 | }, // E2 205 | M41: { 206 | midi: 41, 207 | sampleID: "901cfa41-c230-4c26-903b-22f99ee13deb", 208 | detune: 100 209 | }, 210 | M42: { 211 | midi: 42, 212 | sampleID: "901cfa41-c230-4c26-903b-22f99ee13deb", 213 | detune: 200 214 | }, 215 | M43: { 216 | midi: 43, 217 | sampleID: "6eeff2d5-6c90-43c2-91f6-3a68f0911483", 218 | detune: -200 219 | }, 220 | M44: { 221 | midi: 44, 222 | sampleID: "6eeff2d5-6c90-43c2-91f6-3a68f0911483", 223 | detune: -100 224 | }, 225 | M45: { 226 | midi: 45, 227 | sampleID: "6eeff2d5-6c90-43c2-91f6-3a68f0911483", 228 | detune: 0 229 | }, // A2 230 | M46: { 231 | midi: 46, 232 | sampleID: "6eeff2d5-6c90-43c2-91f6-3a68f0911483", 233 | detune: 100 234 | }, 235 | M47: { 236 | midi: 47, 237 | sampleID: "6eeff2d5-6c90-43c2-91f6-3a68f0911483", 238 | detune: 200 239 | }, 240 | M48: { 241 | midi: 48, 242 | sampleID: "6eeff2d5-6c90-43c2-91f6-3a68f0911483", 243 | detune: 300 244 | }, 245 | M49: { 246 | midi: 49, 247 | sampleID: "53a20b19-712e-4a43-b718-98b7ff897880", 248 | detune: -300 249 | }, 250 | M50: { 251 | midi: 50, 252 | sampleID: "53a20b19-712e-4a43-b718-98b7ff897880", 253 | detune: -200 254 | }, 255 | M51: { 256 | midi: 51, 257 | sampleID: "53a20b19-712e-4a43-b718-98b7ff897880", 258 | detune: -100 259 | }, 260 | M52: { 261 | midi: 52, 262 | sampleID: "53a20b19-712e-4a43-b718-98b7ff897880", 263 | detune: 0 264 | }, // E3 265 | M53: { 266 | midi: 53, 267 | sampleID: "53a20b19-712e-4a43-b718-98b7ff897880", 268 | detune: 100 269 | }, 270 | M54: { 271 | midi: 54, 272 | sampleID: "53a20b19-712e-4a43-b718-98b7ff897880", 273 | detune: 200 274 | }, 275 | M55: { 276 | midi: 55, 277 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 278 | detune: -200 279 | }, 280 | M56: { 281 | midi: 56, 282 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 283 | detune: -100 284 | }, 285 | M57: { 286 | midi: 57, 287 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 288 | detune: 0 289 | }, // A3 290 | M58: { 291 | midi: 58, 292 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 293 | detune: 100 294 | }, 295 | M59: { 296 | midi: 59, 297 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 298 | detune: 200 299 | }, 300 | M60: { 301 | midi: 60, 302 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 303 | detune: 300 304 | }, 305 | M61: { 306 | midi: 61, 307 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 308 | detune: 400 309 | }, 310 | M62: { 311 | midi: 62, 312 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 313 | detune: 500 314 | }, 315 | M63: { 316 | midi: 63, 317 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 318 | detune: 600 319 | }, 320 | M64: { 321 | midi: 64, 322 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 323 | detune: 700 324 | }, 325 | M65: { 326 | midi: 65, 327 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 328 | detune: 800 329 | }, 330 | M66: { 331 | midi: 66, 332 | sampleID: "fc897b72-744c-434b-9018-6e860da11edb", 333 | detune: 900 334 | } 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/redux/store/instrument/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IInstruments { 2 | [instrumentID: string]: IInstrument 3 | } 4 | 5 | export interface IInstrument { 6 | // primary key (UUIDv4) 7 | id: string 8 | 9 | label: string 10 | 11 | group: string 12 | 13 | sampleIDs: string[] 14 | 15 | mapping: { 16 | [midiNote: string]: IInstrumentMapping 17 | } 18 | } 19 | 20 | export interface IInstrumentMapping { 21 | // midi note 22 | midi: number 23 | 24 | // sample ID (UUIDv4) 25 | sampleID: string 26 | 27 | // detune (cents) 28 | detune: number 29 | } 30 | 31 | export type Note = 32 | | "A0" 33 | | "B0" 34 | | "C1" 35 | | "D1" 36 | | "E1" 37 | | "F1" 38 | | "G1" 39 | | "A1" 40 | | "B1" 41 | | "C2" 42 | | "D2" 43 | | "E2" 44 | | "F2" 45 | | "G2" 46 | | "A2" 47 | | "B2" 48 | | "C3" 49 | | "D3" 50 | | "E3" 51 | | "F3" 52 | | "G3" 53 | | "A3" 54 | | "B3" 55 | | "C4" 56 | | "D4" 57 | | "E4" 58 | | "F4" 59 | | "G4" 60 | | "A4" 61 | | "B4" 62 | | "C5" 63 | | "D5" 64 | | "E5" 65 | | "F5" 66 | | "G5" 67 | | "A5" 68 | | "B5" 69 | | "C6" 70 | | "D6" 71 | | "E6" 72 | | "F6" 73 | | "G6" 74 | | "A6" 75 | | "B6" 76 | | "C7" 77 | | "D7" 78 | | "E7" 79 | | "F7" 80 | | "G7" 81 | | "A7" 82 | | "B7" 83 | | "C8" 84 | -------------------------------------------------------------------------------- /src/redux/store/sample/initialState.ts: -------------------------------------------------------------------------------- 1 | import { ISample } from "./interfaces" 2 | 3 | export const samples: { [sampleID: string]: ISample } = { 4 | "7ff6ffa7-9768-4bfc-b6c8-b99a70be556b": { 5 | id: "7ff6ffa7-9768-4bfc-b6c8-b99a70be556b", 6 | filename: "BD2525.WAV", 7 | url: "/sounds/7ff6ffa7-9768-4bfc-b6c8-b99a70be556b.wav", 8 | label: "TR808 - BD2525", 9 | type: "audio/wave" 10 | }, 11 | "7f9a144d-64b5-43e0-a3ca-3878085ce582": { 12 | id: "7f9a144d-64b5-43e0-a3ca-3878085ce582", 13 | filename: "SD0010.WAV", 14 | url: "/sounds/7f9a144d-64b5-43e0-a3ca-3878085ce582.wav", 15 | label: "TR808 - SD0010", 16 | type: "audio/wave" 17 | }, 18 | "9dce9279-194e-4d6f-9f07-d7968eb13f63": { 19 | id: "9dce9279-194e-4d6f-9f07-d7968eb13f63", 20 | filename: "OH00.WAV", 21 | url: "/sounds/9dce9279-194e-4d6f-9f07-d7968eb13f63.wav", 22 | label: "TR808 - OH00", 23 | type: "audio/wave" 24 | }, 25 | "19b606f5-52b5-49f5-a3b6-566c245e0407": { 26 | id: "19b606f5-52b5-49f5-a3b6-566c245e0407", 27 | filename: "MA.WAV", 28 | url: "/sounds/19b606f5-52b5-49f5-a3b6-566c245e0407.wav", 29 | label: "TR808 - MA", 30 | type: "audio/wave" 31 | }, 32 | "8cf86f2f-0b50-42bb-81d8-22731d462161": { 33 | id: "8cf86f2f-0b50-42bb-81d8-22731d462161", 34 | filename: "CP.WAV", 35 | url: "/sounds/8cf86f2f-0b50-42bb-81d8-22731d462161.wav", 36 | label: "TR808 - CP", 37 | type: "audio/wave" 38 | }, 39 | "f57d9727-d7f0-4027-b9cd-fb7b56a79df4": { 40 | id: "f57d9727-d7f0-4027-b9cd-fb7b56a79df4", 41 | filename: "RS.WAV", 42 | url: "/sounds/f57d9727-d7f0-4027-b9cd-fb7b56a79df4.wav", 43 | label: "TR808 - RS", 44 | type: "audio/wave" 45 | }, 46 | "acc4ea8c-cd40-44f2-b553-0642f411a144": { 47 | id: "acc4ea8c-cd40-44f2-b553-0642f411a144", 48 | filename: "BASS_STAC_E0.WAV", 49 | url: "/sounds/acc4ea8c-cd40-44f2-b553-0642f411a144.wav", 50 | label: "BASS_STAC_E0", 51 | type: "audio/wave" 52 | }, 53 | "1e95d1b8-440a-41f3-ab0a-3a48587bf6f7": { 54 | id: "1e95d1b8-440a-41f3-ab0a-3a48587bf6f7", 55 | filename: "BASS_STAC_A0.WAV", 56 | url: "/sounds/1e95d1b8-440a-41f3-ab0a-3a48587bf6f7.wav", 57 | label: "BASS_STAC_A0", 58 | type: "audio/wave" 59 | }, 60 | "901cfa41-c230-4c26-903b-22f99ee13deb": { 61 | id: "901cfa41-c230-4c26-903b-22f99ee13deb", 62 | filename: "BASS_STAC_E1.WAV", 63 | url: "/sounds/901cfa41-c230-4c26-903b-22f99ee13deb.wav", 64 | label: "BASS_STAC_E1", 65 | type: "audio/wave" 66 | }, 67 | "6eeff2d5-6c90-43c2-91f6-3a68f0911483": { 68 | id: "6eeff2d5-6c90-43c2-91f6-3a68f0911483", 69 | filename: "BASS_STAC_A1.WAV", 70 | url: "/sounds/6eeff2d5-6c90-43c2-91f6-3a68f0911483.wav", 71 | label: "BASS_STAC_A1", 72 | type: "audio/wave" 73 | }, 74 | "53a20b19-712e-4a43-b718-98b7ff897880": { 75 | id: "53a20b19-712e-4a43-b718-98b7ff897880", 76 | filename: "BASS_STAC_E2.WAV", 77 | url: "/sounds/53a20b19-712e-4a43-b718-98b7ff897880.wav", 78 | label: "BASS_STAC_E2", 79 | type: "audio/wave" 80 | }, 81 | "fc897b72-744c-434b-9018-6e860da11edb": { 82 | id: "fc897b72-744c-434b-9018-6e860da11edb", 83 | filename: "BASS_STAC_A2.WAV", 84 | url: "/sounds/fc897b72-744c-434b-9018-6e860da11edb.wav", 85 | label: "BASS_STAC_A2", 86 | type: "audio/wave" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/redux/store/sample/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ISamples { 2 | [sampleID: string]: ISample 3 | } 4 | 5 | export interface ISample { 6 | // primary key (UUIDv4) 7 | id: string 8 | 9 | // original filename 10 | filename: string 11 | 12 | url: string 13 | 14 | // mime type 15 | type: string 16 | 17 | label: string 18 | 19 | // sample set 20 | group?: string 21 | } 22 | -------------------------------------------------------------------------------- /src/redux/store/session/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { MaterialColor } from "../../../utils/color/colorLibrary" 2 | import { ISample } from "../sample/interfaces" 3 | import { IInstrument } from "../instrument/interfaces" 4 | 5 | export interface ISession { 6 | // tempo [20, 200] 7 | tempo: number 8 | 9 | // mater gain [0, 1] 10 | masterGain: number 11 | 12 | activeCellBeat: number | null 13 | 14 | matrix: { 15 | // track ID (UUIDv4) 16 | [trackID: string]: ICell[] 17 | } 18 | 19 | trackOrder: string[] 20 | 21 | activeTrackID: string | null 22 | 23 | tracks: { 24 | // track ID (UUIDv4) 25 | [trackID: string]: ITrack 26 | } 27 | 28 | instruments: { 29 | [instrumentID: string]: IInstrument 30 | } 31 | 32 | samples: { 33 | [sampleID: string]: ISample 34 | } 35 | } 36 | 37 | export interface ICell { 38 | // note scheduled or not 39 | scheduled: boolean 40 | 41 | // midi note 42 | midi: number 43 | 44 | // cell processing 45 | processing: IAudioProcessing 46 | } 47 | 48 | export type NoteResolution = 1 | 2 | 4 49 | 50 | export interface ITracks { 51 | [trackID: string]: ITrack 52 | } 53 | 54 | export interface ITrack { 55 | // primary key (UUIDv4) 56 | id: string 57 | 58 | label: string 59 | 60 | // 1=16th note, 2=8th note, 4=quarter note 61 | noteResolution: NoteResolution 62 | 63 | // instrument ID (UUIDv4) 64 | instrumentID: string 65 | 66 | // track color 67 | color: MaterialColor 68 | 69 | // track processing 70 | processing: IAudioProcessing 71 | 72 | muted: boolean 73 | 74 | soloed: boolean 75 | } 76 | 77 | export interface IAudioProcessing { 78 | gain: IGainProcessing 79 | filter?: IFilterProcessing 80 | delay?: IDelayProcessing 81 | distorsion?: IDistorsionProcessing 82 | } 83 | 84 | export interface IGainProcessing { 85 | gain: number 86 | } 87 | 88 | export interface IFilterProcessing { 89 | enabled: boolean 90 | type: string 91 | frequency: number 92 | gain: number 93 | q?: number 94 | } 95 | 96 | export interface IDelayProcessing { 97 | enabled: boolean 98 | delayTime: number 99 | } 100 | 101 | export interface IDistorsionProcessing { 102 | enabled: boolean 103 | curve: Float32Array 104 | oversample?: string 105 | } 106 | -------------------------------------------------------------------------------- /src/serviceWorker.tsx: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config: any) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL as string, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | // tslint:disable-next-line:no-console 45 | console.log( 46 | 'This web app is being served cache-first by a service ' + 47 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 48 | ); 49 | }); 50 | } else { 51 | // Is not localhost. Just register service worker 52 | registerValidSW(swUrl, config); 53 | } 54 | }); 55 | } 56 | } 57 | 58 | function registerValidSW(swUrl: string, config: any) { 59 | navigator.serviceWorker 60 | .register(swUrl) 61 | .then(registration => { 62 | registration.onupdatefound = () => { 63 | const installingWorker = registration.installing; 64 | if (installingWorker == null) { 65 | return; 66 | } 67 | installingWorker.onstatechange = () => { 68 | if (installingWorker.state === 'installed') { 69 | if (navigator.serviceWorker.controller) { 70 | // At this point, the updated precached content has been fetched, 71 | // but the previous service worker will still serve the older 72 | // content until all client tabs are closed. 73 | // tslint:disable-next-line:no-console 74 | console.log( 75 | 'New content is available and will be used when all ' + 76 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 77 | ); 78 | 79 | // Execute callback 80 | if (config && config.onUpdate) { 81 | config.onUpdate(registration); 82 | } 83 | } else { 84 | // At this point, everything has been precached. 85 | // It's the perfect time to display a 86 | // "Content is cached for offline use." message. 87 | // tslint:disable-next-line:no-console 88 | console.log('Content is cached for offline use.'); 89 | 90 | // Execute callback 91 | if (config && config.onSuccess) { 92 | config.onSuccess(registration); 93 | } 94 | } 95 | } 96 | }; 97 | }; 98 | }) 99 | .catch(error => { 100 | // tslint:disable-next-line:no-console 101 | console.error('Error during service worker registration:', error); 102 | }); 103 | } 104 | 105 | function checkValidServiceWorker(swUrl: string, config: any) { 106 | // Check if the service worker can be found. If it can't reload the page. 107 | fetch(swUrl) 108 | .then(response => { 109 | // Ensure service worker exists, and that we really are getting a JS file. 110 | const contentType = response.headers.get('content-type'); 111 | if ( 112 | response.status === 404 || 113 | (contentType != null && contentType.indexOf('javascript') === -1) 114 | ) { 115 | // No service worker found. Probably a different app. Reload the page. 116 | navigator.serviceWorker.ready.then(registration => { 117 | registration.unregister().then(() => { 118 | window.location.reload(); 119 | }); 120 | }); 121 | } else { 122 | // Service worker found. Proceed as normal. 123 | registerValidSW(swUrl, config); 124 | } 125 | }) 126 | .catch(() => { 127 | // tslint:disable-next-line:no-console 128 | console.log( 129 | 'No internet connection found. App is running in offline mode.' 130 | ); 131 | }); 132 | } 133 | 134 | export function unregister() { 135 | if ('serviceWorker' in navigator) { 136 | navigator.serviceWorker.ready.then(registration => { 137 | registration.unregister(); 138 | }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/services/cell.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | describe("cell", () => { 3 | // TODO: test 4 | }) 5 | -------------------------------------------------------------------------------- /src/services/cell.ts: -------------------------------------------------------------------------------- 1 | import { NoteResolution } from "../redux/store/session/interfaces" 2 | 3 | /** 4 | * Check if a cell is played regarding the current note resolution 5 | * 6 | * @param noteResolution 7 | * @param cellBeat 8 | * @param currentBeat 9 | * @return {boolean} 10 | */ 11 | export const isCellPlayed = ( 12 | noteResolution: NoteResolution, 13 | cellBeat: number, 14 | currentBeat: number 15 | ): boolean => { 16 | return noteResolution === 1 17 | ? cellBeat === currentBeat 18 | : noteResolution === 2 19 | ? cellBeat === currentBeat || cellBeat === currentBeat - 1 20 | : noteResolution === 4 21 | ? cellBeat === currentBeat || 22 | cellBeat === currentBeat - 1 || 23 | cellBeat === currentBeat - 2 || 24 | cellBeat === currentBeat - 3 25 | : false 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/audio/MidiConverter.ts: -------------------------------------------------------------------------------- 1 | export default class MidiConverter { 2 | static mapping = new Map([ 3 | [127, "G9"], 4 | [126, "F#9"], 5 | [125, "F9"], 6 | [124, "E9"], 7 | [123, "Eb9"], 8 | [122, "D9"], 9 | [121, "C#9"], 10 | [120, "C9"], 11 | [119, "B8"], 12 | [118, "Bb8"], 13 | [117, "A8"], 14 | [116, "G#8"], 15 | [115, "G8"], 16 | [114, "F#8"], 17 | [113, "F8"], 18 | [112, "E8"], 19 | [111, "Eb8"], 20 | [110, "D8"], 21 | [109, "C#8"], 22 | [108, "C8"], 23 | [107, "B7"], 24 | [106, "Bb7"], 25 | [105, "A7"], 26 | [104, "G#7"], 27 | [103, "G7"], 28 | [102, "F#7"], 29 | [101, "F7"], 30 | [100, "E7"], 31 | [99, "Eb7"], 32 | [98, "D7"], 33 | [97, "C#7"], 34 | [96, "C7"], 35 | [95, "B6"], 36 | [94, "Bb6"], 37 | [93, "A6"], 38 | [92, "G#6"], 39 | [91, "G6"], 40 | [90, "F#6"], 41 | [89, "F6"], 42 | [88, "E6"], 43 | [87, "Eb6"], 44 | [86, "D6"], 45 | [85, "C#6"], 46 | [84, "C6"], 47 | [83, "B5"], 48 | [82, "Bb5"], 49 | [81, "A5"], 50 | [80, "G#5"], 51 | [79, "G5"], 52 | [78, "F#5"], 53 | [77, "F5"], 54 | [76, "E5"], 55 | [75, "Eb5"], 56 | [74, "D5"], 57 | [73, "C#5"], 58 | [72, "C5"], 59 | [71, "B4"], 60 | [70, "Bb4"], 61 | [69, "A4"], 62 | [68, "G#4"], 63 | [67, "G4"], 64 | [66, "F#4"], 65 | [65, "F4"], 66 | [64, "E4"], 67 | [63, "Eb4"], 68 | [62, "D4"], 69 | [61, "C#4"], 70 | [60, "C4"], 71 | [59, "B3"], 72 | [58, "Bb3"], 73 | [57, "A3"], 74 | [56, "G#3"], 75 | [55, "G3"], 76 | [54, "F#3"], 77 | [53, "F3"], 78 | [52, "E3"], 79 | [51, "Eb3"], 80 | [50, "D3"], 81 | [49, "C#3"], 82 | [48, "C3"], 83 | [47, "B2"], 84 | [46, "Bb2"], 85 | [45, "A2"], 86 | [44, "G#2"], 87 | [43, "G2"], 88 | [42, "F#2"], 89 | [41, "F2"], 90 | [40, "E2"], 91 | [39, "Eb2"], 92 | [38, "D2"], 93 | [37, "C#2"], 94 | [36, "C2"], 95 | [35, "B1"], 96 | [34, "Bb1"], 97 | [33, "A1"], 98 | [32, "G#1"], 99 | [31, "G1"], 100 | [30, "F#1"], 101 | [29, "F1"], 102 | [28, "E1"], 103 | [27, "Eb1"], 104 | [26, "D1"], 105 | [25, "C#1"], 106 | [24, "C1"], 107 | [23, "B0"], 108 | [22, "Bb0"], 109 | [21, "A0"], 110 | [20, "M20"], 111 | [19, "M19"], 112 | [18, "M18"], 113 | [17, "M17"], 114 | [16, "M16"], 115 | [15, "M15"], 116 | [14, "M14"], 117 | [13, "M13"], 118 | [12, "M12"], 119 | [11, "M11"], 120 | [10, "M10"], 121 | [9, "M9"], 122 | [8, "M8"], 123 | [7, "M7"], 124 | [6, "M6"], 125 | [5, "M5"], 126 | [4, "M4"], 127 | [3, "M3"], 128 | [2, "M2"], 129 | [1, "M1"], 130 | [0, "M0"] 131 | ]) 132 | 133 | static toNote(midiNote: number | null): string | undefined { 134 | if (midiNote === null) { 135 | return "" 136 | } 137 | 138 | return MidiConverter.mapping.get(midiNote) 139 | } 140 | 141 | // TODO write toMidi converter 142 | static toMidi() { 143 | /* */ 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/utils/color/colorLuminance.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://www.sitepoint.com/javascript-generate-lighter-darker-color/ 3 | * 4 | * @param hex 5 | * @param lum 6 | * @return {string|string} 7 | */ 8 | function colorLuminance(hex: string, lum: number): string { 9 | // validate hex string 10 | let h = String(hex).replace(/[^0-9a-f]/gi, "") 11 | if (h.length < 6) { 12 | h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2] 13 | } 14 | const l = lum || 0 15 | 16 | // convert to decimal and change luminosity 17 | let rgb = "#" 18 | let c 19 | let i 20 | 21 | for (i = 0; i < 3; i++) { 22 | c = parseInt(h.substr(i * 2, 2), 16) 23 | c = Math.round(Math.min(Math.max(0, c + c * l), 255)).toString(16) 24 | rgb += ("00" + c).substr(c.length) 25 | } 26 | 27 | return rgb 28 | } 29 | 30 | export default colorLuminance 31 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | export const NOT_IN_PROD = process.env.NODE_ENV !== "production" 2 | export const IN_DEV = process.env.NODE_ENV === "development" 3 | export const inDev = (): boolean => IN_DEV 4 | -------------------------------------------------------------------------------- /src/utils/trigo/polar.test.ts: -------------------------------------------------------------------------------- 1 | import { coordinates } from "./polar" 2 | 3 | describe("coordinates", () => { 4 | it("should return when angle equals 0", () => { 5 | const coord = coordinates(35, 35, 35) 6 | 7 | const result = coord(0) 8 | 9 | expect(parseFloat(result.x.toFixed(2))).toEqual(35.0) 10 | expect(parseFloat(result.y.toFixed(2))).toEqual(70.0) 11 | }) 12 | 13 | it("should return when angle equals 90", () => { 14 | const coord = coordinates(35, 35, 35) 15 | 16 | const result = coord(90) 17 | 18 | expect(parseFloat(result.x.toFixed(2))).toEqual(0.0) 19 | expect(parseFloat(result.y.toFixed(2))).toEqual(35.0) 20 | }) 21 | 22 | it("should return when angle equals 180", () => { 23 | const coord = coordinates(35, 35, 35) 24 | 25 | const result = coord(180) 26 | 27 | expect(parseFloat(result.x.toFixed(2))).toEqual(35.0) 28 | expect(parseFloat(result.y.toFixed(2))).toEqual(0.0) 29 | }) 30 | 31 | it("should return when angle equals 270", () => { 32 | const coord = coordinates(35, 35, 35) 33 | 34 | const result = coord(270) 35 | 36 | expect(parseFloat(result.x.toFixed(2))).toEqual(70.0) 37 | expect(parseFloat(result.y.toFixed(2))).toEqual(35.0) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/utils/trigo/polar.ts: -------------------------------------------------------------------------------- 1 | export const coordinates = ( 2 | centerX: number, 3 | centerY: number, 4 | radius: number 5 | ) => (angle: number) => { 6 | return { 7 | x: radius * Math.cos(((2 * Math.PI) / 360.0) * (angle + 90)) + centerX, 8 | y: radius * Math.sin(((2 * Math.PI) / 360.0) * (angle + 90)) + centerY 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/uuid/uuid.ts: -------------------------------------------------------------------------------- 1 | export type UUID = string 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve", 21 | "downlevelIteration": true, 22 | "typeRoots": ["./node_modules/@types"], 23 | "plugins": [{ "name": "typescript-plugin-css-modules" }], 24 | "strictPropertyInitialization": true, 25 | "strictNullChecks": true 26 | } , 27 | "include": [ 28 | "src" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "object-literal-sort-keys": false, 5 | "ordered-imports": false, 6 | "member-ordering": [true, { "order": "fields-first" }], 7 | "member-access": [true, "no-public"], 8 | "no-implicit-dependencies": [true, "dev"] 9 | } 10 | } 11 | --------------------------------------------------------------------------------