├── .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 |
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 | [](https://app.netlify.com/sites/vigilant-goldberg-a80afb/deploys)
3 |
4 | What it looks like:
5 | 
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 |
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 |
--------------------------------------------------------------------------------