├── public
├── favicon-32x32.png
├── favicon-64x64.png
├── favicon-128x128.png
├── favicon-192x192.png
├── favicon-256x256.png
└── favicon-32x32alt2.png
├── src
├── client
│ ├── assets
│ │ ├── LA4.jpg
│ │ └── night-sky.jpg
│ ├── helpers
│ │ ├── socket.js
│ │ └── audioHelpers.js
│ ├── components
│ │ ├── Share.jsx
│ │ ├── Footer.jsx
│ │ ├── TimeDisplay.jsx
│ │ ├── TempoSelector.jsx
│ │ ├── Users.jsx
│ │ ├── PlayPauseButton.jsx
│ │ ├── GridButton.jsx
│ │ ├── ControlBar.jsx
│ │ ├── Board.jsx
│ │ ├── ScaleSelector.jsx
│ │ ├── Knob.jsx
│ │ ├── KnobPanel.jsx
│ │ ├── InstrumentColumn.jsx
│ │ └── InlineEdit.jsx
│ ├── index.js
│ ├── constants
│ │ ├── scales.js
│ │ ├── soundPresets.js
│ │ └── initState.js
│ ├── reducer
│ │ ├── reducerConstants.js
│ │ └── reducer.jsx
│ ├── containers
│ │ ├── App.jsx
│ │ ├── HeaderContainer.jsx
│ │ └── MainContainer.jsx
│ ├── styles
│ │ └── globalStyles.js
│ └── hooks
│ │ ├── useOnClickOutside.js
│ │ └── useKeypress.js
└── server
│ ├── assets
│ └── audio
│ │ ├── EA7301_808_Sd.wav
│ │ ├── EA7419_909_Sd.wav
│ │ ├── EA7701_R8_Sd.wav
│ │ ├── EA7716_R8_Rim.wav
│ │ ├── EA7720_R8_Sd.wav
│ │ ├── EA8722_Sd_House.wav
│ │ └── wamb_mbasefree_006.wav
│ ├── cache.js
│ └── server.js
├── .vscode
├── settings.json
└── launch.json
├── tsconfig.json
├── .eslintrc
├── README.md
├── LICENSE
├── webpack.config.js
├── .gitignore
├── package.json
└── sampleData.js
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon-64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/public/favicon-64x64.png
--------------------------------------------------------------------------------
/public/favicon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/public/favicon-128x128.png
--------------------------------------------------------------------------------
/public/favicon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/public/favicon-192x192.png
--------------------------------------------------------------------------------
/public/favicon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/public/favicon-256x256.png
--------------------------------------------------------------------------------
/src/client/assets/LA4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/src/client/assets/LA4.jpg
--------------------------------------------------------------------------------
/public/favicon-32x32alt2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/public/favicon-32x32alt2.png
--------------------------------------------------------------------------------
/src/client/assets/night-sky.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/src/client/assets/night-sky.jpg
--------------------------------------------------------------------------------
/src/client/helpers/socket.js:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client';
2 |
3 | export const socket = io('http://localhost:3000');
4 |
--------------------------------------------------------------------------------
/src/server/assets/audio/EA7301_808_Sd.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/src/server/assets/audio/EA7301_808_Sd.wav
--------------------------------------------------------------------------------
/src/server/assets/audio/EA7419_909_Sd.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/src/server/assets/audio/EA7419_909_Sd.wav
--------------------------------------------------------------------------------
/src/server/assets/audio/EA7701_R8_Sd.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/src/server/assets/audio/EA7701_R8_Sd.wav
--------------------------------------------------------------------------------
/src/server/assets/audio/EA7716_R8_Rim.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/src/server/assets/audio/EA7716_R8_Rim.wav
--------------------------------------------------------------------------------
/src/server/assets/audio/EA7720_R8_Sd.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/src/server/assets/audio/EA7720_R8_Sd.wav
--------------------------------------------------------------------------------
/src/server/assets/audio/EA8722_Sd_House.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/src/server/assets/audio/EA8722_Sd_House.wav
--------------------------------------------------------------------------------
/src/server/assets/audio/wamb_mbasefree_006.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csAudioApps/stepSeq/HEAD/src/server/assets/audio/wamb_mbasefree_006.wav
--------------------------------------------------------------------------------
/src/client/components/Share.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Share = () => (
4 |
5 | );
6 |
7 | export default Share;
8 |
--------------------------------------------------------------------------------
/src/server/cache.js:
--------------------------------------------------------------------------------
1 | const memoryCache = module.exports = function () {
2 | const cache = {};
3 | return {
4 | get: (key) => cache[key],
5 | set: (key, val) => cache[key] = val,
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": [ "javascript", "javascriptreact", "html", "typescriptreact" ],
3 | "eslint.enable": true,
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll.eslint": true
6 | }
7 | }
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './containers/App';
4 |
5 | const root = document.createElement('div');
6 | root.id = 'root';
7 | document.body.appendChild(root);
8 | ReactDOM.render(, root);
9 |
--------------------------------------------------------------------------------
/src/client/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Footer = React.memo(() => (
5 |
6 | {/* footer
*/}
7 |
8 | ));
9 |
10 | export default Footer;
11 |
12 | const StyledFooter = styled.div`
13 | width: 100%;
14 | text-align: center;
15 | `;
16 |
--------------------------------------------------------------------------------
/src/client/constants/scales.js:
--------------------------------------------------------------------------------
1 | const scales = [
2 | ['C', 'D', 'E', 'F', 'G', 'A', 'B'],
3 | ['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb'],
4 | ['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'B'],
5 | ['C', 'D', 'E', 'F#', 'G', 'A', 'B'],
6 | ['C', 'D', 'E', 'F', 'G', 'A', 'Bb'],
7 | ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb'],
8 | ['C', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb'],
9 | ];
10 |
11 | export default scales;
12 |
--------------------------------------------------------------------------------
/src/client/constants/soundPresets.js:
--------------------------------------------------------------------------------
1 | const soundPresets = {
2 | bass: {
3 | volume: -12, // the oscillator volume set to -12dB
4 | oscillator: {
5 | type: 'square', // oscillator type to square wave
6 | },
7 | envelope: {
8 | attack: 0.02, // envelope attack set to 20ms
9 | release: 1, // envelope release set to 1s
10 | },
11 | },
12 | };
13 |
14 | export default soundPresets;
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./build",
4 | "sourceMap": true,
5 | // "noImplicitAny": true,
6 | "module": "commonjs",
7 | "target": "es6",
8 | "jsx": "react",
9 | "allowJs": true,
10 | // "moduleResolution": "node",
11 | // "resolveJsonModule": true,
12 | // "skipLibCheck": true,
13 | // "esModuleInterop": true,
14 | // "baseUrl": ".",
15 | },
16 | "exclude": ["node_modules", ".vscode", "__tests__"]
17 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb",
4 | "airbnb/hooks"
5 | ],
6 | "plugins": [
7 | "react-hooks"
8 | ],
9 | "rules": {
10 | "no-console": "off",
11 | "no-plusplus": "off",
12 | "brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
13 | "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
14 | "react-hooks/exhaustive-deps": "warn", // Checks effect dependencies
15 | "arrow-body-style": "off",
16 | },
17 | "globals": {
18 | "document": true,
19 | "window": true
20 | },
21 | }
--------------------------------------------------------------------------------
/src/client/components/TimeDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import PropTypes from 'prop-types';
4 |
5 | export const TimeDisplay = ({ position }) => (
6 |
7 | {position}
8 |
9 | );
10 |
11 | export default TimeDisplay;
12 |
13 | // TimeDisplay.propTypes = {
14 | // position: PropTypes.string,
15 | // };
16 |
17 | const StyledTimeDisplay = styled.div`
18 | font-family: inherit;
19 | padding: 5px;
20 | float: right
21 | `;
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # stepSeq
2 | Collaborative step-sequencer
3 |
4 | ## Description
5 | Allows multiple users to connect and collaborate on a sequence in real-time
6 |
7 | ### Installing
8 | - npm install
9 | - npm run dev
10 |
11 | ### Executing program
12 | Point browser to localhost:8080 then make sick beats
13 |
14 | ## Authors
15 | - Greg Panciera
16 | - Tom Lutz
17 | - Jonathon Escamilla
18 | - Matt Gin
19 |
20 | ## Version History
21 | * 0.1 Initial Beta Release
22 |
23 | ## License
24 | This project is licensed under the MIT License - see the LICENSE.md file for details
25 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "chrome",
9 | "request": "launch",
10 | "name": "Launch Chrome against localhost",
11 | "url": "http://localhost:8080",
12 | "webRoot": "${workspaceFolder}",
13 | "skipFiles": [
14 | "${workspaceRoot}/node_modules/**/*.js",
15 | ]
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/src/client/reducer/reducerConstants.js:
--------------------------------------------------------------------------------
1 | export const TOGGLE_GRID_BUTTON = 'TOGGLE_GRID_BUTTON';
2 | export const UPDATE_STATUS = 'UPDATE_STATUS';
3 | export const TOGGLE_PLAY_STATE = 'TOGGLE_PLAY_STATE';
4 | export const SET_STATE_FROM_SOCKET = 'SET_STATE_FROM_SOCKET';
5 | export const ADD_USER = 'ADD_USER';
6 | export const REMOVE_USER = 'REMOVE_USER';
7 | export const SET_NEW_USER = 'SET_NEW_USER';
8 | export const SET_LOCAL_USERID = 'SET_LOCAL_USERID';
9 | export const SET_SELECTED_INSTRUMENT = 'SET_SELECTED_INSTRUMENT';
10 | export const SET_SELECTED_SCALE = 'SET_SELECTED_SCALE';
11 | export const SET_TEMPO = 'SET_TEMPO';
12 |
--------------------------------------------------------------------------------
/src/client/containers/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import MainContainer from './MainContainer';
3 | import GlobalStyle from '../styles/globalStyles';
4 | import { socket } from '../helpers/socket';
5 | import ImagePath from '../assets/night-sky.jpg';
6 |
7 | const App = () => {
8 | const [inputText] = useState('');
9 | const send = () => {
10 | console.log('sending text ', inputText);
11 | socket.emit('updateServerState', inputText);
12 | };
13 |
14 | return (
15 | <>
16 |
17 |
18 | >
19 | );
20 | };
21 |
22 | export default App;
23 |
--------------------------------------------------------------------------------
/src/client/containers/HeaderContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Share from '../components/Share';
4 | import Users from '../components/Users';
5 |
6 | const HeaderContainer = React.memo(({ localUserId, username }) => (
7 |
8 |
9 |
13 |
14 | ));
15 |
16 | const StyledDiv = styled.div`
17 | display: flex;
18 | justify-content: flex-end;
19 | color: white;
20 | background-color: #282828;
21 | height: 50px;
22 | width: 100%;
23 | `;
24 |
25 | export default HeaderContainer;
26 |
--------------------------------------------------------------------------------
/src/client/components/TempoSelector.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { number, func } from 'prop-types';
3 | import styled from 'styled-components';
4 | import InlineEdit from './InlineEdit';
5 | import { SET_TEMPO } from '../reducer/reducerConstants';
6 |
7 | const TempoSelector = React.memo(({ dispatch, curTempo }) => (
8 | <>
9 | Tempo
10 | {
13 | let newTempo = Number(newText);
14 | // Make sure it's a valid number, otherwise keep current tempo
15 | newTempo = Number.isNaN(newTempo) ? curTempo : newTempo;
16 | dispatch({ type: SET_TEMPO, payload: { newTempo } });
17 | }}
18 | />
19 | >
20 | ));
21 |
22 | TempoSelector.propTypes = {
23 | dispatch: func.isRequired,
24 | curTempo: number.isRequired,
25 | };
26 |
27 | const TempoTitle = styled.li`
28 | margin: auto 0.4em auto auto;
29 | color: #ababab;
30 | `;
31 |
32 | export default TempoSelector;
33 |
--------------------------------------------------------------------------------
/src/server/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/newline-after-import */
2 | const path = require('path');
3 | const express = require('express');
4 | const app = express();
5 | const http = require('http').createServer(app);
6 | const io = require('socket.io')(http);
7 | const cache = require('./cache')();
8 |
9 | app.use(express.static(path.resolve(__dirname, '/')));
10 |
11 | io.on('connection', (socket) => {
12 | console.log('a user connected');
13 | socket.on('updateServerState', (msg, senderId) => {
14 | // set cached state and broadcast to clients
15 | cache.set('state', msg);
16 | console.log(msg.users);
17 | io.emit('updateClientState', cache.get('state'), senderId);
18 | });
19 |
20 | // if board has already been initialized, send state to client
21 | socket.on('getInitialState', () => {
22 | if (cache.get('users')) io.emit('updateClientState', cache.get('state'));
23 | else (io.emit('firstUser'));
24 | });
25 | });
26 |
27 | http.listen(3000, () => {
28 | console.log('listening on *:3000');
29 | });
30 |
--------------------------------------------------------------------------------
/src/client/components/Users.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Users = React.memo(({ localUserId, username }) => {
5 | console.log('username:', username, 'localUserId:', localUserId);
6 |
7 | return (
8 | <>
9 | user
10 | {' '}
11 |
12 | {username}
13 |
14 |
15 | >
16 | );
17 | });
18 |
19 | const StyledUserTitle = styled.span`
20 | color: #ababab;
21 | margin: auto 10px auto 0px;
22 | font: inherit;
23 | font-weight: 200;
24 | `;
25 |
26 | const UserColorSwatch = styled.span`
27 | background-color: #5dfdcb;
28 | height: 15px;
29 | width: 15px;
30 | border-radius: 50%;
31 | display: inline-block;
32 | margin: auto 10px;
33 | `;
34 |
35 | const StyledUserListDisplay = styled.div`
36 | color: #eaeaea;
37 | margin: auto 10px auto 0px;
38 | font: inherit;
39 | font-weight: 200;
40 | `;
41 |
42 | export default Users;
43 |
--------------------------------------------------------------------------------
/src/client/helpers/audioHelpers.js:
--------------------------------------------------------------------------------
1 | import * as Tone from 'tone';
2 | import scales from '../constants/scales';
3 |
4 | export const updateNoteArray = (grid, scaleNum, rootOctaveNum) => {
5 | const retArr = [];
6 | let curNote;
7 | let oct;
8 |
9 | for (let i = 0; i < grid.length; i++) {
10 | if (grid[i].length > 0) {
11 | curNote = grid[i][0] % 7;
12 | // console.log("updateNoteArray -> curNote", curNote)
13 |
14 | oct = Math.floor(grid[i][0] / 7) + rootOctaveNum;
15 | // console.log("updateNoteArray -> oct", oct)
16 |
17 | retArr[i] = scales[scaleNum][curNote].toString() + oct.toString();
18 | }
19 | else {
20 | retArr[i] = null;
21 | }
22 | }
23 | // console.log(retArr);
24 | return retArr;
25 | };
26 | // export default updateNoteArray;
27 |
28 | export const toggleToneTransport = async () => {
29 | await Tone.start();
30 |
31 | if (Tone.Transport.state === 'stopped' || Tone.Transport.state === 'paused') {
32 | Tone.Transport.start();
33 | }
34 | else {
35 | Tone.Transport.pause();
36 | }
37 | return true;
38 | };
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 csAudioApps
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/client/styles/globalStyles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | const GlobalStyle = createGlobalStyle`
4 | * {
5 | margin: 0;
6 | padding: 0;
7 | box-sizing: border-box;
8 | font: inherit;
9 | }
10 |
11 | *:focus {
12 | outline: 0;
13 | }
14 |
15 | body {
16 | font-family: century-gothic, sans-serif;
17 | font-weight: 200;
18 | font-style: normal;
19 | letter-spacing: 0.15em;
20 | color: #cfcfcf;
21 | background-color: #242424;
22 | height: 100%;
23 | background-image: url(${(props) => props.img});
24 | background-blend-mode: overlay;
25 | background-size: cover;
26 | background-attachment: fixed;
27 | height: 110vh;
28 | overflow-x:hidden;
29 | overflow-y:hidden;
30 | }
31 |
32 | button {
33 | display: inline-block;
34 | border: none;
35 | cursor: pointer;
36 | text-decoration: none;
37 | font-family: century-gothic, sans-serif;
38 | font-weight: 200;
39 | font-style: normal;
40 | font-size: inherit;
41 | color: #cfcfcf;
42 | }
43 |
44 | table {
45 | border-collapse: collapse;
46 | }
47 |
48 | ul {
49 | list-style: none;
50 | }
51 |
52 | `;
53 |
54 | export default GlobalStyle;
55 |
--------------------------------------------------------------------------------
/src/client/hooks/useOnClickOutside.js:
--------------------------------------------------------------------------------
1 | // hook from https://usehooks.com/useOnClickOutside/
2 | import { useEffect } from 'react';
3 |
4 | // Hook
5 | function useOnClickOutside(ref, handler) {
6 | useEffect(() => {
7 | const listener = (event) => {
8 | // Do nothing if clicking ref's element or descendent elements
9 | if (!ref.current || ref.current.contains(event.target)) {
10 | return;
11 | }
12 |
13 | handler(event);
14 | };
15 |
16 | document.addEventListener('mousedown', listener);
17 | document.addEventListener('touchstart', listener);
18 |
19 | return () => {
20 | document.removeEventListener('mousedown', listener);
21 | document.removeEventListener('touchstart', listener);
22 | };
23 | },
24 | // Add ref and handler to effect dependencies
25 | // It's worth noting that because passed in handler is a new ...
26 | // ... function on every render that will cause this effect ...
27 | // ... callback/cleanup to run every render. It's not a big deal ...
28 | // ... but to optimize you can wrap handler in useCallback before ...
29 | // ... passing it into this hook.
30 | [ref, handler]);
31 | }
32 |
33 | export default useOnClickOutside;
34 |
--------------------------------------------------------------------------------
/src/client/hooks/useKeypress.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/exhaustive-deps */
2 | // hook from https://usehooks.com/useKeyPress/
3 | import { useState, useEffect } from 'react';
4 |
5 | // Hook
6 | function useKeyPress(targetKey) {
7 | // State for keeping track of whether key is pressed
8 | const [keyPressed, setKeyPressed] = useState(false);
9 |
10 | // If pressed key is our target key then set to true
11 | const downHandler = ({ key }) => {
12 | if (key === targetKey) {
13 | setKeyPressed(true);
14 | }
15 | };
16 |
17 | // If released key is our target key then set to false
18 | const upHandler = ({ key }) => {
19 | if (key === targetKey) {
20 | setKeyPressed(false);
21 | }
22 | };
23 |
24 | // Add event listeners
25 | useEffect(() => {
26 | window.addEventListener('keydown', downHandler);
27 | window.addEventListener('keyup', upHandler);
28 | // Remove event listeners on cleanup
29 | return () => {
30 | window.removeEventListener('keydown', downHandler);
31 | window.removeEventListener('keyup', upHandler);
32 | };
33 | }, []); // Empty array ensures that effect is only run on mount and unmount
34 |
35 | return keyPressed;
36 | }
37 |
38 | export default useKeyPress;
39 |
--------------------------------------------------------------------------------
/src/client/components/PlayPauseButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import styled from 'styled-components';
4 | import { toggleToneTransport } from '../helpers/audioHelpers';
5 | import { TOGGLE_PLAY_STATE } from '../reducer/reducerConstants';
6 |
7 | const PlayPauseButton = React.memo(({ dispatch, localUserId, isPlaying }) => (
8 |
9 | {
13 | if (toggleToneTransport()) {
14 | dispatch({ type: TOGGLE_PLAY_STATE, payload: { localUserId } });
15 | }
16 | }}
17 | >
18 | { isPlaying ? 'Pause' : 'Play' }
19 |
20 |
21 | ));
22 |
23 | PlayPauseButton.propTypes = {
24 | dispatch: PropTypes.func.isRequired,
25 | localUserId: PropTypes.string.isRequired,
26 | isPlaying: PropTypes.bool.isRequired,
27 | };
28 |
29 | const StyledLi = styled.li`
30 | margin-left: 25px;
31 | align-self: center;
32 | `;
33 |
34 | const StyledPlayPauseButton = styled.button`
35 | width: 120px;
36 | height: 35px;
37 | border-radius: 3px;
38 | background-color: #5b5b5b;
39 | color: #eaeaea;
40 | padding: 5px;
41 | `;
42 |
43 | export default PlayPauseButton;
44 |
--------------------------------------------------------------------------------
/src/client/components/GridButton.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/control-has-associated-label */
2 | import React from 'react';
3 | import styled, { css } from 'styled-components';
4 | import { TOGGLE_GRID_BUTTON } from '../reducer/reducerConstants';
5 |
6 | const COLOR_DEFAULT = '#666666';
7 | const COLOR_SELECTED = '#5dfdcb';
8 | const COLOR_PLAYHEAD = '#c9f9ff';
9 |
10 | const GridButton = ({
11 | x, y, curStepColNum, dispatch, gridState,
12 | }) => {
13 | let isBtnOn = false;
14 | let buttonColor = '';
15 |
16 | if (gridState[x] && Array.isArray(gridState[x])) {
17 | gridState[x].forEach((elem) => {
18 | if (elem === y) {
19 | isBtnOn = true;
20 | // break;
21 | }
22 | });
23 | }
24 |
25 | if (curStepColNum === x) { buttonColor = COLOR_PLAYHEAD; }
26 | else { buttonColor = isBtnOn ? COLOR_SELECTED : COLOR_DEFAULT; }
27 |
28 | return (
29 | dispatch({
34 | type: TOGGLE_GRID_BUTTON,
35 | payload: { x, y },
36 | })}
37 | />
38 | );
39 | };
40 |
41 | const StyledGridButton = styled.button`
42 | border: 0;
43 | margin: 1px;
44 | border-radius: 6px;
45 | width: 50px;
46 | height: 50px;
47 | float: left;
48 | background-color: ${(props) => (props.buttonColor)};
49 | `;
50 |
51 | export default GridButton;
52 |
--------------------------------------------------------------------------------
/src/client/components/ControlBar.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable object-curly-newline */
2 | /* eslint-disable jsx-a11y/control-has-associated-label */
3 | /* eslint-disable jsx-a11y/click-events-have-key-events */
4 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
5 | import { number, func, string, bool } from 'prop-types';
6 | import React from 'react';
7 | import styled from 'styled-components';
8 | import PlayPauseButton from './PlayPauseButton';
9 | import ScaleSelector from './ScaleSelector';
10 | import TempoSelector from './TempoSelector';
11 | // import TimeDisplay from './TimeDisplay';
12 |
13 | const ControlBar = React.memo(({ selectedScale, dispatch, localUserId, isPlaying, curTempo }) => (
14 |
15 |
16 |
17 |
18 | {/* */}
19 |
20 | ));
21 |
22 | ControlBar.propTypes = {
23 | // position: PropTypes.string.isRequired,
24 | selectedScale: number.isRequired,
25 | dispatch: func.isRequired,
26 | localUserId: string.isRequired,
27 | isPlaying: bool.isRequired,
28 | curTempo: number.isRequired,
29 | };
30 |
31 | const ControlBarWrapper = styled.ul`
32 | display: flex;
33 | height: 55px;
34 | border: 1px solid #444444;
35 | `;
36 |
37 | export default ControlBar;
38 |
--------------------------------------------------------------------------------
/src/client/components/Board.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import GridButton from './GridButton';
4 |
5 | const Board = ({
6 | numRows, numColumns, curStepColNum, gridState, dispatch,
7 | }) => {
8 | const renderButtons = () => {
9 | const grid = [];
10 |
11 | for (let y = numRows - 1; y >= 0; y--) {
12 | const buttonRow = [];
13 |
14 | for (let x = 0; x < numColumns; x++) {
15 | buttonRow.push(
16 |
17 |
25 | | ,
26 | );
27 | }
28 | grid.push(
29 |
33 | {buttonRow}
34 |
,
35 | );
36 | }
37 | // console.log("renderButtons -> grid", grid)
38 | return grid;
39 | };
40 |
41 | return (
42 |
43 |
44 |
45 | {renderButtons()}
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | const StyledBoard = styled.div`
53 | padding: 12px;
54 | ${'' /* border: 1px solid grey; */}
55 | border-bottom: 1px solid #444444;
56 | border-right: 1px solid #444444;
57 | `;
58 |
59 | export default Board;
60 |
--------------------------------------------------------------------------------
/src/client/components/ScaleSelector.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/click-events-have-key-events */
2 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
3 | import React from 'react';
4 | import PropTypes from 'prop-types';
5 | import styled from 'styled-components';
6 | import scales from '../constants/scales';
7 | import { SET_SELECTED_SCALE } from '../reducer/reducerConstants';
8 |
9 | const ScaleSelector = React.memo(({ dispatch, localUserId, selectedScale }) => (
10 | <>
11 | Scale
12 | {
13 | scales
14 | ? scales.map((item, scaleIndex) => (
15 | dispatch({
18 | type: SET_SELECTED_SCALE,
19 | payload: { localUserId, selectedScale: scaleIndex },
20 | })}
21 | key={`scaleBtn${scaleIndex.toString()}`}
22 | >
23 | {scaleIndex + 1}
24 |
25 | ))
26 | : Loading...
27 | }
28 | >
29 | ));
30 |
31 | ScaleSelector.propTypes = {
32 | dispatch: PropTypes.func.isRequired,
33 | localUserId: PropTypes.string.isRequired,
34 | selectedScale: PropTypes.number.isRequired,
35 | };
36 |
37 | export default ScaleSelector;
38 |
39 | const ScaleTitle = styled.li`
40 | margin: auto 0.4em auto 20px;
41 | ${'' /* padding: 5px; */}
42 | font-size: 0.95em;
43 | float: left;
44 | color: #eaeaea;
45 | `;
46 |
47 | const ScaleOption = styled.button`
48 | color: #ababab;
49 | margin: 0.3em;
50 | float: left;
51 | font-size: 0.95em;
52 | background: transparent;
53 | font-weight: ${(props) => (props.isSelected ? 700 : 200)};
54 | `;
55 |
--------------------------------------------------------------------------------
/src/client/components/Knob.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React, { useEffect, useRef, memo } from 'react';
3 | import * as Nexus from 'nexusui';
4 |
5 | let id = 0;
6 | function getId() {
7 | id += 1;
8 | return id;
9 | }
10 |
11 | function NO_OP() {}
12 |
13 | const Dial = memo(({
14 | size,
15 | interaction,
16 | max,
17 | min,
18 | mode,
19 | value,
20 | onChange = NO_OP,
21 | onReady = NO_OP,
22 | }) => {
23 | const dial = useRef(null);
24 | const elementId = useRef(`nexus-ui-dial-${getId()}`);
25 |
26 | useEffect(() => {
27 | dial.current = new Nexus.Dial(elementId.current, {
28 | size,
29 | interaction,
30 | max,
31 | min,
32 | mode,
33 | });
34 | onReady(dial.current);
35 | // dial.current.colorize('accent', 'rgba(201, 249, 255, 0.7)');
36 | dial.current.colorize('accent', '#c1c1c1');
37 | dial.current.colorize('fill', '#333');
38 | dial.current.on('change', (newState) => {
39 | onChange(newState);
40 | });
41 | return () => {
42 | dial.current.destroy();
43 | };
44 | }, []);
45 |
46 | useEffect(() => {
47 | if (dial.current === null) return;
48 | if (!Array.isArray(size)) {
49 | return;
50 | }
51 | dial.current.resize(...size);
52 | }, [size]);
53 |
54 | useEffect(() => {
55 | if (dial.current === null) return;
56 | if (value === undefined) return;
57 |
58 | dial.current.value = value;
59 | }, [value]);
60 |
61 | useEffect(() => {
62 | if (dial.current === null) return;
63 | if (min === undefined) return;
64 | dial.current.min = min;
65 | }, [min]);
66 |
67 | useEffect(() => {
68 | if (dial.current === null) return;
69 | if (max === undefined) return;
70 | dial.current.max = max;
71 | }, [max]);
72 |
73 | useEffect(() => {
74 | if (dial.current === null) return;
75 | if (interaction === undefined) return;
76 | dial.current.interaction = interaction;
77 | }, [interaction]);
78 | return ;
79 | });
80 |
81 | export default Dial;
82 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const path = require('path');
4 |
5 | module.exports = {
6 | mode: process.env.NODE_ENV,
7 | entry: './src/client/index.js',
8 |
9 | // Enable sourcemaps for debugging webpack's output.
10 | devtool: 'source-map',
11 |
12 | output: {
13 | path: path.resolve(__dirname, 'dist'),
14 | publicPath: './',
15 | filename: 'bundle.js',
16 | },
17 |
18 | devServer: {
19 | host: 'localhost',
20 | port: 8080,
21 | // match the output path
22 | contentBase: path.resolve(__dirname, 'dist'),
23 | // enable HMR on the devServer
24 | hot: true,
25 | // match the output 'publicPath'
26 | publicPath: '/',
27 | // fallback to root for other urls
28 | historyApiFallback: true,
29 | },
30 |
31 | module: {
32 | rules: [
33 | {
34 | test: /\.(ts|js)x?$/,
35 | include: [path.resolve(__dirname, './src')],
36 | use: {
37 | loader: 'babel-loader',
38 | options: {
39 | presets: [
40 | '@babel/preset-env',
41 | '@babel/preset-react',
42 | // "@babel/preset-typescript",
43 | ],
44 | plugins: [
45 | '@babel/transform-runtime',
46 | ],
47 | },
48 | },
49 | },
50 | {
51 | test: /\.s[ac]ss$/i,
52 | use: ['style-loader', 'css-loader', 'sass-loader'],
53 | },
54 | {
55 | test: /\.(mp3|wav)$/,
56 | use: {
57 | loader: 'file-loader',
58 | options: {
59 | name: '[name].[contenthash].[ext]',
60 | outputPath: 'assets/audio/',
61 | publicPath: 'assets/audio/',
62 | },
63 | },
64 | },
65 | {
66 | test: /\.(png|svg|jpg|gif)$/,
67 | use: ['file-loader'],
68 | },
69 | ],
70 | },
71 | resolve: {
72 | extensions: ['.tsx', '.ts', '.js', '.jsx'],
73 | },
74 | plugins: [
75 | new HtmlWebpackPlugin({
76 | title: 'stepSeq',
77 | favicon: 'public/favicon-32x32.png',
78 | }),
79 | ],
80 | };
81 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 | package-lock.json
44 |
45 | # TypeScript v1 declaration files
46 | typings/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 |
76 | # parcel-bundler cache (https://parceljs.org/)
77 | .cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 |
86 | # Gatsby files
87 | .cache/
88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
89 | # https://nextjs.org/blog/next-9-1#public-directory-support
90 | # public
91 |
92 | # vuepress build output
93 | .vuepress/dist
94 |
95 | # Serverless directories
96 | .serverless/
97 |
98 | # FuseBox cache
99 | .fusebox/
100 |
101 | # DynamoDB Local files
102 | .dynamodb/
103 |
104 | # TernJS port file
105 | .tern-port
106 |
--------------------------------------------------------------------------------
/src/client/components/KnobPanel.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React, { useState, useCallback } from 'react';
3 | import styled from 'styled-components';
4 | // import { Dial } from 'react-nexusui';
5 | import Dial from './Knob';
6 |
7 | const Knobs = React.memo(({
8 | k1Val, k2Val, k3Val, k4Val,
9 | handleK1Change, handleK2Change, handleK3Change, handleK4Change,
10 | }) => {
11 | // console.log('Knobs, k1Val: ', k1Val, 'k2Val', k2Val);
12 |
13 | return (
14 |
15 |
16 |
26 |
27 |
28 |
38 |
39 |
40 |
50 |
51 |
52 |
62 |
63 |
64 | );
65 | });
66 |
67 | const KnobPanelWrapper = styled.div`
68 | display: flex;
69 | flex-direction: column;
70 | padding: 24px 0 24px 24px;
71 | border-left: 1px solid #444444;
72 | border-right: 1px solid #444444;
73 | border-bottom: 1px solid #444444;
74 | `;
75 |
76 | const DialWrapper = styled.div`
77 | margin: ${(props) => (props.alignRight ? '0px 0px 0px 40px' : '0px 0px 0px 0px')};
78 | `;
79 |
80 | export default Knobs;
81 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stepseq",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "webpack.config.js",
6 | "scripts": {
7 | "start": "cross-env NODE_ENV=production node src/server/server.js",
8 | "build": "cross-env NODE_ENV=production webpack",
9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open & nodemon src/server/server.js",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/csAudioApps/stepSeq.git"
15 | },
16 | "author": "",
17 | "license": "ISC",
18 | "bugs": {
19 | "url": "https://github.com/csAudioApps/stepSeq/issues"
20 | },
21 | "homepage": "https://github.com/csAudioApps/stepSeq#readme",
22 | "dependencies": {
23 | "@babel/plugin-transform-runtime": "^7.10.5",
24 | "@babel/runtime": "^7.10.5",
25 | "dompurify": "^2.0.12",
26 | "express": "^4.17.1",
27 | "nexusui": "^2.1.4",
28 | "prop-types": "^15.7.2",
29 | "react": "^16.13.1",
30 | "react-dom": "^16.13.1",
31 | "regenerator-runtime": "^0.13.7",
32 | "socket.io": "^2.3.0",
33 | "standardized-audio-context": "^24.1.22",
34 | "styled-components": "^5.2.0",
35 | "tone": "^14.7.32"
36 | },
37 | "devDependencies": {
38 | "@babel/core": "^7.10.5",
39 | "@babel/preset-env": "^7.10.4",
40 | "@babel/preset-react": "^7.10.4",
41 | "@babel/preset-typescript": "^7.10.4",
42 | "@types/react": "^16.9.43",
43 | "@types/react-dom": "^16.9.8",
44 | "babel-loader": "^8.1.0",
45 | "cross-env": "^7.0.2",
46 | "css-loader": "^3.6.0",
47 | "eslint": "^7.2.0",
48 | "eslint-config-airbnb": "^18.2.0",
49 | "eslint-plugin-import": "^2.22.0",
50 | "eslint-plugin-jsx-a11y": "^6.3.1",
51 | "eslint-plugin-react": "^7.20.6",
52 | "eslint-plugin-react-hooks": "^4.1.0",
53 | "file-loader": "^6.0.0",
54 | "html-webpack-plugin": "^4.3.0",
55 | "node-sass": "^4.14.1",
56 | "nodemon": "^2.0.4",
57 | "sass-loader": "^9.0.2",
58 | "source-map-loader": "^1.0.1",
59 | "style-loader": "^1.2.1",
60 | "ts-loader": "^8.0.1",
61 | "ts-node": "^8.10.2",
62 | "tslint": "^6.1.2",
63 | "typescript": "^3.9.7",
64 | "webpack": "^4.43.0",
65 | "webpack-cli": "^3.3.12",
66 | "webpack-dev-server": "^3.11.0"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/sampleData.js:
--------------------------------------------------------------------------------
1 |
2 | state = {
3 | // global state
4 | instruments = [
5 | { name: "Drums", soundPreset: "BasicDrumset", mono: null, legato: false, grid:
6 | [
7 | [0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
8 | [0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
9 | [0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
10 | [0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
11 | [0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
12 | [1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
13 | [1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
14 | [1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
15 | ],
16 | },
17 |
18 | {
19 | name: "Bass", soundPreset: "ClassicBassSynth", mono: true, legato: true, grid:
20 | [ [2], [3], [4], [], [0], [], [], [], [], [2], [], [0], [], [0], [1], [2] ]
21 | },
22 | { name: "Synth1", soundPreset: "SpaceySynth", mono: false, legato: false, grid:
23 | [ [2, 5], [3, 6], [4, 6], [], [0, 6], [], [], [], [], [2, 5], [], [0, 6], [], [0], [1, 3], [2, 5] ]
24 | },
25 | ],
26 |
27 | status = {
28 | secsRemaining: 257,
29 | bpm: 120,
30 | isPlaying: true,
31 | },
32 |
33 | users = [
34 | { userId: 1, userName: 'Tom L', instrumentSelected: 2, color: 'blue'},
35 | { userId: 2, userName: 'Matt Gin', instrumentSelected: 2, color: 'green'},
36 | ],
37 |
38 | localState = {
39 | localUserId: 1,
40 | localScale: 0,
41 | // localOctaveStart: 'C2'
42 | }
43 | }
44 |
45 | const scales = [
46 | ['C', 'D', 'E', 'F', 'G', 'A', 'B'],
47 | ['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb'],
48 | ['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'B'],
49 | ['C', 'D', 'E', 'F#', 'G', 'A', 'B'],
50 | ['C', 'D', 'E', 'F', 'G', 'A', 'Bb'],
51 | ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb'],
52 | ['C', 'Db', 'Eb', 'F', 'Gb', 'Aa', 'Bb'],
53 | ],
54 |
55 |
56 | // { name: "Bass", soundPreset: "ClassicBassSynth", mono: true, legato: true, grid:
57 | // [
58 | // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
59 | // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
60 | // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
61 | // [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
62 | // [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
63 | // [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
64 | // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
65 | // [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0],
66 | // ],
--------------------------------------------------------------------------------
/src/client/components/InstrumentColumn.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/control-has-associated-label */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
4 | import PropTypes from 'prop-types';
5 | import React from 'react';
6 | import styled from 'styled-components';
7 | import * as reducerConstants from '../reducer/reducerConstants';
8 |
9 | const InstrumentColumn = React.memo(({
10 | instruments, dispatch, localUserId, selectedInstr,
11 | }) => (
12 |
13 |
14 | {
15 | instruments
16 | ? instruments.map((item, i) => (
17 |
18 | {i + 1}
19 |
20 | dispatch({
25 | type: reducerConstants.SET_SELECTED_INSTRUMENT,
26 | payload: { localUserId, instrumentSelected: i },
27 | })}
28 | >
29 | {instruments[i].name}
30 |
31 |
32 | { selectedInstr === i ? : null }
33 |
34 | ))
35 | : Loading...
36 | }
37 |
38 |
39 | ));
40 |
41 | InstrumentColumn.propTypes = {
42 | instruments: PropTypes.arrayOf(PropTypes.shape({
43 | name: PropTypes.string.isRequired,
44 | soundPreset: PropTypes.string,
45 | mono: PropTypes.bool,
46 | legato: PropTypes.bool,
47 | grid: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)).isRequired,
48 | })).isRequired,
49 | dispatch: PropTypes.func.isRequired,
50 | localUserId: PropTypes.string.isRequired,
51 | selectedInstr: PropTypes.number.isRequired,
52 | };
53 |
54 | const InstrumentColumnWrapper = styled.div`
55 | display: flex;
56 | flex-direction: column;
57 | padding: 24px 20px 0 20px;
58 | width: 16em;
59 | flex-grow: 2;
60 | border-left: 1px solid #444444;
61 | border-right: 1px solid #444444;
62 | border-bottom: 1px solid #444444;
63 | `;
64 |
65 | const InstrumentItemWrapper = styled.div`
66 | display: flex;
67 | `;
68 |
69 | const InstrumentNumber = styled.span`
70 | color: grey;
71 | margin: auto 0.6em auto 0;
72 | font-size: 0.7em;
73 | `;
74 |
75 | const UserColorSwatch = styled.span`
76 | background-color: #5dfdcb;
77 | height: 12px;
78 | width: 12px;
79 | border-radius: 50%;
80 | display: inline-block;
81 | margin: auto 0;
82 | margin-left: auto;
83 | `;
84 |
85 | const InstrumentButton = styled.button`
86 | cursor: pointer;
87 | margin: 10px 0px;
88 | background-color: transparent;
89 | font-weight: 200;
90 | font-size: 0.85em;
91 | letter-spacing: 0.25em;
92 | color: #bbbbbb;
93 | ${'' /* font-weight: ${(props) => (props.isSelected ? 700 : 200)}; */}
94 | `;
95 |
96 | export default InstrumentColumn;
97 |
--------------------------------------------------------------------------------
/src/client/components/InlineEdit.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable object-curly-newline */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 | /* eslint-disable jsx-a11y/no-static-element-interactions */
4 | import React, { useState, useEffect, useRef, useCallback } from 'react';
5 | import { func, string } from 'prop-types';
6 | import DOMPurify from 'dompurify';
7 | import styled from 'styled-components';
8 | import useKeypress from '../hooks/useKeypress';
9 | import useOnClickOutside from '../hooks/useOnClickOutside';
10 |
11 | const InlineEdit = ({ text, onSetText }) => {
12 | // console.log('InlineEdit -> text', text);
13 | // console.log('InlineEdit -> onSetText', onSetText);
14 | const [isInputActive, setIsInputActive] = useState(false);
15 | const [inputValue, setInputValue] = useState(text);
16 |
17 | const wrapperRef = useRef(null);
18 | const textRef = useRef(null);
19 | const inputRef = useRef(null);
20 |
21 | const enter = useKeypress('Enter');
22 | const esc = useKeypress('Escape');
23 |
24 | // check to see if the user clicked outside of this component
25 | useOnClickOutside(wrapperRef, () => {
26 | // console.log('OUTSIDE CLICK');
27 | if (isInputActive) {
28 | onSetText(inputValue);
29 | setIsInputActive(false);
30 | }
31 | });
32 |
33 | const onEnter = useCallback(() => {
34 | // console.log('ENTER');
35 | if (enter) {
36 | onSetText(inputValue);
37 | setIsInputActive(false);
38 | }
39 | }, [enter, inputValue, onSetText]);
40 |
41 | const onEsc = useCallback(() => {
42 | // console.log('ESCAPE');
43 | if (esc) {
44 | setInputValue(text);
45 | setIsInputActive(false);
46 | }
47 | }, [esc, text]);
48 |
49 | // focus the cursor in the input field on edit start
50 | useEffect(() => {
51 | if (isInputActive) {
52 | inputRef.current.focus();
53 | }
54 | }, [isInputActive]);
55 |
56 | useEffect(() => {
57 | if (isInputActive) {
58 | // if Enter is pressed, save the text and close the editor
59 | onEnter();
60 | // if Escape is pressed, revert the text and close the editor
61 | onEsc();
62 | }
63 | }, [onEnter, onEsc, isInputActive]); // watch the Enter and Escape key presses
64 |
65 | const handleInputChange = useCallback(
66 | (event) => {
67 | // sanitize the input a little
68 | const sanitizedText = DOMPurify.sanitize(event.target.value);
69 | // console.log('InlineEdit -> sanitizedText', sanitizedText);
70 |
71 | setInputValue(sanitizedText);
72 | },
73 | [setInputValue],
74 | );
75 |
76 | const handleSpanClick = useCallback(() => setIsInputActive(true), [
77 | setIsInputActive,
78 | ]);
79 |
80 | return (
81 |
82 |
87 | {text}
88 |
89 |
95 |
96 | );
97 | };
98 |
99 | export default InlineEdit;
100 |
101 | InlineEdit.propTypes = {
102 | text: string.isRequired,
103 | onSetText: func.isRequired,
104 | };
105 |
106 | const StyledInlineEdit = styled.span`
107 | background: #222222;
108 | padding: 0.3em;
109 | margin: auto 12px auto 0.1em;
110 | width: 2.7em;
111 | `;
112 |
113 | const StyledInputInactive = styled.span`
114 | display: ${(props) => (props.isVisible ? 'inline' : 'none')};
115 | `;
116 |
117 | const StyledActiveInput = styled.input`
118 | color: #bbbbbb;
119 | background: grey;
120 | width: 2.7em;
121 | text-align: inherit;
122 | letter-spacing: inherit;
123 | border: inherit;
124 | cursor: pointer;
125 | display: ${(props) => (props.isVisible ? 'inline' : 'none')};
126 | `;
127 |
--------------------------------------------------------------------------------
/src/client/constants/initState.js:
--------------------------------------------------------------------------------
1 | export const userInitState = {
2 | username: 'beatsmith519', instrumentSelected: 0, selectedScale: 0, color: 'red', isPlaying: false,
3 | };
4 |
5 | export const mainInitState = {
6 | instruments: [
7 | {
8 | name: 'Percussion Synth',
9 | soundPreset: 'BasicDrumset',
10 | mono: null,
11 | legato: false,
12 | grid:
13 | [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []],
14 | },
15 | {
16 | name: 'Classic 808',
17 | soundPreset: 'BasicDrumset',
18 | mono: null,
19 | legato: false,
20 | grid:
21 | [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []],
22 | },
23 | {
24 | name: 'Monobass',
25 | soundPreset: 'ClassicBassSynth',
26 | mono: true,
27 | legato: true,
28 | grid:
29 | [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []],
30 | },
31 | {
32 | name: 'Spacey Poly',
33 | soundPreset: 'SpaceySynth',
34 | mono: false,
35 | legato: false,
36 | grid:
37 | [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []],
38 | },
39 | {
40 | name: 'Meatwave',
41 | soundPreset: 'Meatwave',
42 | mono: false,
43 | legato: false,
44 | grid:
45 | [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []],
46 | },
47 | ],
48 |
49 | status: {
50 | secsRemaining: 300,
51 | tempo: 120,
52 | },
53 |
54 | users: {
55 |
56 | },
57 |
58 | local: {
59 | localUserId: '12345',
60 | seqLen: 16,
61 | // localOctaveStart: 'C2'
62 | },
63 | };
64 |
65 | // export default mainInitState;
66 |
67 | // eslint-disable-next-line no-unused-vars
68 | // const exampleState = {
69 | // instruments: [
70 | // {
71 | // name: 'Drums',
72 | // soundPreset: 'BasicDrumset',
73 | // mono: null,
74 | // legato: false,
75 | // grid:
76 | // [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []],
77 | // },
78 | // {
79 | // name: 'Bass',
80 | // soundPreset: 'ClassicBassSynth',
81 | // mono: true,
82 | // legato: true,
83 | // grid:
84 | // [[1], [1], [1], [1], [1], [1], [1], [], [], [], [], [], [], [], [], []],
85 | // },
86 | // {
87 | // name: 'Synth1',
88 | // soundPreset: 'SpaceySynth',
89 | // mono: false,
90 | // legato: false,
91 | // grid:
92 | // [[], [], [], [], [], [1, 2, 3, 4, 5, 6], [], [], [], [], [], [], [], [], [], []],
93 | // },
94 | // ],
95 |
96 | // status: {
97 | // secsRemaining: 300,
98 | // bpm: 120,
99 | // isPlaying: false,
100 | // },
101 |
102 | // users: {
103 | // wsxk943KJk: {
104 | // userName: '', instrumentSelected: 2, color: 'blue', selectedScale: 0, isPlaying: true,
105 | // },
106 | // asv543fgs: {
107 | // userName: '', instrumentSelected: 0, color: 'red', selectedScale: 0, isPlaying: true,
108 | // },
109 | // },
110 |
111 | // local: {
112 | // localUserId: 'wsxsk943KJk',
113 | // // localScale: 0,
114 | // seqLen: 16,
115 | // // localOctaveStart: 'C2'
116 | // },
117 | // };
118 |
119 | // export const initialState3 = {
120 | // instruments: [
121 | // {
122 | // name: 'Drums',
123 | // soundPreset: 'BasicDrumset',
124 | // mono: null,
125 | // legato: false,
126 | // grid:
127 | // [[2], [5], [7], [], [], [], [4], [], [], [], [], [], [], [], [], []],
128 | // },
129 | // {
130 | // name: 'Bass',
131 | // soundPreset: 'ClassicBassSynth',
132 | // mono: true,
133 | // legato: true,
134 | // grid:
135 | // [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []],
136 | // },
137 | // {
138 | // name: 'Synth1',
139 | // soundPreset: 'SpaceySynth',
140 | // mono: false,
141 | // legato: false,
142 | // grid:
143 | // [[], [], [], [], [], [1, 2, 3, 4, 5, 6], [], [], [], [], [], [], [], [], [], []],
144 | // },
145 | // ],
146 |
147 | // status: {
148 | // secsRemaining: 300,
149 | // bpm: 120,
150 | // isPlaying: true,
151 | // },
152 |
153 | // users: {
154 | // wsxk943KJk: {
155 | // userName: '', instrumentSelected: 2, color: 'blue', selectedScale: 0,
156 | // },
157 | // asv543fgs: {
158 | // userName: '', instrumentSelected: 0, color: 'red', selectedScale: 0,
159 | // },
160 | // },
161 |
162 | // local: {
163 | // localUserId: 'wsxk943KJk',
164 | // // localScale: 0,
165 | // seqLen: 16,
166 | // // localOctaveStart: 'C2'
167 | // },
168 | // };
169 |
--------------------------------------------------------------------------------
/src/client/reducer/reducer.jsx:
--------------------------------------------------------------------------------
1 | import * as Tone from 'tone';
2 | import * as reducerConstants from './reducerConstants';
3 | import { socket } from '../helpers/socket';
4 | import scales from '../constants/scales';
5 |
6 | const reducer = (state, action) => {
7 | // console.log('reducing, state: ', state);
8 | // console.log('reducing, action: ', action);
9 | let newState;
10 | let instrumentSelected;
11 | // console.log('action.type: ', action.type);
12 | switch (action.type) {
13 | case reducerConstants.SET_LOCAL_USERID: {
14 | return {
15 | ...state,
16 | local: {
17 | ...state.local,
18 | localUserId: action.payload,
19 | },
20 | };
21 | }
22 | //
23 | case reducerConstants.SET_NEW_USER: {
24 | return {
25 | ...state,
26 | };
27 | }
28 | // payload = state object
29 | case reducerConstants.SET_STATE_FROM_SOCKET: {
30 | return {
31 | ...action.payload,
32 | local: state.local,
33 | };
34 | }
35 | // payload = user object {string: {username, instrument...}}
36 | case reducerConstants.ADD_USER: {
37 | newState = {
38 | ...state,
39 | users: {
40 | ...state.users,
41 | ...action.payload,
42 | },
43 | local: {
44 | ...state.local,
45 | localUserId: Object.keys(action.payload)[0],
46 | },
47 | };
48 | socket.emit('updateServerState', newState, socket.id);
49 | return newState;
50 | }
51 | // payload = userId: string
52 | case reducerConstants.REMOVE_USER: {
53 | const newUsers = { ...state.users };
54 | delete newUsers[action.payload];
55 |
56 | return {
57 | ...state,
58 | users: newUsers,
59 | };
60 | }
61 | // payoad: object with x and y coordinates of grid button: {x: number, y: number}
62 | case reducerConstants.TOGGLE_GRID_BUTTON: {
63 | // make copy of grid at user's selected instrument
64 | instrumentSelected = state.users[state.local.localUserId].instrumentSelected;
65 | const newGrid = [...state.instruments[instrumentSelected].grid];
66 |
67 | // if value is included in grid, remove, else add it and sort array
68 | const currentColumn = newGrid[action.payload.x];
69 | const newColumn = currentColumn.includes(action.payload.y)
70 | ? currentColumn.filter((num) => num !== action.payload.y)
71 | : currentColumn.concat(action.payload.y).sort((a, b) => a - b);
72 |
73 | // set column into new grid
74 | newGrid[action.payload.x] = newColumn;
75 |
76 | // set new grid into instrument list
77 | const newInstruments = [...state.instruments];
78 | newInstruments[instrumentSelected].grid = newGrid;
79 |
80 | // return state with new intrument list
81 | newState = {
82 | ...state,
83 | instruments: newInstruments,
84 | };
85 | socket.emit('updateServerState', newState, socket.id);
86 | return newState;
87 | }
88 |
89 | case reducerConstants.TOGGLE_PLAY_STATE: {
90 | // const isCurrentlyPlaying = state.users[action.payload.localUserId].isPlaying;
91 | // console.log('Tone.Context.');
92 | // if (!isCurrentlyPlaying) {
93 | // Tone.Transport.start();
94 | // }
95 | // else {
96 | // Tone.Transport.toggle();
97 | // // Tone.Transport.pause();
98 | // }
99 |
100 | newState = {
101 | ...state,
102 | users: {
103 | ...state.users,
104 | [action.payload.localUserId]: {
105 | ...state.users[action.payload.localUserId],
106 | isPlaying: !state.users[action.payload.localUserId].isPlaying,
107 | },
108 | },
109 | };
110 | socket.emit('updateServerState', newState, socket.id);
111 | return newState;
112 | }
113 |
114 | case reducerConstants.SET_SELECTED_INSTRUMENT: {
115 | let numSelected = action.payload.instrumentSelected;
116 | if (numSelected < state.instruments.length) {
117 | numSelected = action.payload.instrumentSelected;
118 | }
119 | else {
120 | return state;
121 | }
122 |
123 | newState = {
124 | ...state,
125 | users: {
126 | ...state.users,
127 | [action.payload.localUserId]: {
128 | ...state.users[action.payload.localUserId],
129 | instrumentSelected: numSelected,
130 | },
131 | },
132 | };
133 | socket.emit('updateServerState', newState, socket.id);
134 | return newState;
135 | }
136 | case reducerConstants.SET_SELECTED_SCALE: {
137 | console.log('in reducer->set selected scale');
138 | console.log(action.payload);
139 | let numSelected = action.payload.selectedScale;
140 | if (numSelected < scales.length) {
141 | numSelected = action.payload.selectedScale;
142 | }
143 | else {
144 | return state;
145 | }
146 |
147 | newState = {
148 | ...state,
149 | users: {
150 | ...state.users,
151 | [action.payload.localUserId]: {
152 | ...state.users[action.payload.localUserId],
153 | selectedScale: action.payload.selectedScale,
154 | },
155 | },
156 | };
157 | console.log('reducer -> set selected scale', newState);
158 |
159 | // TODO This is gonna break - other users need to know what scale you're using
160 | socket.emit('updateServerState', newState, socket.id);
161 | return newState;
162 | }
163 |
164 | case reducerConstants.SET_TEMPO: {
165 | Tone.Transport.bpm.value = action.payload.newTempo;
166 | console.log('reducer -> Tone.Transport.bpm.value', Tone.Transport.bpm.value);
167 | newState = {
168 | ...state,
169 | status: {
170 | tempo: action.payload.newTempo,
171 | },
172 | };
173 | socket.emit('updateServerState', newState, socket.id);
174 | return newState;
175 | }
176 |
177 | default: {
178 | throw new Error('Error: invalid reducer action type');
179 | }
180 | }
181 | };
182 |
183 | export default reducer;
184 |
--------------------------------------------------------------------------------
/src/client/containers/MainContainer.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable object-curly-newline */
2 | /* eslint-disable no-multi-spaces */
3 | /* eslint-disable consistent-return */
4 | import React, { useState, useEffect, useRef, useReducer, useCallback } from 'react';
5 | import * as Tone from 'tone';
6 | import styled from 'styled-components';
7 | import HeaderContainer from './HeaderContainer';
8 | import ControlBar from '../components/ControlBar';
9 | import KnobPanel from '../components/KnobPanel';
10 | import InstrumentColumn from '../components/InstrumentColumn';
11 | import Board from '../components/Board';
12 | import Footer from '../components/Footer';
13 | import { socket } from '../helpers/socket';
14 | import reducer from '../reducer/reducer';
15 | import * as types from '../reducer/reducerConstants';
16 | import { updateNoteArray, toggleToneTransport } from '../helpers/audioHelpers';
17 | import { mainInitState, userInitState } from '../constants/initState';
18 | import { soundPresets } from '../constants/soundPresets';
19 |
20 | const MainContainer = () => {
21 | const [state, dispatch] = useReducer(reducer, mainInitState);
22 | const [step, setStep] = useState(0);
23 | // const [position, setPosition] = useState('0:0:0');
24 |
25 | const { users, local, instruments } = state;
26 | const { localUserId } = local;
27 | const selectedScale = (users[localUserId]) ? users[localUserId].selectedScale : 0;
28 | const selectedInstr = (users[localUserId]) ? users[localUserId].instrumentSelected : 1;
29 | const username = (users[localUserId]) ? users[localUserId].username : '';
30 | const gridForCurInstr = (users[localUserId]) ? instruments[selectedInstr].grid : [[]];
31 | const isPlaying = (users[localUserId]) ? users[localUserId].isPlaying : false;
32 |
33 | const transport = useRef(null);
34 | const bassSynth = useRef(null);
35 | const drumSynth = useRef(null);
36 | const dly = useRef(null);
37 | const dist = useRef(null);
38 |
39 | const [k1Val, setk1Val] = useState(0.7);
40 | const [k2Val, setk2Val] = useState(0.5);
41 | const [k3Val, setk3Val] = useState(0.6);
42 | const [k4Val, setk4Val] = useState(0.5);
43 |
44 | // BASS VOLUME KNOB
45 | const handleK1Change = useCallback((val) => { setk1Val(val); }, []);
46 | useEffect(() => {
47 | if (bassSynth && bassSynth.current) {
48 | bassSynth.current.volume.value = (Math.log(k1Val) * 8).toFixed(2);
49 | }
50 | }, [k1Val]);
51 |
52 | // BASS DISTORTION KNOB
53 | const handleK2Change = useCallback((val) => { setk2Val(val); }, []);
54 | useEffect(() => { if (dist && dist.current) { dist.current.distortion = k2Val; } }, [k2Val]);
55 |
56 | // BASS DELAY FEEDBACK KNOB
57 | const handleK3Change = useCallback((val) => { setk3Val(val); }, []);
58 | useEffect(() => { if (dly && dly.current) { dly.current.feedback.value = k3Val; } }, [k3Val]);
59 |
60 | // BASS DELAY MIX KNOB
61 | const handleK4Change = useCallback((val) => { setk4Val(val); }, []);
62 | useEffect(() => { if (dly && dly.current) { dly.current.wet.value = k4Val; } }, [k4Val]);
63 |
64 | // open socket connection
65 | useEffect(() => {
66 | // get initial state
67 | console.log('socket: ', socket);
68 | socket.emit('getInitialState');
69 | socket.on('updateClientState', (msg, senderId) => {
70 | if (socket.id !== senderId) {
71 | dispatch({ type: types.SET_STATE_FROM_SOCKET, payload: msg });
72 | }
73 | else {
74 | // console.log('they are equal')
75 | }
76 | });
77 | return () => socket.disconnect();
78 | }, []);
79 |
80 | // Add User
81 | useEffect(() => {
82 | if (!Object.keys(users).includes(localUserId)) {
83 | dispatch({ type: types.ADD_USER, payload: { [localUserId]: userInitState } });
84 | }
85 | }, [users, localUserId]);
86 |
87 | // Initial (one-time) Tone and Tranpost Setup
88 | useEffect(() => {
89 | Tone.Context.latencyHint = 'playback';
90 | Tone.Transport.bpm.value = state.status.tempo ? state.status.tempo : 120;
91 | transport.current = new Tone.Sequence((time, curStep) => {
92 | setStep(curStep);
93 | }, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], '8n').start(0);
94 |
95 | return () => {
96 | transport.current.dispose();
97 | };
98 | }, []);
99 |
100 | // Set up Bass synth
101 | useEffect(() => {
102 | dly.current = new Tone.FeedbackDelay('8n', k3Val).toDestination();
103 | dly.current.wet.value = k4Val;
104 | dist.current = new Tone.Distortion(k2Val).connect(dly.current);
105 | bassSynth.current = new Tone.Synth({ volume: k1Val }).connect(dist.current);
106 |
107 | return () => {
108 | bassSynth.current.dispose();
109 | dist.current.dispose();
110 | dly.current.dispose();
111 | };
112 | }, []);
113 |
114 | // Set up Percussion synth
115 | useEffect(() => {
116 | drumSynth.current = new Tone.MembraneSynth({ volume: -12 }).toDestination();
117 | return () => { drumSynth.current.dispose(); };
118 | }, []);
119 |
120 | // Set up and handle changes to SEQUENCE for Bass
121 | useEffect(() => {
122 | if (bassSynth && bassSynth.current) {
123 | const bassNoteArr = updateNoteArray(instruments[2].grid, selectedScale, 2);
124 | const bassSynthSeq = new Tone.Sequence((time, note) => {
125 | bassSynth.current.triggerAttackRelease(note, '8n', time);
126 | }, bassNoteArr).start(0);
127 |
128 | return () => bassSynthSeq.dispose();
129 | }
130 | return null;
131 | }, [instruments, selectedScale]);
132 |
133 | // Set up and handle changes to SEQUENCE for Percussion Synth
134 | useEffect(() => {
135 | if (drumSynth && drumSynth.current) {
136 | const drumNoteArr = updateNoteArray(instruments[0].grid, selectedScale, 0);
137 | const drumSynthSeq = new Tone.Sequence((time, note) => {
138 | drumSynth.current.triggerAttackRelease(note, '8n', time);
139 | }, drumNoteArr).start(0);
140 |
141 | return () => drumSynthSeq.dispose();
142 | }
143 | return null;
144 | }, [instruments, selectedScale]);
145 |
146 | // If user changes tempo, update delayTime
147 | useEffect(() => {
148 | console.log('in useEffect delay update');
149 | dly.current.delayTime.value = '8n';
150 | }, [state.status.tempo]);
151 |
152 | const handleUserKeyPress = useCallback((event) => {
153 | const { code, altKey } = event;
154 | switch (code) {
155 | case 'Space': {
156 | if (toggleToneTransport()) {
157 | dispatch({ type: types.TOGGLE_PLAY_STATE, payload: { localUserId } });
158 | }
159 | break;
160 | }
161 | case 'Digit1':
162 | case 'Digit2':
163 | case 'Digit3':
164 | case 'Digit4':
165 | case 'Digit5':
166 | case 'Digit6':
167 | case 'Digit7':
168 | case 'Digit8':
169 | case 'Digit9':
170 | case 'Digit0': {
171 | const lastDigit = Number(code[code.length - 1]);
172 | const selectedIndex = lastDigit === 0 ? 10 : lastDigit - 1;
173 | if (altKey === true) { // Numbers + alt key change scale
174 | dispatch({
175 | type: types.SET_SELECTED_SCALE,
176 | payload: { localUserId, selectedScale: selectedIndex },
177 | });
178 | }
179 | else { // Numbers alone change instrument
180 | dispatch({
181 | type: types.SET_SELECTED_INSTRUMENT,
182 | payload: { localUserId, instrumentSelected: selectedIndex },
183 | });
184 | }
185 | break;
186 | }
187 | default:
188 | break;
189 | }
190 | }, [localUserId]);
191 |
192 | // Add keyboard event listener
193 | useEffect(() => {
194 | window.addEventListener('keydown', handleUserKeyPress);
195 | return () => { window.removeEventListener('keydown', handleUserKeyPress); };
196 | }, [handleUserKeyPress]);
197 |
198 | return (
199 |
200 |
204 |
205 |
212 |
213 |
214 |
224 |
230 |
231 |
238 |
239 |
240 |
241 |
242 | );
243 | };
244 |
245 | const MainWrapper = styled.div`
246 | margin-top: 50px;
247 | `;
248 |
249 | const StyledMainContainer = styled.div`
250 | display: flex;
251 | flex-direction: column;
252 | align-items: center;
253 | height: 100vh;
254 | width: 100vw;
255 |
256 | `;
257 |
258 | const StyledRow = styled.div`
259 | display: flex;
260 | flex-grow: 1;
261 | `;
262 |
263 | const StyledCol = styled.div`
264 | display: flex;
265 | flex-direction: column;
266 | `;
267 |
268 | export default MainContainer;
269 |
270 | // if we are first user, create state in the server
271 | // socket.on('firstUser', () => {
272 | // console.log('first')
273 | // socket.emit('updateServerState', state, socket.id);
274 | // })
275 | // when we receive and updated state from server, update local state
276 | // only update if update was sent fron another user
277 |
278 | // Update Transport Position Display
279 | // useEffect(() => {
280 | // const id = Tone.Transport.scheduleRepeat(() => {
281 | // const curPosition = Tone.Transport.position.toString().split('.')[0];
282 | // console.log('Updating position -> curPosition', curPosition);
283 | // setPosition(curPosition);
284 | // }, '8n');
285 |
286 | // return () => { Tone.Transport.clear(id); };
287 | // }, []);
288 |
--------------------------------------------------------------------------------