├── .env
├── assets
├── icon.png
└── favicon.ico
├── public
├── assets
│ ├── logo.png
│ ├── favicon.ico
│ └── apple-touch-icon.png
├── const.js
├── rendererApi
│ ├── path.js
│ ├── dialog.js
│ ├── store.js
│ ├── wav.js
│ ├── fs.js
│ └── midi.js
├── events
│ ├── events.js
│ ├── mainProcessTriggers.js
│ ├── mainProcessCallbacks.js
│ ├── rendererProcessEvents.js
│ └── mainProcessEvents.js
├── manifest.json
├── README.md
├── index.html
├── preload.js
├── electron.js
└── mainApi
│ └── menu.js
├── src
├── README.md
├── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ └── glyphicons-halflings-regular.woff2
├── util
│ ├── buffer.js
│ ├── fileDialog.js
│ ├── storage.js
│ ├── sampleStore.js
│ └── kitFile.js
├── css
│ ├── EditKit.css
│ ├── EditKit.scss
│ ├── Pad
│ │ ├── Velocity.scss
│ │ ├── Velocity.css
│ │ ├── MuteGroup.css
│ │ ├── MuteGroup.scss
│ │ ├── Control.scss
│ │ └── Control.css
│ ├── Notice.css
│ ├── Notice.scss
│ ├── index.css
│ ├── index.scss
│ ├── SampleList.css
│ ├── SampleList.scss
│ ├── Pad.css
│ ├── Pad.scss
│ └── icons.css
├── state
│ ├── store.js
│ ├── sortModels.js
│ ├── globalState.js
│ ├── models.js
│ └── reducers.js
├── menu
│ ├── edit.js
│ ├── midi.js
│ └── deviceType.js
├── actions
│ ├── notice.js
│ ├── app.js
│ ├── modal.js
│ ├── drive.js
│ ├── pad.js
│ └── kit.js
├── index.js
├── component
│ ├── Editor
│ │ ├── Samplerack.js
│ │ └── SamplepadPro.js
│ ├── Pad
│ │ ├── Row.js
│ │ ├── MidiNoteSelect.js
│ │ ├── PadName.js
│ │ ├── KnobControl.js
│ │ ├── SampleDropTarget.js
│ │ ├── Velocity.js
│ │ ├── SlideControl.js
│ │ ├── MuteGroup.js
│ │ ├── LayerB.js
│ │ └── LayerA.js
│ ├── Header.js
│ ├── KitList.js
│ ├── Notice.js
│ ├── Sample.js
│ ├── App.js
│ ├── EditKit.js
│ ├── SamplePlayer.js
│ ├── Modal.js
│ └── SampleList.js
└── const.js
├── docs
├── SamplePad Kit Editor v1.png
├── SamplePad Kit Editor v2.png
├── SamplePad Kit Editor v6.png
└── kit-format-notes.txt
├── craco.config.js
├── LICENSE
├── .gitignore
├── package.json
└── README.md
/.env:
--------------------------------------------------------------------------------
1 | NODE_PATH='src/'
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/assets/favicon.ico
--------------------------------------------------------------------------------
/public/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/public/assets/logo.png
--------------------------------------------------------------------------------
/src/README.md:
--------------------------------------------------------------------------------
1 | # Src Directory
2 | This directory holds all classes necessary for the renderer electron process.
--------------------------------------------------------------------------------
/public/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/public/assets/favicon.ico
--------------------------------------------------------------------------------
/docs/SamplePad Kit Editor v1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/docs/SamplePad Kit Editor v1.png
--------------------------------------------------------------------------------
/docs/SamplePad Kit Editor v2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/docs/SamplePad Kit Editor v2.png
--------------------------------------------------------------------------------
/docs/SamplePad Kit Editor v6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/docs/SamplePad Kit Editor v6.png
--------------------------------------------------------------------------------
/public/assets/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/public/assets/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/const.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | DeviceType: {
3 | SAMPLEPAD_PRO: 'samplepad_pro',
4 | SAMPLERACK: 'samplerack'
5 | }
6 | }
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack: {
3 | configure: {
4 | target: 'electron-renderer'
5 | }
6 | }
7 | };
--------------------------------------------------------------------------------
/src/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/src/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/src/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/src/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/src/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/src/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/src/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LesserChance/samplepad-editor/HEAD/src/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/public/rendererApi/path.js:
--------------------------------------------------------------------------------
1 | /* Electron imports */
2 | const path = require('path')
3 |
4 | module.exports = {
5 | parse: (fileName) => {
6 | return path.parse(fileName)
7 | }
8 | }
--------------------------------------------------------------------------------
/src/util/buffer.js:
--------------------------------------------------------------------------------
1 | /* Electron imports */
2 | const { fs } = window.api
3 |
4 | export const getBuffer = (filePath) => {
5 | return Buffer.from(fs.readFileBufferArray(filePath));
6 | }
--------------------------------------------------------------------------------
/public/rendererApi/dialog.js:
--------------------------------------------------------------------------------
1 | /* Electron imports */
2 | const { remote } = require("electron")
3 |
4 | module.exports = {
5 | showOpenDialog: (options) => {
6 | return remote.dialog.showOpenDialog(options)
7 | }
8 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This work is licensed under the Creative Commons Attribution 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
--------------------------------------------------------------------------------
/src/css/EditKit.css:
--------------------------------------------------------------------------------
1 | .kit {
2 | margin: .8em 0 0 0; }
3 |
4 | .kitHeader {
5 | margin: 1em 0 .7em 0 !important; }
6 |
7 | .pad-table {
8 | max-height: 33.5em;
9 | overflow-y: scroll;
10 | padding-right: .2em;
11 | padding-bottom: 2em; }
12 |
--------------------------------------------------------------------------------
/src/css/EditKit.scss:
--------------------------------------------------------------------------------
1 | .kit {
2 | margin: .8em 0 0 0;
3 | }
4 | .kitHeader {
5 | margin: 1em 0 .7em 0 !important;
6 | }
7 | .pad-table {
8 | max-height: 33.5em;
9 | overflow-y: scroll;
10 | padding-right: .2em;
11 | padding-bottom: 2em;
12 | }
13 |
--------------------------------------------------------------------------------
/public/events/events.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | SELECT_MIDI_INPUT: 'SELECT_MIDI_INPUT',
3 | SELECT_MIDI_SCAN: 'SELECT_MIDI_SCAN',
4 | GENERATE_MIDI_MENU: 'GENERATE_MIDI_MENU',
5 | SELECT_DEVICE_TYPE: 'SELECT_DEVICE_TYPE',
6 | SET_DEVICE_TYPE: 'SET_DEVICE_TYPE',
7 | LOAD_SD_CARD: 'LOAD_SD_CARD'
8 | }
--------------------------------------------------------------------------------
/public/rendererApi/store.js:
--------------------------------------------------------------------------------
1 | /* Electron imports */
2 | const Store = require('electron-store')
3 |
4 | const store = new Store()
5 |
6 | module.exports = {
7 | get: (param) => {
8 | return store.get(param)
9 | },
10 | save: (param, val) => {
11 | return store.set(param, val)
12 | }
13 | }
--------------------------------------------------------------------------------
/src/css/Pad/Velocity.scss:
--------------------------------------------------------------------------------
1 | .velocityContainer {
2 | width: 5em;
3 | text-align: center;
4 |
5 | input {
6 | width: 1.8em;
7 | padding: .2em 0;
8 | height: 1.5em;
9 | }
10 | &.has-tooltip-bottom::before {
11 | width: 11em;
12 | white-space: normal !important;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/css/Pad/Velocity.css:
--------------------------------------------------------------------------------
1 | .velocityContainer {
2 | width: 5em;
3 | text-align: center; }
4 | .velocityContainer input {
5 | width: 1.8em;
6 | padding: .2em 0;
7 | height: 1.5em; }
8 | .velocityContainer.has-tooltip-bottom::before {
9 | width: 11em;
10 | white-space: normal !important; }
11 |
--------------------------------------------------------------------------------
/src/state/store.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import { createStore, applyMiddleware } from 'redux'
3 | import thunk from 'redux-thunk';
4 |
5 | /* App imports */
6 | import reducers from 'state/reducers'
7 |
8 | /* Initalize React App */
9 | const store = createStore(reducers, applyMiddleware(thunk));
10 |
11 | export default store;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 |
4 | # production
5 | /build
6 | /dist
7 |
8 | # misc
9 | package-lock.json
10 | yarn.lock
11 | .DS_Store
12 | .env.local
13 | .env.development.local
14 | .env.test.local
15 | .env.production.local
16 | .tern-project
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "SamplePad Kit Editorr",
3 | "name": "SamplePad Kit Editor",
4 | "icons": [
5 | {
6 | "src": "assets/favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | }
14 |
--------------------------------------------------------------------------------
/src/css/Notice.css:
--------------------------------------------------------------------------------
1 | .Notice {
2 | position: absolute;
3 | width: 100%;
4 | top: 0;
5 | z-index: 10;
6 | -webkit-box-shadow: 15px 5px 15px 0px rgba(0, 0, 0, 0.3);
7 | -moz-box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.3);
8 | box-shadow: 5px 5px 15px 0px rgba(0, 0, 0, 0.3); }
9 | .Notice.hidden {
10 | transition: transform .5s;
11 | transform: translateY(-5em); }
12 |
--------------------------------------------------------------------------------
/src/css/Notice.scss:
--------------------------------------------------------------------------------
1 | .Notice {
2 | position: absolute;
3 | width: 100%;
4 | top: 0;
5 | z-index:10;
6 |
7 | -webkit-box-shadow: 15px 5px 15px 0px rgba(0,0,0,0.3);
8 | -moz-box-shadow: 5px 5px 15px 0px rgba(0,0,0,0.3);
9 | box-shadow: 5px 5px 15px 0px rgba(0,0,0,0.3);
10 |
11 | &.hidden {
12 | transition: transform .5s;
13 | transform: translateY(-5em);
14 | }
15 | }
--------------------------------------------------------------------------------
/src/css/index.css:
--------------------------------------------------------------------------------
1 | @import url(bulma.min.css);
2 | @import url(bulma-tooltip.min.css);
3 | @import url(icons.css);
4 | button.link {
5 | background: none;
6 | color: inherit;
7 | border: none;
8 | font: inherit;
9 | cursor: pointer;
10 | outline: inherit; }
11 |
12 | .splash p {
13 | margin: auto;
14 | max-width: 50%;
15 | padding-top: 2em;
16 | text-align: center; }
17 |
--------------------------------------------------------------------------------
/src/css/index.scss:
--------------------------------------------------------------------------------
1 | // use buttons as links, but remove all button-like style
2 | button.link {
3 | background: none;
4 | color: inherit;
5 | border: none;
6 | font: inherit;
7 | cursor: pointer;
8 | outline: inherit;
9 | }
10 |
11 | .splash p {
12 | margin: auto;
13 | max-width: 50%;
14 | padding-top: 2em;
15 | text-align: center;
16 | }
17 |
18 | @import 'bulma.min.css';
19 | @import 'bulma-tooltip.min.css';
20 | @import 'icons.css';
21 |
--------------------------------------------------------------------------------
/src/menu/edit.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import store from 'state/store'
3 | import { selectAndLoadDrive } from 'actions/drive'
4 |
5 | /* Electron imports */
6 | const { mainProcessCallbacks } = window.api
7 |
8 | /**
9 | * Initialize the renderer process handlers for the menu
10 | */
11 | export const initEditMenu = () => {
12 | mainProcessCallbacks.setLoadSDCardCallback(loadSDCard)
13 | }
14 |
15 | /**
16 | * "Load SD Card" has been selected from the menu
17 | */
18 | export const loadSDCard = () => {
19 | store.dispatch(selectAndLoadDrive());
20 | }
--------------------------------------------------------------------------------
/src/actions/notice.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { Actions } from 'const';
3 | import { NoticeModel } from 'state/models';
4 |
5 | /** NOTICE ACTION CREATORS */
6 | /**
7 | * Show a temporary notice across the header
8 | * @param {String} style - classname for thie notice (is-success, is-warning, is-danger, ...)
9 | * @param {String} text - the text to show in the notice
10 | */
11 | export function showNotice(style, text) {
12 | return (dispatch, getState) => {
13 | dispatch({ type: Actions.SHOW_NOTICE, notice: NoticeModel(style, text)});
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/public/README.md:
--------------------------------------------------------------------------------
1 | # Public Directory
2 | This directory holds all classes necessary for the main electron process.
3 |
4 | **rendererApi**
5 | This directory holds all classes that expose all native node and electron APIs to the renderer process. It is expected that any access to electron apis would need to use remote.
6 |
7 | **mainApi**
8 | This directory holds all classes that expose all native electron APIs to the main process. It is expected that any access to electron apis can be used directly.
9 |
10 | **events**
11 | This directory holds all classes that expose channels for communication between the main and renderer processes
--------------------------------------------------------------------------------
/public/events/mainProcessTriggers.js:
--------------------------------------------------------------------------------
1 | const Events = require("./events")
2 |
3 | /**
4 | * This class is responsible for the main process receiving events
5 | * from the renderer process
6 | *
7 | * context: renderer
8 | */
9 | module.exports = {
10 | generateMidiMenu: (inputList, currentMidiInput) => {
11 | window.postMessage({
12 | type: Events.GENERATE_MIDI_MENU,
13 | midiInputs: inputList,
14 | currentMidiInput: currentMidiInput
15 | })
16 | },
17 |
18 | setDeviceType: (deviceType) => {
19 | window.postMessage({
20 | type: Events.SET_DEVICE_TYPE,
21 | deviceType: deviceType
22 | })
23 | }
24 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | SamplePad Kit Editor
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/actions/app.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { Actions } from 'const';
3 | import { loadKitDetails } from 'actions/kit';
4 |
5 | /** APP ACTION CREATORS */
6 | /**
7 | * set the dropdown's selected kit and set it as currently active
8 | * @param {String} kitId
9 | */
10 | export function selectKit(kitId) {
11 | return (dispatch, getState) => {
12 | // if the kit isnt loaded, load it before activating it
13 | dispatch(loadKitDetails(kitId));
14 |
15 | // set it as the selected and active kit
16 | dispatch({ type: Actions.SET_SELECTED_KIT_ID, kitId: kitId });
17 | dispatch({ type: Actions.SET_ACTIVE_KIT_ID, kitId: kitId });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react'
3 | import { render } from 'react-dom'
4 | import { Provider } from 'react-redux'
5 |
6 | /* App imports */
7 | import store from 'state/store'
8 | import { initEditMenu } from 'menu/edit'
9 | import { initMidiMenu } from 'menu/midi'
10 | import { initDeviceTypeMenu } from 'menu/deviceType'
11 |
12 | /* Component imports */
13 | import App from 'component/App'
14 | import 'css/index.css';
15 |
16 | /* Initalize Electron App From Renderer Process */
17 | initEditMenu();
18 | initMidiMenu();
19 | initDeviceTypeMenu();
20 |
21 | render(
22 |
23 |
24 | ,
25 | document.getElementById('root')
26 | )
--------------------------------------------------------------------------------
/src/css/Pad/MuteGroup.css:
--------------------------------------------------------------------------------
1 | .mgrpContainer {
2 | position: relative; }
3 | .mgrpContainer .mgrpIcon {
4 | border-radius: 2px;
5 | box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
6 | display: inline-block;
7 | height: 1.5em;
8 | width: 1.5em;
9 | text-align: center; }
10 | .mgrpContainer > div {
11 | z-index: 10;
12 | left: -5.3em !important; }
13 | .mgrpContainer > div > div {
14 | left: 6em !important; }
15 | .mgrpContainer .selectMgrp button {
16 | width: 100%;
17 | text-align: center;
18 | display: inline-block;
19 | margin-top: -.3em;
20 | padding-bottom: .3em; }
21 | .mgrpContainer .selectMgrp .mgrpIcon {
22 | cursor: pointer;
23 | margin: .1em; }
24 |
--------------------------------------------------------------------------------
/src/css/Pad/MuteGroup.scss:
--------------------------------------------------------------------------------
1 | .mgrpContainer {
2 | position: relative;
3 |
4 | .mgrpIcon {
5 | border-radius: 2px;
6 | box-shadow: 0 2px 3px 0 rgba(0,0,0,.1),inset 0 0 0 1px rgba(0,0,0,.1);
7 | display: inline-block;
8 | height: 1.5em;
9 | width: 1.5em;
10 | text-align: center;
11 | }
12 |
13 | & > div {
14 | z-index: 10;
15 | left: -5.3em !important;
16 |
17 | & > div {
18 | left: 6em !important;
19 | }
20 | }
21 |
22 | .selectMgrp {
23 | button {
24 | width: 100%;
25 | text-align: center;
26 | display: inline-block;
27 | margin-top:-.3em;
28 | padding-bottom: .3em;
29 | }
30 | .mgrpIcon {
31 | cursor: pointer;
32 | margin: .1em;
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/component/Editor/Samplerack.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 |
5 | /* Component imports */
6 | import PadRowComponent from 'component/Pad/Row'
7 |
8 | const EditKit = (props) => {
9 |
10 | return (
11 |
12 | {
13 | props.pads.map((padId) => {
14 | return (
15 |
16 | );
17 | })
18 | }
19 |
20 | );
21 | }
22 |
23 | const mapStateToProps = (state, ownProps) => {
24 | let kit = state.kits.models[ownProps.kitId];
25 |
26 | return {
27 | pads: kit.pads
28 | }
29 | }
30 |
31 | const mapDispatchToProps = (dispatch, ownProps) => {
32 | return {}
33 | }
34 |
35 | export default connect(mapStateToProps, mapDispatchToProps)(EditKit)
36 |
--------------------------------------------------------------------------------
/src/css/SampleList.css:
--------------------------------------------------------------------------------
1 | .SampleList {
2 | margin: .5em 0 0 .5em; }
3 | .SampleList .samples {
4 | width: 100%;
5 | overflow-y: scroll;
6 | max-height: 30em; }
7 | .SampleList .samples button.sample {
8 | text-align: left; }
9 | .SampleList .samples .sampleName {
10 | max-width: 10em; }
11 | .SampleList .samples .sampleContainer:hover {
12 | width: 100%;
13 | background: white; }
14 | .SampleList .samples .sampleContainer:hover .sample {
15 | width: 100%;
16 | background: white;
17 | border-radius: 2px;
18 | box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1); }
19 | .SampleList .samples .sampleContainer:hover .sample .sampleName {
20 | background: white;
21 | text-overflow: normal;
22 | white-space: normal; }
23 |
--------------------------------------------------------------------------------
/src/css/SampleList.scss:
--------------------------------------------------------------------------------
1 | .SampleList {
2 | margin: .5em 0 0 .5em;
3 |
4 | .samples {
5 | width: 100%;
6 | overflow-y: scroll;
7 | max-height: 30em;
8 |
9 | button.sample {
10 | text-align: left;
11 | }
12 |
13 | .sampleName {
14 | // text-overflow: ellipsis;
15 | max-width: 10em;
16 | // overflow: hidden;
17 | // white-space: nowrap;
18 | }
19 |
20 | .sampleContainer:hover {
21 | width: 100%;
22 | background: white;
23 |
24 | .sample {
25 | width: 100%;
26 | background: white;
27 | border-radius: 2px;
28 | box-shadow: 0 2px 3px 0 rgba(0,0,0,.1),inset 0 0 0 1px rgba(0,0,0,.1);
29 |
30 | .sampleName {
31 | background: white;
32 | text-overflow: normal;
33 | white-space: normal;
34 | }
35 | }
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/src/component/Pad/Row.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 |
4 | /* Component imports */
5 | import PadSampleDropTargetComponent from 'component/Pad/SampleDropTarget'
6 | import 'css/Pad.css'
7 |
8 | class PadRowComponent extends React.Component {
9 | /*
10 | * @constructor
11 | * @param {Object} props
12 | */
13 | constructor(props) {
14 | super(props);
15 |
16 | this.state = {
17 | showLayerB: false
18 | };
19 |
20 | this.toggleLayerB = this.toggleLayerB.bind(this);
21 | }
22 |
23 | toggleLayerB(filter) {
24 | this.setState({showLayerB: !this.state.showLayerB})
25 | }
26 |
27 | render() {
28 | return ;
33 | }
34 | }
35 |
36 | export default PadRowComponent
37 |
--------------------------------------------------------------------------------
/src/css/Pad/Control.scss:
--------------------------------------------------------------------------------
1 | .controlContainer {
2 | width: 1.5em;
3 | height: 1.5em;
4 | overflow: hidden;
5 | padding-left: .2em;
6 | cursor: pointer;
7 |
8 | .rc-slider-rail, .rc-slider-track, .rc-slider-step, .rc-slider-mark {
9 | display: none;
10 | }
11 |
12 | .overlapContainer {
13 | position: absolute;
14 | top: 0;
15 |
16 | .glyphicon.overlapValue {
17 | overflow: hidden;
18 | top: 2px;
19 | }
20 |
21 | .overlapHandle {
22 | width:1.5em;
23 | }
24 | }
25 | }
26 |
27 | .height-25 {
28 | .rc-slider, .rc-slider-rail, .rc-slider-mark {
29 | height: 25px !important;
30 | }
31 | }
32 |
33 | .height-50 {
34 | .rc-slider, .rc-slider-rail, .rc-slider-mark {
35 | height: 50px !important;
36 | }
37 | }
38 |
39 | .height-100 {
40 | .rc-slider, .rc-slider-rail, .rc-slider-mark {
41 | height: 100px !important;
42 | }
43 | }
--------------------------------------------------------------------------------
/src/css/Pad/Control.css:
--------------------------------------------------------------------------------
1 | .controlContainer {
2 | width: 1.5em;
3 | height: 1.5em;
4 | overflow: hidden;
5 | padding-left: .2em;
6 | cursor: pointer; }
7 | .controlContainer .rc-slider-rail, .controlContainer .rc-slider-track, .controlContainer .rc-slider-step, .controlContainer .rc-slider-mark {
8 | display: none; }
9 | .controlContainer .overlapContainer {
10 | position: absolute;
11 | top: 0; }
12 | .controlContainer .overlapContainer .glyphicon.overlapValue {
13 | overflow: hidden;
14 | top: 2px; }
15 | .controlContainer .overlapContainer .overlapHandle {
16 | width: 1.5em; }
17 |
18 | .height-25 .rc-slider, .height-25 .rc-slider-rail, .height-25 .rc-slider-mark {
19 | height: 25px !important; }
20 |
21 | .height-50 .rc-slider, .height-50 .rc-slider-rail, .height-50 .rc-slider-mark {
22 | height: 50px !important; }
23 |
24 | .height-100 .rc-slider, .height-100 .rc-slider-rail, .height-100 .rc-slider-mark {
25 | height: 100px !important; }
26 |
--------------------------------------------------------------------------------
/public/preload.js:
--------------------------------------------------------------------------------
1 | /* Electron imports */
2 | const { contextBridge } = require("electron")
3 | const path = require('./rendererApi/path')
4 | const wav = require('./rendererApi/wav')
5 | const fs = require('./rendererApi/fs')
6 | const store = require('./rendererApi/store')
7 | const dialog = require('./rendererApi/dialog')
8 | const midi = require('./rendererApi/midi')
9 |
10 | const mainProcessCallbacks = require('./events/mainProcessCallbacks')
11 | const mainProcessTriggers = require('./events/mainProcessTriggers')
12 | const mainProcessEvents = require('./events/mainProcessEvents')
13 |
14 | // Initialize the apis that should be accessible from the renderer process
15 | contextBridge.exposeInMainWorld(
16 | "api", {
17 | path: path,
18 | wav: wav,
19 | fs: fs,
20 | store: store,
21 | dialog: dialog,
22 | midi: midi,
23 | mainProcessTriggers: mainProcessTriggers,
24 | mainProcessCallbacks: mainProcessCallbacks
25 | }
26 | )
27 |
28 | // Initialize the renderer message handlers for communication with the main process
29 | mainProcessEvents.initIpcRendererSender();
--------------------------------------------------------------------------------
/public/rendererApi/wav.js:
--------------------------------------------------------------------------------
1 | /* Electron imports */
2 | const { remote } = require("electron")
3 | const spawn = require('child_process').spawn
4 |
5 | let wavSpawn = {}
6 |
7 | module.exports = {
8 | playWavFile: (wavId, path) => {
9 | return new Promise((resolve, reject) => {
10 | switch (remote.process.platform) {
11 | case 'darwin':
12 | wavSpawn[wavId] = spawn('afplay', [path])
13 | break
14 | case 'win32':
15 | wavSpawn[wavId] = spawn('powershell', [
16 | '-c',
17 | '(New-Object System.Media.SoundPlayer "' + path + '").PlaySync()'
18 | ])
19 | wavSpawn[wavId].stdin.end()
20 | break
21 | default:
22 | resolve()
23 | break
24 | }
25 |
26 | wavSpawn[wavId].on('close', (code) => {
27 | resolve()
28 | })
29 | })
30 | },
31 | stopWavFile: (wavId) => {
32 | if (wavSpawn[wavId]) {
33 | wavSpawn[wavId].removeAllListeners('close')
34 | if (wavSpawn[wavId]) {
35 | wavSpawn[wavId].kill()
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/component/Pad/MidiNoteSelect.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 |
4 | /* App imports */
5 | import { MidiMap } from 'const'
6 |
7 | const MidiNoteSelect = (props) => {
8 | return (
9 |
10 |
11 |
12 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default MidiNoteSelect;
--------------------------------------------------------------------------------
/src/menu/midi.js:
--------------------------------------------------------------------------------
1 | /* Electron imports */
2 | const { midi, mainProcessCallbacks, mainProcessTriggers } = window.api
3 |
4 | /**
5 | * Initialize the renderer process handlers for the midi device menu
6 | */
7 | export const initMidiMenu = () => {
8 | midi.enable()
9 |
10 | let inputList = midi.getInputList()
11 | mainProcessTriggers.generateMidiMenu(inputList, (inputList.length ? 0 : null))
12 | mainProcessCallbacks.setSelectMidiInputCallback(selectMidiMenuItem)
13 | mainProcessCallbacks.setSelectMidiScanCallback(scanForMidiDevices)
14 |
15 | if (inputList.length) {
16 | selectMidiMenuItem(0)
17 | }
18 | }
19 |
20 | /**
21 | * Tell the main process to scan for midi devices, when it finds any
22 | * the menu will be regenerated with available devices
23 | */
24 | export const scanForMidiDevices = () => {
25 | midi.scanForMidiDevices();
26 |
27 | let inputList = midi.getInputList()
28 | mainProcessTriggers.generateMidiMenu(inputList, (inputList.length ? 0 : null))
29 |
30 | if (inputList.length) {
31 | selectMidiMenuItem(0)
32 | }
33 | }
34 |
35 | /**
36 | * A midi device has been selected from the menu, bind note handlers
37 | */
38 | export const selectMidiMenuItem = (inputIndex) => {
39 | midi.bindMidiInput(inputIndex);
40 | }
--------------------------------------------------------------------------------
/public/electron.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow, Menu } = require('electron')
2 | const path = require('path');
3 | const isDev = require('electron-is-dev');
4 | const mainProcessEvents = require('./events/mainProcessEvents')
5 | const { getMenuTemplate } = require('./mainApi/menu')
6 |
7 | let mainWindow;
8 |
9 | function createWindow() {
10 | mainWindow = new BrowserWindow({
11 | width: 1000,
12 | height: 780,
13 | minWidth: 900,
14 | webPreferences: {
15 | nodeIntegration: false,
16 | contextIsolation: true,
17 | preload: path.join(__dirname, "preload.js")
18 | }
19 | });
20 | mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`);
21 | mainWindow.on('closed', () => mainWindow = null);
22 |
23 | // Initialize the menu
24 | const menu = Menu.buildFromTemplate(getMenuTemplate())
25 | Menu.setApplicationMenu(menu)
26 |
27 | // Initialize the renderer message handlers
28 | mainProcessEvents.initIpcMainReceiver();
29 | }
30 |
31 | app.on('ready', createWindow);
32 |
33 | app.on('window-all-closed', () => {
34 | if (process.platform !== 'darwin') {
35 | app.quit();
36 | }
37 | });
38 |
39 | app.on('activate', () => {
40 | if (mainWindow === null) {
41 | createWindow();
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/src/util/fileDialog.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { Drive } from 'const';
3 |
4 | /* Electron imports */
5 | const { dialog } = window.api;
6 |
7 | /**
8 | * Open a file dialog with appropriate file type filters for kits
9 | * @return {Promise}
10 | */
11 | export function openKitFileDialog() {
12 | return dialog.showOpenDialog({
13 | title: 'Import kit',
14 | properties:["openFile"],
15 | filters: [
16 | { name: 'Kits (* .' + Drive.KIT_FILE_TYPE + ')',
17 | extensions: [Drive.KIT_FILE_TYPE] },
18 | { name: 'All Files', extensions: ['*'] }
19 | ]
20 | })
21 | }
22 |
23 | /**
24 | * Open a file dialog with appropriate file type filters for samples
25 | * @return {Promise}
26 | */
27 | export function openSampleFileDialog() {
28 | return dialog.showOpenDialog({
29 | title: 'Import Sample(s)',
30 | properties:["openFile", 'multiSelections', 'openDirectory'],
31 | filters: [
32 | { name: 'wav (* .' + Drive.SAMPLE_FILE_TYPE + ')',
33 | extensions: [Drive.SAMPLE_FILE_TYPE] },
34 | { name: 'All Files', extensions: ['*'] }
35 | ]
36 | })
37 | }
38 | /**
39 | * Open a directory dialog
40 | * @return {Promise}
41 | */
42 | export function openDriveDirectoryDialog() {
43 | return dialog.showOpenDialog({
44 | title: 'Load SamplePad Root Directory',
45 | properties:["openDirectory"]
46 | })
47 | }
48 |
--------------------------------------------------------------------------------
/src/state/sortModels.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { MidiMap } from 'const';
3 |
4 | export const getSortedKitIds = (kits) => {
5 | return Object.keys(kits)
6 | .map((kitId) => {
7 | let kit = kits[kitId];
8 | return {id: kitId, name: kit.kitName, isNew: kit.isNew}
9 | })
10 | .sort((a, b) => {
11 | // new kits are sorted to the top
12 | if (a.isNew) {
13 | if (!b.isNew || a.name < b.name) {
14 | return -1;
15 | }
16 | } else if (b.isNew) {
17 | if (!a.isNew || b.name < a.name) {
18 | return 1;
19 | }
20 | }
21 |
22 | // neither or both kits are new, just sort by name
23 | if (a.name < b.name) {
24 | return -1;
25 | }
26 | if (a.name > b.name) {
27 | return 1;
28 | }
29 |
30 | return 0;
31 | }).map((kit) => {
32 | return kit.id
33 | });
34 | }
35 |
36 | export const getSortedPadIds = (drive, pads) => {
37 | let padPriority = Object.keys(MidiMap[drive.deviceType]);
38 |
39 | let ret = Object.keys(pads)
40 | .map((padId) => {
41 | let pad = pads[padId];
42 | return {id: padId, type: pad.padType}
43 | })
44 | .sort((a, b) => {
45 | return (padPriority.indexOf(a.type) > padPriority.indexOf(b.type)) ? 1 : -1;
46 | }).map((pad) => {
47 | return pad.id
48 | });
49 |
50 | return ret;
51 | }
--------------------------------------------------------------------------------
/src/actions/modal.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { Actions } from 'const';
3 |
4 | /** MODAL ACTION CREATORS */
5 | /**
6 | * @param {Function} callback - function to call when the modal is closed
7 | */
8 | export function confirmFileOverwrite(callback) {
9 | return (dispatch, getState) => {
10 | dispatch({ type: Actions.SHOW_MODAL_CONFIRM_OVERWRITE, callback: callback });
11 | }
12 | }
13 | /**
14 | * hide the confirm overwrite modal and call the callback
15 | * @param {Boolean} result - true if the user chose to overwrite
16 | */
17 | export function confirmFileOverwriteAction(result) {
18 | return (dispatch, getState) => {
19 | let state = getState();
20 | let callback = state.modals.confirmOverwriteCallback;
21 |
22 | dispatch({ type: Actions.HIDE_MODAL_CONFIRM_OVERWRITE });
23 | dispatch(callback(result));
24 | }
25 | }
26 |
27 | /**
28 | * @param {Function} callback - function to call when the modal is closed
29 | */
30 | export function confirmLoadCard(callback) {
31 | return (dispatch, getState) => {
32 | dispatch({ type: Actions.SHOW_MODAL_CONFIRM_LOAD_CARD, callback: callback });
33 | }
34 | }
35 | /**
36 | * hide the modal and call the callback
37 | * @param {Boolean} result - true if the user chose to overwrite
38 | */
39 | export function confirmLoadCardAction(result) {
40 | return (dispatch, getState) => {
41 | let state = getState();
42 | let callback = state.modals.confirmLoadCardCallback;
43 |
44 | dispatch({ type: Actions.HIDE_MODAL_CONFIRM_LOAD_CARD });
45 | dispatch(callback(result));
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/public/rendererApi/fs.js:
--------------------------------------------------------------------------------
1 | /* Electron imports */
2 | const fs = require('fs')
3 | const path = require('path')
4 |
5 | module.exports = {
6 | exists: (file) => {
7 | return fs.existsSync(file)
8 | },
9 | isDirectory: (file) => {
10 | let stats = fs.statSync(file)
11 | return stats.isDirectory()
12 | },
13 | mkdir: (file) => {
14 | return fs.mkdirSync(file)
15 | },
16 | renameFile: (source, destination) => {
17 | return fs.renameSync(source, destination)
18 | },
19 | readFileAsString: (file) => {
20 | return fs.readFileSync(file, "utf8")
21 | },
22 | readFileAsArrayByLine: (file) => {
23 | return fs.readFileSync(file, "utf8").split("\n");
24 | },
25 | readFileBufferArray: (file) => {
26 | return fs.readFileSync(file)
27 | },
28 | writeFile: (file, content) => {
29 | return fs.writeFileSync(file, content)
30 | },
31 | getFileListFromDirectory: (dirPath) => {
32 | return fs.readdirSync(dirPath)
33 | .map((dirFile) => {
34 | return dirPath + "/" + dirFile
35 | })
36 | },
37 | getSampleFiles: (sampleDirectoryPath) => {
38 | return fs.readdirSync(sampleDirectoryPath, {withFileTypes: true})
39 | .filter((dirent) => {
40 | return dirent.isFile()
41 | && path.extname(dirent.name).toUpperCase() === ".WAV"
42 | && !(/(^|\/)\.[^/.]/g).test(dirent.name)
43 | })
44 | },
45 | getKitFiles: (kitPath) => {
46 | return fs.readdirSync(kitPath, {withFileTypes: true})
47 | .filter((dirent) => {
48 | return dirent.isFile()
49 | && path.extname(dirent.name).toUpperCase() === ".KIT"
50 | && !(/(^|\/)\.[^/.]/g).test(dirent.name)
51 | })
52 | }
53 | }
--------------------------------------------------------------------------------
/public/events/mainProcessCallbacks.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer } = require("electron")
2 | const Events = require("./events")
3 |
4 | /**
5 | * This class is responsible for the renderer process receiving events
6 | * from the main process
7 | *
8 | * Any main event triggered that the renderer is allowed to respond
9 | * to needs to have an associated callback bound here
10 | *
11 | * context: renderer
12 | */
13 | module.exports = {
14 | /**
15 | * bind a renderer callback to the main processes
16 | * main process event: a midi device has been selected in the menu
17 | */
18 | setSelectMidiInputCallback: (callback) => {
19 | ipcRenderer.on(Events.SELECT_MIDI_INPUT, (event, inputIndex) => {
20 | callback(inputIndex)
21 | })
22 | },
23 |
24 | /**
25 | * bind a renderer callback to the main processes
26 | * main process event: the "scan for midi devices" menu item has been clicked
27 | */
28 | setSelectMidiScanCallback: (callback) => {
29 | ipcRenderer.on(Events.SELECT_MIDI_SCAN, (event, inputIndex) => {
30 | callback(inputIndex)
31 | })
32 | },
33 |
34 | /**
35 | * bind a renderer callback to the main processes
36 | * main process event: a new device type has been selected
37 | */
38 | setSelectDeviceTypeCallback: (callback) => {
39 | ipcRenderer.on(Events.SELECT_DEVICE_TYPE, (event, deviceType) => {
40 | callback(deviceType)
41 | })
42 | },
43 |
44 | /**
45 | * bind a renderer callback to the main processes
46 | * main process event: load SD Card has been clicked
47 | */
48 | setLoadSDCardCallback: (callback) => {
49 | ipcRenderer.on(Events.LOAD_SD_CARD, (event) => {
50 | callback()
51 | })
52 | }
53 | }
--------------------------------------------------------------------------------
/public/events/rendererProcessEvents.js:
--------------------------------------------------------------------------------
1 | const { BrowserWindow } = require("electron")
2 | const Events = require("./events")
3 |
4 | /**
5 | * This class is responsible for the main process sending events
6 | * to the renderer process
7 | *
8 | * Any main event triggered that the renderer is allowed to respond
9 | * to needs to get triggered this way
10 | *
11 | * context: main
12 | */
13 | module.exports = {
14 | /**
15 | * tell the renderer process a main process event has happened
16 | * main process event: a midi device has been selected in the menu
17 | */
18 | selectMidiInput: (midiInput) => {
19 | let windows = BrowserWindow.getAllWindows()
20 | windows[0].webContents.send(Events.SELECT_MIDI_INPUT, midiInput)
21 | },
22 |
23 | /**
24 | * tell the renderer process a main process event has happened
25 | * main process event: the "scan for midi devices" menu item has been clicked
26 | */
27 | selectMidiScan: () => {
28 | let windows = BrowserWindow.getAllWindows()
29 | windows[0].webContents.send(Events.SELECT_MIDI_SCAN, null)
30 | },
31 |
32 | /**
33 | * tell the renderer process a main process event has happened
34 | * main process event: a new device type has been selected
35 | */
36 | selectDeviceType: (deviceType) => {
37 | let windows = BrowserWindow.getAllWindows()
38 | windows[0].webContents.send(Events.SELECT_DEVICE_TYPE, deviceType)
39 | },
40 |
41 | /**
42 | * tell the renderer process a main process event has happened
43 | * main process event: load SD Card has been clicked
44 | */
45 | loadSDCard: () => {
46 | let windows = BrowserWindow.getAllWindows()
47 | windows[0].webContents.send(Events.LOAD_SD_CARD)
48 | }
49 | }
--------------------------------------------------------------------------------
/public/events/mainProcessEvents.js:
--------------------------------------------------------------------------------
1 | const { ipcMain, ipcRenderer } = require('electron')
2 | const { regenerateMidiMenu, rengenerateDeviceMenu } = require('../mainApi/menu')
3 | const Events = require("./events")
4 |
5 | /**
6 | * This class is responsible for the main process receiving events
7 | * from the renderer process
8 | *
9 | * every main process event that can be triggered from the renderer process
10 | * needs an associated event handler in initIpcMainReceiver
11 | */
12 | module.exports = {
13 | /**
14 | * Initialize functionality for the
15 | * renderer process to send events to main process
16 | */
17 | initIpcRendererSender:() => {
18 | process.once('loaded', () => {
19 | window.addEventListener('message', event => {
20 | const message = event.data
21 |
22 | if (message.type) {
23 | ipcRenderer.send(message.type, message)
24 | }
25 | });
26 | });
27 | },
28 |
29 | /**
30 | * Initialize functionality for the
31 | * main process to receive events from the renderer process
32 | *
33 | * This can only be called in a context where we have an active renderer process
34 | *
35 | * renderer triggers to the main process are what cause these events to fire
36 | */
37 | initIpcMainReceiver: () => {
38 | ipcMain.on(Events.GENERATE_MIDI_MENU, (event, message) => {
39 | // context: main
40 | // the renderer process wants the midi menu regenerated
41 | regenerateMidiMenu(message.midiInputs, message.currentMidiInput)
42 | });
43 |
44 | ipcMain.on(Events.SET_DEVICE_TYPE, (event, message) => {
45 | // context: main
46 | // the renderer process wants to select a new device type
47 | rengenerateDeviceMenu(message.deviceType)
48 | });
49 | }
50 | }
--------------------------------------------------------------------------------
/src/component/Header.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 |
5 | /* App imports */
6 | import { DeviceType } from 'const'
7 | import { selectAndLoadDrive } from 'actions/drive'
8 |
9 | /* Component imports */
10 | import KitListComponent from 'component/KitList'
11 |
12 | const HeaderComponent = (props) => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | SamplePad Kit Editor
22 |
Model: { props.deviceType }
23 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | const mapStateToProps = (state, ownProps) => {
41 | let deviceType = "";
42 | switch (state.drive.deviceType) {
43 | case DeviceType.SAMPLEPAD_PRO:
44 | deviceType = "SAMPLEPAD PRO";
45 | break;
46 | case DeviceType.SAMPLERACK:
47 | deviceType = "SAMPLERACK";
48 | break;
49 | default:
50 | deviceType = "";
51 | break;
52 | }
53 |
54 | return {
55 | deviceType: deviceType
56 | }
57 | }
58 |
59 | const mapDispatchToProps = (dispatch, ownProps) => {
60 | return {
61 | loadCard: () => {
62 | dispatch(selectAndLoadDrive())
63 | }
64 | }
65 | }
66 |
67 | export default connect(mapStateToProps, mapDispatchToProps)(HeaderComponent)
68 |
--------------------------------------------------------------------------------
/src/component/Pad/PadName.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { v1 as uuidv1 } from 'uuid';
4 |
5 | /* Electron imports */
6 | const { midi } = window.api
7 |
8 | class PadName extends React.Component {
9 |
10 | /*
11 | * @constructor
12 | * @param {Object} props
13 | */
14 | constructor(props) {
15 | super(props)
16 |
17 | this.state = {
18 | playing: false
19 | }
20 |
21 | this.handlerId = uuidv1()
22 | this.addMidiHandler()
23 | }
24 |
25 | render() {
26 | return (
27 |
28 |
29 | {this.props.padName}:
30 |
31 |
32 | );
33 | }
34 |
35 | componentDidUpdate(prevProps, prevState, snapshot) {
36 | if (this.props.midi) {
37 | if (prevProps.midi.note !== this.props.midi.note ||
38 | prevProps.midi.min !== this.props.midi.min ||
39 | prevProps.midi.max !== this.props.midi.max) {
40 |
41 | this.removeMidiHandler(prevProps.midi.note)
42 | this.addMidiHandler()
43 | }
44 | }
45 | }
46 |
47 | componentWillUnmount() {
48 | if (this.props.midi) {
49 | this.removeMidiHandler(this.props.midi.note)
50 | }
51 | }
52 |
53 | addMidiHandler() {
54 | if (this.props.midi) {
55 | midi.addMidiNoteOnHandler(this.handlerId, this.props.midi.note, this.props.midi.min, this.props.midi.max, (e) => {
56 | this.setState({playing: true})
57 | clearTimeout(this.timeout)
58 | this.timeout = setTimeout(() => {
59 | this.setState({playing: false})
60 | }, 300)
61 | })
62 | }
63 | }
64 |
65 | removeMidiHandler(note) {
66 | if (this.handlerId) {
67 | midi.removeMidiNoteOnHandler(this.handlerId, note)
68 | }
69 | }
70 | }
71 |
72 | export default PadName
73 |
--------------------------------------------------------------------------------
/src/menu/deviceType.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { Actions } from 'const';
3 | import store from 'state/store'
4 | import { getGlobalStateFromDirectory, writeDeviceDetailsToFile } from 'state/globalState';
5 |
6 | /* Electron imports */
7 | const { mainProcessCallbacks, mainProcessTriggers } = window.api
8 |
9 | /**
10 | * Initialize the renderer process handlers for the device type menu
11 | */
12 | export const initDeviceTypeMenu = () => {
13 | mainProcessCallbacks.setSelectDeviceTypeCallback(selectDeviceTypeItem)
14 | }
15 |
16 | /**
17 | * Tell the main process to select a new device type
18 | */
19 | export const resetDeviceType = (deviceType) => {
20 | mainProcessTriggers.setDeviceType(deviceType)
21 | }
22 |
23 | /**
24 | * A SamplePad device type has been selected from the menu, update the root model
25 | */
26 | export const selectDeviceTypeItem = (deviceType) => {
27 | // todo: should show a warning before doing this, since you could lose unsaved data
28 | let state = store.getState();
29 |
30 | // if we already have a loaded drive, we need to reset state to handle it
31 | if (state.drive.deviceId) {
32 | // reset app selections to beginning state
33 | store.dispatch({ type: Actions.SET_SELECTED_KIT_ID, kitId: null });
34 | store.dispatch({ type: Actions.SET_ACTIVE_KIT_ID, kitId: null });
35 |
36 | // change the drive device type
37 | store.dispatch({ type: Actions.SET_DEVICE_TYPE, deviceType: deviceType });
38 |
39 | // save the new device details
40 | writeDeviceDetailsToFile(state.drive.rootPath, state.drive.deviceId, deviceType)
41 |
42 | // re-load the drive kits, reading in the new set of pads
43 | let {drive, kits} = getGlobalStateFromDirectory(state.drive.rootPath);
44 | store.dispatch({ type: Actions.RESET_KITS, kits: kits });
45 | store.dispatch({ type: Actions.SORT_KITS });
46 | } else {
47 | // change the drive device type
48 | store.dispatch({ type: Actions.SET_DEVICE_TYPE, deviceType: deviceType });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/component/KitList.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 |
5 | /* App imports */
6 | import { selectKit } from 'actions/app'
7 | import { importKitFromFile, loadNewKit } from 'actions/kit'
8 |
9 | const KitList = (props) => {
10 | return (
11 |
12 |
13 |
14 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | const mapStateToProps = (state, ownProps) => {
43 | return {
44 | kits: state.kits.models,
45 | sortedKitIds: state.kits.ids,
46 | selectedKitId: state.app.selectedKitId
47 | }
48 | }
49 |
50 | const mapDispatchToProps = (dispatch, ownProps) => {
51 | return {
52 | loadKitFromFile: () => {
53 | dispatch(importKitFromFile());
54 | },
55 | loadNewKit: () => {
56 | dispatch(loadNewKit());
57 | },
58 | setSelectedKit: (kitId) => {
59 | dispatch(selectKit(kitId));
60 | }
61 | }
62 | }
63 |
64 | export default connect(mapStateToProps, mapDispatchToProps)(KitList)
65 |
--------------------------------------------------------------------------------
/src/component/Pad/KnobControl.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import Slider from 'rc-slider';
4 |
5 | /* Component imports */
6 | import 'css/Pad/Control.css'
7 |
8 | class KnobComponent extends React.Component {
9 |
10 | /*
11 | * @constructor
12 | * @param {Object} props
13 | */
14 | constructor(props) {
15 | super(props)
16 |
17 | this.state = {
18 | value: props.value
19 | };
20 | }
21 |
22 | onSliderChange(value) {
23 | this.setState({
24 | value,
25 | });
26 | };
27 |
28 | getRotateTransform(value) {
29 | // rotation goes from -90 to 90
30 | let range = this.props.max - this.props.min
31 | let step = 180/range;
32 | let rotation = -90 + (step*(value - this.props.min))
33 | let rotate = 'rotate(' + (rotation) + 'deg)';
34 | return rotate;
35 | }
36 |
37 | getHandle(handleProps) {
38 | const { value, ...restProps } = handleProps;
39 |
40 | return (
41 |
42 |
47 |
48 | );
49 | }
50 |
51 | render() {
52 | return (
53 |
55 |
56 | { this.state.value }
57 | this.props.onChange(value)} />
65 |
66 |
67 | );
68 | }
69 | }
70 |
71 | export default KnobComponent;
72 |
--------------------------------------------------------------------------------
/src/component/Editor/SamplepadPro.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 |
5 | /* App imports */
6 | import { getPadWithType } from 'util/kitFile'
7 |
8 | /* Component imports */
9 | import PadRowComponent from 'component/Pad/Row'
10 |
11 | const EditKit = (props) => {
12 |
13 | return (
14 |
15 |
Main Pads
16 | {
17 | props.mainPads.map((pad) => {
18 | return (
19 |
20 | );
21 | })
22 | }
23 |
24 |
External Pads
25 | {
26 | props.extPads.map((pad) => {
27 | return (
28 |
29 | );
30 | })
31 | }
32 |
33 | );
34 | }
35 |
36 | const mapStateToProps = (state, ownProps) => {
37 | let kit = state.kits.models[ownProps.kitId];
38 |
39 | return {
40 | mainPads: [
41 | getPadWithType(kit, state.pads, "pad_01"),
42 | getPadWithType(kit, state.pads, "pad_02"),
43 | getPadWithType(kit, state.pads, "pad_03"),
44 | getPadWithType(kit, state.pads, "pad_04"),
45 | getPadWithType(kit, state.pads, "pad_05"),
46 | getPadWithType(kit, state.pads, "pad_06"),
47 | getPadWithType(kit, state.pads, "pad_07"),
48 | getPadWithType(kit, state.pads, "pad_08"),
49 | ],
50 | extPads: [
51 | getPadWithType(kit, state.pads, "ext_1a"),
52 | getPadWithType(kit, state.pads, "ext_1b"),
53 | getPadWithType(kit, state.pads, "ext_2"),
54 | getPadWithType(kit, state.pads, "kick"),
55 | getPadWithType(kit, state.pads, "hh_ope"),
56 | getPadWithType(kit, state.pads, "hh_mid"),
57 | getPadWithType(kit, state.pads, "hh_clo"),
58 | getPadWithType(kit, state.pads, "hh_chk"),
59 | getPadWithType(kit, state.pads, "hh_spl"),
60 | ]
61 | }
62 | }
63 |
64 | const mapDispatchToProps = (dispatch, ownProps) => {
65 | return {}
66 | }
67 |
68 | export default connect(mapStateToProps, mapDispatchToProps)(EditKit)
69 |
--------------------------------------------------------------------------------
/src/component/Pad/SampleDropTarget.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 | import { useDrop } from 'react-dnd';
5 |
6 | /* App imports */
7 | import { DragItemTypes } from 'const';
8 | import { updatePadStringProperty } from 'actions/pad'
9 |
10 | /* Component imports */
11 | import PadLayerAComponent from 'component/Pad/LayerA';
12 | import PadLayerBComponent from 'component/Pad/LayerB';
13 |
14 | const PadSampleDropTargetComponent = (props) => {
15 |
16 | const [{ isOver }, drop] = useDrop({
17 | accept: DragItemTypes.SAMPLE,
18 | drop: (item) => props.updatePadSample(item),
19 | collect: mon => ({
20 | isOver: !!mon.isOver()
21 | }),
22 | })
23 |
24 | const [{ isOverB }, dropB] = useDrop({
25 | accept: DragItemTypes.SAMPLE,
26 | drop: (item) => props.updatePadSampleB(item),
27 | collect: mon => ({
28 | isOverB: !!mon.isOver()
29 | }),
30 | })
31 |
32 | return (
33 |
34 |
35 |
39 | {isOver && (
40 |
41 | )}
42 |
43 | {props.showLayerB &&
44 |
45 |
46 | {isOverB && (
47 |
48 | )}
49 |
50 | }
51 |
52 | );
53 | };
54 |
55 |
56 | const mapStateToProps = (state, ownProps) => {
57 | return {}
58 | }
59 |
60 | const mapDispatchToProps = (dispatch, ownProps) => {
61 | return {
62 | updatePadSample: (item) => {
63 | dispatch(updatePadStringProperty(ownProps.padId, 'fileName', item.fileName));
64 | },
65 | updatePadSampleB: (item) => {
66 | dispatch(updatePadStringProperty(ownProps.padId, 'fileNameB', item.fileName));
67 | },
68 | }
69 | }
70 |
71 | export default connect(mapStateToProps, mapDispatchToProps)(PadSampleDropTargetComponent)
72 |
--------------------------------------------------------------------------------
/src/actions/drive.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { Actions } from 'const';
3 | import { confirmLoadCard } from 'actions/modal';
4 | import { showNotice } from 'actions/notice';
5 | import { getGlobalStateFromDirectory } from 'state/globalState';
6 | import { openDriveDirectoryDialog } from 'util/fileDialog';
7 | import { storeLastLoadedDirectory } from 'util/storage';
8 |
9 | /** DRIVE ACTION CREATORS */
10 | /**
11 | * Open a file dialog, and parse the resulting directory as a SamplePad drive
12 | */
13 | export function selectAndLoadDrive(confirmedLoadCard=false) {
14 | return (dispatch, getState) => {
15 | let state = getState()
16 |
17 | // show the warning modal - on confirm, load the card
18 | // todo: only do this if some information hasnt been saved
19 | if (state.drive.deviceId && !confirmedLoadCard) {
20 | dispatch(confirmLoadCard((result) => {
21 | return (dispatch, getState) => {
22 | if (result) {
23 | dispatch(selectAndLoadDrive(true));
24 | }
25 | }
26 | }));
27 | return;
28 | }
29 |
30 | openDriveDirectoryDialog()
31 | .then(result => {
32 |
33 | if (result.canceled) {
34 | return null;
35 | }
36 |
37 | storeLastLoadedDirectory(result.filePaths[0]);
38 | dispatch(loadDrive(result.filePaths[0]));
39 | dispatch(
40 | showNotice("is-success", "SD card and any existing samples and kits have been successfully loaded.")
41 | );
42 | })
43 | }
44 | }
45 | /**
46 | * Parse kits, samples, and drive details from the given directory
47 | * @param {String} drivePath - root path of the SamplePad drive
48 | */
49 | export function loadDrive(drivePath) {
50 | return (dispatch) => {
51 | let {drive, kits} = getGlobalStateFromDirectory(drivePath);
52 |
53 | // resetting to beginning state
54 | dispatch({ type: Actions.SET_SELECTED_KIT_ID, kitId: null });
55 | dispatch({ type: Actions.SET_ACTIVE_KIT_ID, kitId: null });
56 |
57 | // load all the drive details
58 | dispatch({ type: Actions.LOAD_DRIVE, drive: drive });
59 | dispatch({ type: Actions.RESET_KITS, kits: kits });
60 | dispatch({ type: Actions.SORT_KITS });
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/component/Notice.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import update from 'immutability-helper';
4 |
5 | /* Component imports */
6 | import 'css/Notice.css'
7 |
8 | class NoticeComponent extends React.Component {
9 | /*
10 | * @constructor
11 | * @param {Object} props
12 | */
13 | constructor(props) {
14 | super(props);
15 |
16 | this.state = {
17 | hiddenNotices: [],
18 | hidingNotices: []
19 | };
20 |
21 | this.hideNotice = this.hideNotice.bind(this);
22 | }
23 |
24 | hideNotice(noticeId) {
25 | this.setState(update(this.state, {
26 | hiddenNotices: {$push: [noticeId]},
27 | hidingNotices: {$push: [noticeId]}
28 | }));
29 | }
30 |
31 | componentDidUpdate() {
32 | // start timeouts for any new notices that were added
33 | this.props.notices.forEach((notice) => {
34 | if (this.state.hidingNotices.indexOf(notice.id) === -1) {
35 | this.startTimeout(notice.id)
36 | }
37 | })
38 | }
39 |
40 | startTimeout(noticeId) {
41 | if (this.state.hidingNotices.indexOf(noticeId) === -1) {
42 | setTimeout(() => {
43 | this.setState(update(this.state, {
44 | hiddenNotices: {$push: [noticeId]}
45 | }));
46 | }, 2000);
47 |
48 | this.setState(update(this.state, {
49 | hidingNotices: {$push: [noticeId]}
50 | }));
51 | }
52 | }
53 |
54 | render() {
55 | return (
56 |
57 | {
58 | this.props.notices.map((notice) => {
59 | return (
60 |
-1) ? 'hidden ' : '') + notice.style}>
62 |
63 | {notice.text}
64 |
65 |
70 |
71 |
72 | );
73 | })
74 | }
75 |
76 | );
77 | }
78 | }
79 |
80 | export default NoticeComponent;
--------------------------------------------------------------------------------
/src/component/Pad/Velocity.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 |
4 | /* Component imports */
5 | import 'css/Pad/Velocity.css'
6 |
7 | class VelocityComponent extends React.Component {
8 |
9 | /*
10 | * @constructor
11 | * @param {Object} props
12 | */
13 | constructor(props) {
14 | super(props);
15 |
16 | this.state = {
17 | editingMin: false,
18 | editingMax: false
19 | };
20 |
21 | this.toggleEditMin = this.toggleEditMin.bind(this);
22 | this.toggleEditMax = this.toggleEditMax.bind(this);
23 | }
24 |
25 | toggleEditMin() {
26 | this.setState({editingMin: !this.state.editingMin});
27 | }
28 |
29 | toggleEditMax() {
30 | this.setState({editingMax: !this.state.editingMax});
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
37 | (
38 | {this.state.editingMin &&
39 | input && input.focus()}
43 | onFocus={(e) => e.target.select()}
44 | className="input is-static"
45 | value={this.props.min}
46 | onBlur={this.toggleEditMin}
47 | onChange={(e) => this.props.onChangeMin(e.target.value)} />
48 | }
49 | {!this.state.editingMin &&
50 |
51 | {this.props.min}
52 |
53 | }
54 | -
55 | {this.state.editingMax &&
56 | input && input.focus()}
60 | onFocus={(e) => e.target.select()}
61 | className="input is-static"
62 | value={this.props.max}
63 | onBlur={this.toggleEditMax}
64 | onChange={(e) => this.props.onChangeMax(e.target.value)} />
65 | }
66 | {!this.state.editingMax &&
67 |
68 | {this.props.max}
69 |
70 | }
71 | )
72 |
73 |
74 | );
75 | }
76 | }
77 |
78 | export default VelocityComponent
79 |
--------------------------------------------------------------------------------
/src/state/globalState.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import { v1 as uuidv1 } from 'uuid';
3 |
4 | /* App imports */
5 | import { Drive, DeviceType } from 'const';
6 | import { RootModel, KitModel } from 'state/models';
7 | import SampleStore from 'util/sampleStore';
8 | import { resetDeviceType } from 'menu/deviceType'
9 |
10 | /* Electron imports */
11 | const { fs } = window.api;
12 |
13 | /**
14 | * @param {String} rootPath
15 | * @return {RootModel, KitModel[]}
16 | */
17 | export const getGlobalStateFromDirectory = (rootPath) => {
18 | if(!fs.exists(rootPath)) {
19 | throw new Error("Invalid directory")
20 | }
21 |
22 | let {deviceId, deviceType} = getDeviceDetailsFromDirectory(rootPath)
23 | let kits = {};
24 | let kitPath = rootPath + "/" + Drive.KIT_DIRECTORY;
25 |
26 | SampleStore.loadSamplesFromDirectory(deviceId, rootPath)
27 |
28 | if(fs.exists(kitPath)) {
29 | let kitFiles = fs.getKitFiles(kitPath)
30 |
31 | kitFiles.forEach((kitFile) => {
32 | let kit = KitModel(kitPath, kitFile.name, null, true, null, kitFile.name.slice(0, -4));
33 | kits[kit.id] = kit;
34 | });
35 | }
36 |
37 | let drive = RootModel(deviceId, deviceType, rootPath, kitPath, Object.keys(SampleStore.getSamples()));
38 |
39 | // set the menu to this drive's device type
40 | resetDeviceType(deviceType);
41 |
42 | return {drive, kits};
43 | }
44 |
45 | const getDeviceDetailsFromDirectory = (devicePath) => {
46 | let deviceId = uuidv1()
47 | let deviceFile = devicePath + "/" + Drive.DEVICE_ID_FILE
48 | let deviceType = DeviceType.SAMPLERACK;
49 |
50 | let writeFile = true;
51 |
52 | // look for an existing device id on the card
53 | if(fs.exists(deviceFile)) {
54 | let deviceDetails = fs.readFileAsArrayByLine(deviceFile);
55 | deviceId = deviceDetails[0];
56 |
57 | if (deviceDetails.length > 1) {
58 | writeFile = false;
59 | deviceType = deviceDetails[1];
60 | }
61 | }
62 |
63 | if (writeFile) {
64 | writeDeviceDetailsToFile(devicePath, deviceId, deviceType)
65 | }
66 |
67 | return {deviceId, deviceType}
68 | }
69 |
70 | export const writeDeviceDetailsToFile = (devicePath, deviceId, deviceType) => {
71 | // todo: should probably store config data as json in device file
72 | let deviceFile = devicePath + "/" + Drive.DEVICE_ID_FILE
73 | let deviceDetails = deviceId + "\n";
74 | deviceDetails += deviceType + "\n";
75 |
76 | fs.writeFile(deviceFile, deviceDetails)
77 | }
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/component/Sample.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { useDrag } from 'react-dnd';
4 |
5 | /* App imports */
6 | import { DragItemTypes } from 'const';
7 |
8 | const SampleComponent = (props) => {
9 | let [, drag] = useDrag({
10 | item: { type: DragItemTypes.SAMPLE, fileName: props.fileName },
11 | canDrag: monitor => (props.draggable)
12 | });
13 |
14 | let hasSample = !!props.fileName;
15 |
16 | // highlight the search term in the sample name
17 | let displayName = props.fileName;
18 | if (hasSample && props.highlightKeyword) {
19 | var start=displayName.toLowerCase().indexOf(props.highlightKeyword.toLowerCase());
20 |
21 | displayName = displayName.substr(0,start)
22 | + ''
23 | + displayName.substr(start, props.highlightKeyword.length)
24 | + ''
25 | + displayName.substr(start + props.highlightKeyword.length)
26 | }
27 |
28 | let containerProps = {
29 | 'className': 'sampleContainer'
30 | };
31 | if (hasSample && props.useTooltip && props.fileName.length > 12 ) {
32 | containerProps = {
33 | 'className': 'sampleContainer has-tooltip-bottom',
34 | 'data-tooltip': props.fileName
35 | };
36 | }
37 |
38 | return (
39 |
40 |
41 |
54 |
55 |
56 | { hasSample && props.removable &&
57 |
58 |
61 |
62 | }
63 |
64 | );
65 | }
66 |
67 | export default SampleComponent;
68 |
--------------------------------------------------------------------------------
/src/component/Pad/SlideControl.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import Slider from 'rc-slider';
4 |
5 | /* Component imports */
6 | import 'css/Pad/Control.css'
7 |
8 | class SlideComponent extends React.Component {
9 |
10 | /*
11 | * @constructor
12 | * @param {Object} props
13 | */
14 | constructor(props) {
15 | super(props)
16 |
17 | this.state = {
18 | value: props.value
19 | };
20 | }
21 |
22 | onSliderChange(value) {
23 | this.setState({
24 | value,
25 | });
26 | };
27 |
28 | getWidth(value) {
29 | let range = this.props.max - this.props.min;
30 | let step = 1.2/range;
31 |
32 | return (value * step) + 'em';
33 | }
34 |
35 | getColor(value) {
36 | let range = this.props.max - this.props.min
37 | let step = 255/range;
38 | let color = Math.floor((255 - (value * step))).toString(16);
39 | return '#' + color + color + color;
40 | }
41 |
42 | getHandle(handleProps) {
43 | const { value, ...restProps } = handleProps;
44 |
45 | let newStyle = {};
46 | if (this.props.overlayProperty === 'width') {
47 | newStyle.width = this.getWidth(restProps.dragging ? value : this.props.value);
48 | }
49 | if (this.props.overlayProperty === 'color') {
50 | newStyle.color = this.getColor(restProps.dragging ? value : this.props.value);
51 | }
52 |
53 | return (
54 |
55 |
59 |
60 | );
61 | }
62 |
63 | render() {
64 | return (
65 |
66 |
67 | { this.state.value }
68 |
69 | this.props.onChange(value)} />
77 |
78 |
79 | );
80 | }
81 | }
82 |
83 | export default SlideComponent;
84 |
--------------------------------------------------------------------------------
/src/actions/pad.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { Actions, PadErrors } from 'const'
3 |
4 | /** PAD ACTION CREATORS */
5 | /**
6 | * Update an integer property of a pad, value is cast to an int
7 | * @param {String} padId
8 | * @param {String} property
9 | * @param {?} value
10 | */
11 | export function updatePadIntProperty(padId, property, value) {
12 | if (value === "") {
13 | // dont cast empty value, let it be empty
14 | return updatePadProperty(padId, property, value);
15 | }
16 | return updatePadProperty(padId, property, parseInt(value, 10));
17 | }
18 | /**
19 | * Update an integer property of a pad, value is cast to a string
20 | * @param {String} padId
21 | * @param {String} property
22 | * @param {?} value
23 | */
24 | export function updatePadStringProperty(padId, property, value) {
25 | return updatePadProperty(padId, property, '' + value);
26 | }
27 | /**
28 | * Update the sensitivity property of a pad
29 | * @param {String} padId
30 | * @param {String} value
31 | */
32 | export function updatePadSensitivity(padId, value) {
33 | return updatePadProperty(padId, "sensitivity", value);
34 | }
35 | /**
36 | * Update an individual property of a pad
37 | * @param {String} padId
38 | * @param {String} property
39 | * @param {?} value
40 | */
41 | export function updatePadProperty(padId, property, value) {
42 | return (dispatch, getState) => {
43 | dispatch({ type: Actions.UPDATE_PAD_PROPERTY, padId: padId, property: property, value: value });
44 |
45 | dispatch(validatePad(padId));
46 | }
47 | }
48 | /**
49 | * Validate all pad params, update the pad state with any new errors
50 | * @param {String} padId
51 | */
52 | export function validatePad(padId) {
53 | return (dispatch, getState) => {
54 | let state = getState();
55 | let pad = state.pads[padId];
56 | let prevErrors = pad.errors;
57 | let errors = [];
58 |
59 | if (pad.velocityMin > pad.velocityMax) {
60 | errors.push(PadErrors.VELOCITY_SWAPPED_A)
61 | }
62 | if (pad.velocityMin > 127 || pad.velocityMax > 127) {
63 | errors.push(PadErrors.VELOCITY_TOO_HIGH_A);
64 | }
65 | if (pad.velocityMinB > pad.velocityMaxB) {
66 | errors.push(PadErrors.VELOCITY_SWAPPED_B)
67 | }
68 | if (pad.velocityMinB > 127 || pad.velocityMaxB > 127) {
69 | errors.push(PadErrors.VELOCITY_TOO_HIGH_B);
70 | }
71 |
72 | if (prevErrors.length || errors.length) {
73 | dispatch({ type: Actions.UPDATE_PAD_PROPERTY, padId: padId, property: 'errors', value: errors });
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/component/App.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 | import { DndProvider } from 'react-dnd'
5 | import { HTML5Backend } from 'react-dnd-html5-backend'
6 |
7 | /* App imports */
8 | import { selectAndLoadDrive } from 'actions/drive'
9 |
10 | /* Component imports */
11 | import ModalComponent from 'component/Modal'
12 | import NoticeComponent from 'component/Notice'
13 | import EditKitComponent from 'component/EditKit'
14 | import HeaderComponent from 'component/Header'
15 | import SampleListComponent from 'component/SampleList'
16 |
17 | const AppComponent = (props) => {
18 | return (
19 |
20 | {props.showSplash &&
21 |
22 |
23 |
24 |
Make sure your SamplePad SD card is inserted into your computer. Click the "Load SD Card" button below and select the root directory of the SD card
25 |
26 |
27 |
28 | }
29 |
30 | {!props.showSplash &&
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {!props.hasActiveKit &&
43 |
44 |
45 |
Select, import, or create a new kit to begin
46 |
47 |
48 | }
49 |
50 | {props.hasActiveKit &&
51 |
52 | }
53 |
54 |
55 |
56 | }
57 |
58 | );
59 | }
60 |
61 | const mapStateToProps = (state, ownProps) => {
62 | return {
63 | showSplash: !state.drive.deviceId,
64 | notices: state.notices,
65 | activeKitId: state.app.activeKitId,
66 | hasActiveKit: (state.app.activeKitId !== null)
67 | }
68 | }
69 |
70 | const mapDispatchToProps = (dispatch, ownProps) => {
71 | return {
72 | loadCard: () => {
73 | dispatch(selectAndLoadDrive())
74 | }
75 | }
76 | }
77 |
78 | export default connect(mapStateToProps, mapDispatchToProps)(AppComponent)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "samplepad-editor",
3 | "productName": "SamplePad Kit Editor",
4 | "description": "Build drum kits for the Alesis SamplePad",
5 | "author": "Ryan Bateman",
6 | "version": "0.7.0",
7 | "private": true,
8 | "main": "public/electron.js",
9 | "homepage": "./",
10 | "build": {
11 | "appId": "com.electron.samplepadeditor",
12 | "productName": "SamplePad Kit Editor",
13 | "files": [
14 | "build/**/*",
15 | "node_modules/**/*"
16 | ],
17 | "directories": {
18 | "buildResources": "assets"
19 | },
20 | "mac": {
21 | "category": "public.app-category.music"
22 | },
23 | "linux": {
24 | "target": "tar.gz"
25 | }
26 | },
27 | "resolutions": {
28 | "react-dev-utils": "10.1.0"
29 | },
30 | "dependencies": {
31 | "@craco/craco": "^5.6.3",
32 | "electron-is-dev": "^1.1.0",
33 | "electron-store": "^6.0.0",
34 | "immutability-helper": "^3.0.1",
35 | "node-sass-chokidar": "^1.4.0",
36 | "rc-slider": "^9.3.1",
37 | "react": "^16.12.0",
38 | "react-dev-utils": "10.2.1",
39 | "react-dnd": "^11.1.3",
40 | "react-dnd-html5-backend": "^11.1.3",
41 | "react-dom": "^16.12.0",
42 | "react-redux": "^7.1.0",
43 | "react-scripts": "3.4.3",
44 | "react-simple-popover": "^0.2.4",
45 | "redux": "^4.0.4",
46 | "redux-thunk": "^2.3.0",
47 | "uuid": "^8.3.0",
48 | "wavefile": "^11.0.0",
49 | "webmidi": "^2.5.1"
50 | },
51 | "devDependencies": {
52 | "concurrently": "^5.0.2",
53 | "electron": "10.1.3",
54 | "electron-builder": "^22.8.0",
55 | "electron-rebuild": "^1.10.0",
56 | "wait-on": "^5.2.0",
57 | "yarn-run-all": "^3.1.1"
58 | },
59 | "scripts": {
60 | "preelectron-pack": "yarn run build",
61 | "electron-pack": "electron-builder build -l -m -w -c.extraMetadata.main=build/electron.js",
62 | "electron-dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\"",
63 | "postinstall": "electron-builder install-app-deps",
64 | "build-css": "node-sass-chokidar src/ -o src/",
65 | "watch-css": "yarn run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
66 | "start": "npm-run-all -p watch-css start-react",
67 | "build": "npm-run-all -s build-css build-react",
68 | "start-react": "react-scripts start",
69 | "build-react": "react-scripts build",
70 | "test": "react-scripts test",
71 | "eject": "react-scripts eject"
72 | },
73 | "eslintConfig": {
74 | "extends": "react-app"
75 | },
76 | "browserslist": {
77 | "production": [
78 | ">0.2%",
79 | "not dead",
80 | "not op_mini all"
81 | ],
82 | "development": [
83 | "last 1 chrome version",
84 | "last 1 firefox version",
85 | "last 1 safari version"
86 | ]
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/component/EditKit.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 |
5 | /* App imports */
6 | import { KitErrors, KitErrorStrings, DeviceType } from 'const'
7 | import { saveKit, updateKitName } from 'actions/kit'
8 |
9 | /* Component imports */
10 | import SamplerackEditor from 'component/Editor/Samplerack'
11 | import SamplepadProEditor from 'component/Editor/SamplepadPro'
12 | import 'css/EditKit.css'
13 |
14 | const EditKit = (props) => {
15 | let kitNameControlProps = {};
16 |
17 | if (props.hasKitNameError) {
18 | kitNameControlProps = {
19 | 'data-tooltip': KitErrorStrings.INVALID_KIT_NAME
20 | }
21 | }
22 |
23 | return (
24 |
25 |
26 |
Kit: {props.originalKitName}
27 |
28 |
29 | props.updateKitName(e.target.value)} />
34 |
35 |
36 |
37 |
38 | {
39 | props.showSaveAsNew &&
40 |
41 | }
42 |
43 |
44 |
45 | { props.showSamplerackEditor &&
46 |
47 | }
48 |
49 | { props.showSamplepadProEditor &&
50 |
51 | }
52 |
53 |
54 | );
55 | }
56 |
57 | const mapStateToProps = (state, ownProps) => {
58 | let kit = state.kits.models[ownProps.kitId];
59 |
60 | return {
61 | showSamplerackEditor: (state.drive.deviceType === DeviceType.SAMPLERACK),
62 | showSamplepadProEditor: (state.drive.deviceType === DeviceType.SAMPLEPAD_PRO),
63 | showSaveAsNew: kit.isExisting,
64 | kitName: kit.kitName,
65 | originalKitName: kit.originalKitName,
66 | hasKitNameError: (kit.errors.indexOf(KitErrors.INVALID_KIT_NAME) > -1),
67 | pads: kit.pads
68 | }
69 | }
70 |
71 | const mapDispatchToProps = (dispatch, ownProps) => {
72 | return {
73 | saveKit: () => {
74 | dispatch(saveKit(ownProps.kitId));
75 | },
76 | saveNewKit: () => {
77 | dispatch(saveKit(ownProps.kitId, true));
78 | },
79 | updateKitName: (value) => {
80 | dispatch(updateKitName(ownProps.kitId, value));
81 | }
82 | }
83 | }
84 |
85 | export default connect(mapStateToProps, mapDispatchToProps)(EditKit)
86 |
--------------------------------------------------------------------------------
/src/component/Pad/MuteGroup.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import Popover from 'react-simple-popover';
4 |
5 | /* Component imports */
6 | import 'css/Pad/MuteGroup.css'
7 |
8 | class MuteGroupComponent extends React.Component {
9 |
10 | /*
11 | * @constructor
12 | * @param {Object} props
13 | */
14 | constructor(props) {
15 | super(props);
16 |
17 | this.state = {
18 | open: false
19 | };
20 | }
21 |
22 | openPopover = (e) => {
23 | this.setState({open: !this.state.open});
24 | }
25 |
26 | handleClose = (e) => {
27 | this.setState({open: false});
28 | }
29 |
30 | getMgrpBackgroundColor(value) {
31 | // 0 to 16
32 | return [
33 | '#ffffff',
34 | '#73dc32','#d12394','#2394d1','#ffb3b3',
35 | '#79ff57','#dd57ff','#ffdd57','#d16023',
36 | '#5779ff','#00d1b2','#d1001f','#d1b200',
37 | '#3273dc','#dc9b32','#9b32dc','#001fd1'
38 | ][value];
39 | }
40 |
41 | getMgrpForegroundClass(value) {
42 | if ([2,3,8,9,11,13,15,16].indexOf(value) > -1) {
43 | // these background colors require light text
44 | return 'has-text-white';
45 | }
46 |
47 | // other background colors require dark text
48 | return 'has-text-black';
49 | }
50 |
51 | render() {
52 | return (
53 |
54 |
{ this.target = node }}
56 | onClick={this.openPopover}
57 | className={"mgrpIcon has-tooltip-left " + this.getMgrpForegroundClass(this.props.mgrp)}
58 | style={{backgroundColor: this.getMgrpBackgroundColor(this.props.mgrp)}}
59 | data-tooltip={this.props.tooltip}>
60 | {this.props.mgrp > 0 ? this.props.mgrp : '-'}
61 |
62 |
63 |
71 |
72 | {this.props.mgrp > 0 &&
73 |
76 | }
77 | {
78 | [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16].map((mgrp) => {
79 | return {this.props.onChange(mgrp);this.handleClose();}}
83 | style={{backgroundColor: this.getMgrpBackgroundColor(mgrp)}}>
84 | {mgrp > 0 ? mgrp : '-'}
85 |
86 | })
87 | }
88 |
89 |
90 |
91 | );
92 | }
93 | }
94 |
95 | export default MuteGroupComponent
96 |
--------------------------------------------------------------------------------
/src/css/Pad.css:
--------------------------------------------------------------------------------
1 | .Pad .level {
2 | margin-bottom: 0 !important; }
3 |
4 | .Pad .layerA .padName {
5 | width: 9em;
6 | padding: .5em;
7 | border-radius: 5px; }
8 |
9 | .Pad .PadLayer .level-left {
10 | width: 50%;
11 | padding-right: 2em; }
12 | .Pad .PadLayer .level-left .PadMidi .padName {
13 | transition: background-color 300ms linear; }
14 | .Pad .PadLayer .level-left .PadMidi .padName.playing {
15 | transition: background-color 30ms linear; }
16 | .Pad .PadLayer .level-left .MidiNote {
17 | margin-left: -1em; }
18 | .Pad .PadLayer .level-left .MidiNote select {
19 | padding-left: 2em !important;
20 | padding-right: .25em !important; }
21 | .Pad .PadLayer .level-left .MidiNote .select::after {
22 | display: none !important; }
23 |
24 | .Pad .PadLayer .level-left:hover .removeSample {
25 | display: inline-block; }
26 |
27 | .Pad .PadLayer .level-right .modeIcon {
28 | padding-left: .2em; }
29 | .Pad .PadLayer .level-right .modeIcon select {
30 | padding-left: .25em !important;
31 | padding-right: .25em !important; }
32 | .Pad .PadLayer .level-right .modeIcon .select::after {
33 | display: none !important; }
34 |
35 | .Pad .PadLayer .level-right .layerBIcon {
36 | cursor: pointer;
37 | width: 1.5em;
38 | padding: .2em 0 0 .4em; }
39 |
40 | .Pad .PadLayer .level-right .value {
41 | position: absolute;
42 | right: 0;
43 | bottom: 0;
44 | font-size: 7px; }
45 |
46 | .Pad .layerB {
47 | margin: 0 3em 0 0; }
48 | .Pad .layerB .level-left {
49 | width: 85%; }
50 | .Pad .layerB .padName {
51 | width: 12.9em;
52 | padding: .5em;
53 | border-radius: 5px;
54 | text-align: right; }
55 |
56 | .Pad .Sample {
57 | width: 50%;
58 | border: 1px solid #FBFBFF;
59 | border-radius: 5px; }
60 | .Pad .Sample > div {
61 | width: 100%; }
62 | .Pad .Sample .sampleContainer {
63 | position: relative; }
64 | .Pad .Sample .sampleContainer .dragContainer {
65 | display: inline-block;
66 | width: 100%; }
67 | .Pad .Sample .sampleContainer .panel-block.sample {
68 | border-radius: 6px;
69 | padding: .2em .5em;
70 | width: 100%; }
71 | .Pad .Sample .sampleContainer .sampleNameContainer {
72 | width: 100%;
73 | text-align: left; }
74 | .Pad .Sample .sampleContainer .sampleName {
75 | text-align: left;
76 | text-overflow: ellipsis;
77 | overflow: hidden;
78 | white-space: nowrap; }
79 | .Pad .Sample .sampleContainer .removeSample {
80 | display: none;
81 | position: absolute;
82 | top: .3em;
83 | right: -2em; }
84 |
85 | @media (min-width: 950px) {
86 | .Pad .PadLayer .level-left, .Pad .Sample {
87 | width: 55%; } }
88 |
89 | @media (min-width: 1000px) {
90 | .Pad .PadLayer .level-left, .Pad .Sample {
91 | width: 60%; } }
92 |
93 | .Pad .sampleDrop {
94 | position: relative;
95 | width: 100%;
96 | height: 100%;
97 | margin-bottom: .24em; }
98 |
99 | .Pad .sampleDropHighlight {
100 | position: absolute;
101 | top: 0;
102 | left: 0;
103 | height: 100%;
104 | width: 100%;
105 | z-index: 1;
106 | opacity: 0.5; }
107 |
--------------------------------------------------------------------------------
/src/component/Pad/LayerB.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 |
5 | /* App imports */
6 | import { PadErrors, PadErrorStrings } from 'const'
7 | import { updatePadIntProperty, updatePadProperty } from 'actions/pad'
8 |
9 | /* Component imports */
10 | import SamplePlayerComponent from 'component/SamplePlayer'
11 | import SampleComponent from 'component/Sample'
12 | import PadNameComponent from 'component/Pad/PadName'
13 | import VelocityComponent from 'component/Pad/Velocity'
14 |
15 | const PadLayerBComponent = (props) => {
16 | let pad = props.pad;
17 | let midiProps = {
18 | note: pad.midiNote,
19 | min: pad.velocityMinB,
20 | max: pad.velocityMaxB
21 | }
22 |
23 | return (
24 |
25 |
26 |
30 |
31 |
32 |
35 | {props.removePadSample(null)}}
41 | />
42 |
43 |
44 |
45 |
46 |
47 |
48 | props.updatePadIntProperty('velocityMinB', value)}
54 | onChangeMax={(value) => props.updatePadIntProperty('velocityMaxB', value)} />
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | const mapStateToProps = (state, ownProps) => {
62 | let pad = state.pads[ownProps.padId];
63 | let velocityTooltip = "Velocity: " + pad.velocityMinB + "-" + pad.velocityMaxB;
64 |
65 | let hasVelocityError = false;
66 | if (pad.errors.indexOf(PadErrors.VELOCITY_SWAPPED_B) > -1) {
67 | hasVelocityError = true;
68 | velocityTooltip = "Error: " + PadErrorStrings.VELOCITY_SWAPPED_B;
69 | } else if (pad.errors.indexOf(PadErrors.VELOCITY_TOO_HIGH_B) > -1) {
70 | hasVelocityError = true;
71 | velocityTooltip = "Error: " + PadErrorStrings.VELOCITY_TOO_HIGH_B;
72 | }
73 |
74 | return {
75 | pad: pad,
76 | sampleFile: pad.fileNameB,
77 | hasVelocityError: hasVelocityError,
78 | velocityTooltip: velocityTooltip
79 | }
80 | }
81 |
82 | const mapDispatchToProps = (dispatch, ownProps) => {
83 | return {
84 | updatePadIntProperty: (property, value) => {
85 | dispatch(updatePadIntProperty(ownProps.padId, property, value));
86 | },
87 | removePadSample: (value) => {
88 | dispatch(updatePadProperty(ownProps.padId, 'fileNameB', null));
89 | },
90 | }
91 | }
92 |
93 | export default connect(mapStateToProps, mapDispatchToProps)(PadLayerBComponent)
94 |
--------------------------------------------------------------------------------
/src/component/SamplePlayer.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react'
3 | import { v1 as uuidv1 } from 'uuid';
4 | import update from 'immutability-helper'
5 |
6 | /* App imports */
7 | import SampleStore from 'util/sampleStore'
8 |
9 | /* Electron imports */
10 | const { wav, midi } = window.api
11 |
12 |
13 | class SamplePlayerComponent extends React.Component {
14 |
15 | /*
16 | * @constructor
17 | * @param {Object} props
18 | */
19 | constructor(props) {
20 | super(props)
21 |
22 | this.state = {
23 | wavStack: [],
24 | player: null,
25 | playingSample: false
26 | }
27 |
28 | this.renderChildren = this.renderChildren.bind(this)
29 | this.playOrStopSample = this.playOrStopSample.bind(this)
30 |
31 | this.handlerId = uuidv1()
32 | this.addMidiHandler()
33 | }
34 |
35 | renderChildren() {
36 | return React.Children.map(this.props.children, child => {
37 | return React.cloneElement(child, {
38 | playOrStopSample: this.playOrStopSample,
39 | playingSample: this.state.playingSample
40 | })
41 | })
42 | }
43 |
44 | render() {
45 | return (
46 |
47 | {this.renderChildren()}
48 |
49 | )
50 | }
51 |
52 | stopSample() {
53 | for (let i = 0; i < this.state.wavStack.length; i++) {
54 | wav.stopWavFile(this.state.wavStack[i])
55 | }
56 |
57 | this.setState({
58 | playingSample: false,
59 | wavStack: []
60 | })
61 | }
62 |
63 | playSample() {
64 | let wavId = uuidv1()
65 |
66 | this.setState(update(this.state, {
67 | playingSample: {$set: true},
68 | wavStack: {$push: [wavId]}
69 | }))
70 |
71 | wav.playWavFile(wavId,SampleStore.getFileNameOnDisk(this.props.sampleFile))
72 | .then(() => {
73 | this.setState(update(this.state, {
74 | playingSample: {$set: (this.state.wavStack.length > 1)},
75 | wavStack: {$splice: [[this.state.wavStack.indexOf(wavId), 1]]}
76 | }))
77 | })
78 | }
79 |
80 | playOrStopSample() {
81 | if (this.state.playingSample) {
82 | this.stopSample()
83 | return
84 | }
85 |
86 | this.playSample()
87 | }
88 |
89 | componentDidUpdate(prevProps, prevState, snapshot) {
90 | if (this.props.midi) {
91 | if (prevProps.sampleFile !== this.props.sampleFile ||
92 | prevProps.midi.note !== this.props.midi.note ||
93 | prevProps.midi.min !== this.props.midi.min ||
94 | prevProps.midi.max !== this.props.midi.max) {
95 |
96 | this.removeMidiHandler(prevProps.midi.note)
97 | this.addMidiHandler()
98 | }
99 | }
100 | }
101 |
102 | componentWillUnmount() {
103 | if (this.props.midi) {
104 | this.removeMidiHandler(this.props.midi.note)
105 | }
106 | }
107 |
108 | addMidiHandler() {
109 | if (this.props.sampleFile && this.props.midi) {
110 | midi.addMidiNoteOnHandler(this.handlerId, this.props.midi.note, this.props.midi.min, this.props.midi.max, (e) => {
111 | this.playSample()
112 | })
113 | }
114 | }
115 |
116 | removeMidiHandler(note) {
117 | if (this.handlerId) {
118 | midi.removeMidiNoteOnHandler(this.handlerId, note)
119 | }
120 | }
121 | }
122 |
123 | export default SamplePlayerComponent
124 |
--------------------------------------------------------------------------------
/src/css/Pad.scss:
--------------------------------------------------------------------------------
1 | .Pad {
2 |
3 | .level {
4 | margin-bottom:0 !important;
5 | }
6 |
7 | .layerA {
8 | .padName {
9 | width: 9em;
10 | padding: .5em;
11 | border-radius: 5px;
12 | }
13 | }
14 |
15 | .PadLayer .level-left {
16 | width: 50%;
17 | padding-right: 2em;
18 |
19 | .PadMidi {
20 | .padName {
21 | transition: background-color 300ms linear;
22 | }
23 | .padName.playing {
24 | transition: background-color 30ms linear;
25 | }
26 | }
27 |
28 | .MidiNote {
29 | margin-left: -1em;
30 |
31 | select {
32 | padding-left: 2em !important;
33 | padding-right: .25em !important;
34 | }
35 | .select::after {
36 | display: none !important;
37 | }
38 | }
39 | }
40 |
41 | .PadLayer .level-left:hover {
42 | .removeSample {
43 | display: inline-block;
44 | }
45 | }
46 |
47 | .PadLayer .level-right {
48 | .modeIcon {
49 | padding-left: .2em;
50 |
51 | select {
52 | padding-left: .25em !important;
53 | padding-right: .25em !important;
54 | }
55 | .select::after {
56 | display: none !important;
57 | }
58 | }
59 |
60 | .layerBIcon {
61 | cursor: pointer;
62 | width: 1.5em;
63 | padding: .2em 0 0 .4em;
64 | }
65 |
66 | .value {
67 | position: absolute;
68 | right: 0;
69 | bottom: 0;
70 | font-size: 7px;
71 | }
72 | }
73 |
74 | .layerB {
75 | margin: 0 3em 0 0;
76 |
77 | .level-left {
78 | width: 85%;
79 | }
80 |
81 | .padName {
82 | width: 12.9em;
83 | padding: .5em;
84 | border-radius: 5px;
85 | text-align: right;
86 | }
87 | }
88 |
89 |
90 | .Sample {
91 | width: 50%;
92 | border: 1px solid #FBFBFF;
93 | border-radius: 5px;
94 |
95 | > div {
96 | width: 100%;
97 | }
98 |
99 | .sampleContainer {
100 | position: relative;
101 |
102 | .dragContainer {
103 | display: inline-block;
104 | width: 100%;
105 | }
106 |
107 | .panel-block.sample {
108 | border-radius: 6px;
109 | padding: .2em .5em;
110 | width: 100%;
111 | }
112 |
113 | .sampleNameContainer {
114 | width: 100%;
115 | text-align: left;
116 | }
117 |
118 | .sampleName {
119 | text-align: left;
120 | text-overflow: ellipsis;
121 | overflow: hidden;
122 | white-space: nowrap;
123 | }
124 |
125 | .removeSample {
126 | display: none;
127 | position: absolute;
128 | top: .3em;
129 | right: -2em;
130 | }
131 | }
132 | }
133 |
134 | @media (min-width: 950px) {
135 | .PadLayer .level-left, .Sample {
136 | width: 55%;
137 | }
138 | }
139 | @media (min-width: 1000px) {
140 | .PadLayer .level-left, .Sample {
141 | width: 60%;
142 | }
143 | }
144 |
145 | .sampleDrop {
146 | position: relative;
147 | width: 100%;
148 | height: 100%;
149 | margin-bottom: .24em;
150 | }
151 | .sampleDropHighlight {
152 | position: absolute;
153 | top: 0;
154 | left: 0;
155 | height: 100%;
156 | width: 100%;
157 | z-index: 1;
158 | opacity: 0.5;
159 | }
160 | }
--------------------------------------------------------------------------------
/src/component/Modal.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 |
5 | /* App imports */
6 | import { confirmFileOverwriteAction, confirmLoadCardAction } from 'actions/modal'
7 |
8 | const ModalComponent = (props) => {
9 | return (
10 |
11 | {props.showConfirmOverwrite &&
12 |
13 |
14 |
15 |
16 |
19 |
20 |
Saving the kit with this name will overwrite an existing kit. Are you sure you want to do this?
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | }
34 |
35 | {props.showConfirmLoadCard &&
36 |
37 |
38 |
39 |
40 |
43 |
44 |
You will lose any unsaved kit data when you load another card. Are you sure you want to do this?
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | }
58 |
59 | );
60 | }
61 |
62 | const mapStateToProps = (state, ownProps) => {
63 | return {
64 | showConfirmOverwrite: state.modals.confirmOverwriteVisible,
65 | showConfirmLoadCard: state.modals.confirmLoadCardVisible
66 | }
67 | }
68 |
69 | const mapDispatchToProps = (dispatch, ownProps) => {
70 | return {
71 | closeConfirmOverwrite: () => {
72 | dispatch(confirmFileOverwriteAction(false));
73 | },
74 | dispatchCallbackAndCloseConfirmOverwrite: () => {
75 | dispatch(confirmFileOverwriteAction(true));
76 | },
77 | closeConfirmLoadCard: () => {
78 | dispatch(confirmLoadCardAction(false));
79 | },
80 | dispatchCallbackAndCloseLoadCard: () => {
81 | dispatch(confirmLoadCardAction(true));
82 | }
83 | }
84 | }
85 |
86 | export default connect(mapStateToProps, mapDispatchToProps)(ModalComponent)
--------------------------------------------------------------------------------
/public/rendererApi/midi.js:
--------------------------------------------------------------------------------
1 | const WebMidi = require('webmidi')
2 |
3 | /* @var midi note => {handlerId => {min, max, callback},...} */
4 | let noteOnCallbacks = {}
5 |
6 | /** @var track if WebMidi is actively running */
7 | let enabled = false
8 |
9 | /**
10 | * Enable WebMidi
11 | */
12 | const enable = () => {
13 | WebMidi.enable(function (err) {
14 | if (err) {
15 | console.error("WebMidi could not be enabled.", err);
16 | }
17 |
18 | enabled = true
19 | })
20 | }
21 |
22 | /**
23 | * Get a list of midi devices
24 | * @return {Tuple} [input index, input name]
25 | */
26 | const getInputList = () => {
27 | return WebMidi.inputs.map((input, index) => {
28 | return [index, input.name]
29 | });
30 | }
31 |
32 | /**
33 | * Bind note on listeners for the given midi device
34 | */
35 | const bindMidiInput = (inputIndex) => {
36 | if (!enabled) {
37 | return
38 | }
39 |
40 | // remove any current listeners
41 | for (let i = 0; i < WebMidi.inputs.length; i++) {
42 | WebMidi.inputs[i].removeListener();
43 | }
44 |
45 | if (inputIndex === null) {
46 | // no midi device selected
47 | return
48 | }
49 |
50 | // add midi listener to selected device
51 | var input = WebMidi.inputs[inputIndex];
52 | input.addListener('noteon', "all",
53 | function (e) {
54 | let noteOnCallbackList = noteOnCallbacks[e.note.number]
55 | if (noteOnCallbackList) {
56 |
57 | for (let handlerId of Object.keys(noteOnCallbackList)) {
58 | let noteOnCallback = noteOnCallbackList[handlerId]
59 | if (e.rawVelocity >= noteOnCallback.min && e.rawVelocity <= noteOnCallback.max) {
60 | noteOnCallback.callback(e);
61 | }
62 | }
63 | }
64 | }
65 | );
66 | }
67 |
68 | // Enable WebMidi as soon as called
69 | enable()
70 |
71 | module.exports = {
72 | enable: enable,
73 | getInputList: getInputList,
74 | bindMidiInput: bindMidiInput,
75 |
76 | /**
77 | * Assure WebMidi is enabled, regenerate the list of midi devices
78 | * regenerate the midi menu with the new inputs
79 | */
80 | scanForMidiDevices: () => {
81 | // deselect any current device
82 | bindMidiInput(null)
83 |
84 | // assure WebMidi is active
85 | enable()
86 | },
87 |
88 | /**
89 | * Whenever the selcted midi device has a note on event, call the given callback
90 | * @param {String} handlerId - a unique id for this handler, necessary for destroying it
91 | * @param {Number} note - the midi note to listen for
92 | * @param {Number} min - the min velocity to detect
93 | * @param {Number} max - the max velocity to detect
94 | * @param {Function} callback
95 | */
96 | addMidiNoteOnHandler: (handlerId, note, min, max, callback) => {
97 | if (!noteOnCallbacks[note]){
98 | noteOnCallbacks[note] = {}
99 | }
100 |
101 | noteOnCallbacks[note][handlerId] = {
102 | min: min,
103 | max: max,
104 | callback: callback
105 | }
106 | },
107 |
108 | /**
109 | * remove midi note on handler
110 | * @param {String} handlerId - the unique id reference of the handler
111 | * @param {Number} note - the midi note being listened for
112 | */
113 | removeMidiNoteOnHandler: (handlerId, note) => {
114 | if (noteOnCallbacks[note] && noteOnCallbacks[note][handlerId]) {
115 | delete noteOnCallbacks[note][handlerId]
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/component/SampleList.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 |
5 | /* App imports */
6 | import { Drive } from 'const'
7 | import SampleStore from 'util/sampleStore'
8 |
9 | /* Component imports */
10 | import SampleComponent from 'component/Sample'
11 | import SamplePlayerComponent from 'component/SamplePlayer'
12 | import 'css/SampleList.css'
13 |
14 | class SampleList extends React.Component {
15 | /*
16 | * @constructor
17 | * @param {Object} props
18 | */
19 | constructor(props) {
20 | super(props);
21 |
22 | this.state = {
23 | filter: ""
24 | };
25 |
26 | this.filterSamples = this.filterSamples.bind(this);
27 | }
28 |
29 | filterSamples(filter) {
30 | this.setState({filter: filter})
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
88 |
89 | );
90 | }
91 | }
92 |
93 | const mapStateToProps = (state, ownProps) => {
94 | return {
95 | samples: state.drive.samples
96 | }
97 | }
98 |
99 | const mapDispatchToProps = (dispatch, ownProps) => {
100 | return {
101 | importSamples: () => {
102 | dispatch(SampleStore.importSamples());
103 | }
104 | }
105 | }
106 |
107 | export default connect(mapStateToProps, mapDispatchToProps)(SampleList)
108 |
--------------------------------------------------------------------------------
/src/util/storage.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import { WaveFile } from 'wavefile'
3 |
4 | /* App imports */
5 | import { Drive } from 'const'
6 | import { getBuffer } from 'util/buffer'
7 | import { getKitFileBuffer } from 'util/kitFile'
8 |
9 | /* Electron imports */
10 | const { store, fs, path } = window.api
11 |
12 | /**
13 | * Store the directory for next time the app opens
14 | * @param {String} directory
15 | */
16 | export function storeLastLoadedDirectory(directory) {
17 | store.save('lastLoadedDirectory', directory);
18 | }
19 |
20 | /**
21 | * @return {String} the directory used last time the application was open
22 | */
23 | export function getLastLoadedDirectory() {
24 | return store.get('lastLoadedDirectory');
25 | }
26 |
27 | /**
28 | * Copy a source sample to the sample directory
29 | * @param {String} source
30 | * @return {String} new sample file path
31 | * @return {String} new sample file name
32 | */
33 | export function copySample(source, destinationDirectory, newFileName=false) {
34 | let sourcePath = path.parse(source);
35 |
36 | if (!newFileName) {
37 | newFileName = sourcePath.base;
38 | }
39 |
40 | let destination = destinationDirectory + "/" + newFileName;
41 |
42 | try {
43 | // The samples files must be 16-bit, mono or stereo .WAV files.
44 | // with a sample rate of 48K, 44.1K, 32K, 22.05K, and 11.025K.
45 | let buffer = getBuffer(source);
46 | let wav = new WaveFile(buffer);
47 |
48 | if (parseInt(wav.bitDepth, 10) !== 16) {
49 | wav.toBitDepth("16");
50 | }
51 |
52 | if (![48000,44100,32000,22050,11025].includes(wav.fmt.sampleRate)) {
53 | wav.toSampleRate(44100);
54 | }
55 |
56 | fs.writeFile(destination, wav.toBuffer());
57 | return destination;
58 | } catch (err) {
59 | console.error(err)
60 | throw(err);
61 | }
62 | }
63 |
64 | /**
65 | * @param {KitModel} kit
66 | * @param {Boolean} asNew
67 | * @returns {Boolean} true if the kit would clobber another
68 | */
69 | export const kitWillOverwriteExisting = (kit, asNew = false) => {
70 | if(!fs.exists(kit.filePath)) {
71 | return false;
72 | }
73 |
74 | let desiredFileName = kit.kitName + Drive.KIT_EXTENSION;
75 | let currentFileName = kit.fileName;
76 | let kitFile = kit.filePath + "/" + desiredFileName;
77 |
78 | if (!currentFileName || currentFileName.toUpperCase() !== desiredFileName.toUpperCase()) {
79 | return fs.exists(kitFile);
80 | }
81 |
82 | return false;
83 | }
84 |
85 | /**
86 | * @param {RootModel} drive
87 | * @param {KitModel} kit
88 | * @param {PadModel[]} pads
89 | * @param {Boolean} asNew
90 | * @returns {String} the file name the kit was stored as
91 | */
92 | export const saveKitToFile = (drive, kit, pads, asNew = false) => {
93 | if(!fs.exists(kit.filePath)) {
94 | // need to create the kits directory
95 | fs.mkdir(kit.filePath);
96 | }
97 |
98 | let desiredFileName = kit.kitName + Drive.KIT_EXTENSION;
99 | let currentFileName = kit.fileName;
100 |
101 | if (asNew) {
102 | // the kit is being stored as a new kit, drop the reference to the old file
103 | currentFileName = null;
104 | }
105 |
106 | let kitFile = kit.filePath + "/" + desiredFileName;
107 | try {
108 | if (currentFileName && desiredFileName !== currentFileName && !asNew) {
109 | // we need to rename the file first
110 | fs.renameFile(kit.filePath + "/" + currentFileName, kitFile);
111 | }
112 |
113 | fs.writeFile(kitFile, getKitFileBuffer(drive, kit, pads))
114 | } catch (err) {
115 | console.error(err);
116 | throw(err);
117 | }
118 |
119 | return desiredFileName;
120 | }
121 |
--------------------------------------------------------------------------------
/src/state/models.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import { v1 as uuidv1 } from 'uuid';
3 |
4 | export const NoticeModel = (style = "", text = "") => {
5 | return {
6 | id: uuidv1(),
7 | style: style,
8 | text: text
9 | };
10 | }
11 |
12 | export const RootModel = (deviceId = "", deviceType = "", rootPath = "", kitPath = "", samples = []) => {
13 | return {
14 | deviceId: deviceId,
15 | deviceType: deviceType,
16 | rootPath: rootPath,
17 | kitPath: kitPath,
18 | samples: samples
19 | };
20 | }
21 |
22 | export const Sample = (fileName = "", fileNameOnDisk = "") => {
23 | return {
24 | fileName: fileName,
25 | fileNameOnDisk: fileNameOnDisk
26 | };
27 | }
28 |
29 | export const KitModel = (filePath = "", fileName = "", isNew = false, isExisting = false, isLoaded = false, kitName = "", pads = []) => {
30 | return {
31 | id: uuidv1(),
32 | isNew: isNew,
33 | isExisting: isExisting,
34 | isLoaded: isLoaded,
35 | filePath: filePath,
36 | fileName: fileName,
37 | kitName: kitName,
38 | originalKitName: kitName,
39 | pads: pads,
40 | errors: []
41 | };
42 | }
43 |
44 | export class PadModel {
45 | static getPad(padType = "", location = 1, level = 10, tune = 0, pan = 0, reverb = 0, midiNote = 0, mode = 1, sensitivity = 1, mgrp = 0, velocityMin = 0, velocityMax = 127, fileName = "", velocityMinB = 0, velocityMaxB = 127, fileNameB = "") {
46 | return {
47 | id: uuidv1(),
48 | padType: padType,
49 | location: location,
50 | level: level,
51 | tune: tune,
52 | pan: pan,
53 | reverb: reverb,
54 | midiNote: midiNote,
55 | mode: mode,
56 | sensitivity: sensitivity,
57 | mgrp: mgrp,
58 | velocityMin: velocityMin,
59 | velocityMax: velocityMax,
60 | fileName: fileName,
61 | velocityMinB: velocityMinB,
62 | velocityMaxB: velocityMaxB,
63 | fileNameB: fileNameB,
64 | errors: []
65 | };
66 | }
67 |
68 | /**
69 | * Map file values to display values
70 | */
71 | static fromFile(padType = "", location = "", level = 0, tune = 0, pan = 0, reverb = 0, midiNote = 0, mode = 0, sensitivity = 1, mgrp = 0, velocityMin = 0, velocityMax = 127, fileName = "", velocityMinB = 0, velocityMaxB = 127, fileNameB = "") {
72 | return this.getPad(
73 | padType,
74 | location,
75 | level,
76 | this.getUIntDisplayValue(tune),
77 | this.getUIntDisplayValue(pan),
78 | reverb,
79 | midiNote,
80 | mode,
81 | this.getSensitivityDisplayValue(sensitivity),
82 | mgrp,
83 | velocityMin,
84 | velocityMax,
85 | fileName,
86 | velocityMinB,
87 | velocityMaxB,
88 | fileNameB
89 | );
90 | }
91 |
92 | /*
93 | * used for values: -4 to +4 (unsigned int: 252,253,254,255,0,1,2,3,4)
94 | * @returns {Number}
95 | */
96 | static getUIntDisplayValue(value) {
97 | // todo: theres definitely a better way to convert uint8 to signed int here, right?
98 | if (value >= 252) {
99 | return (-1 * (256 - value));
100 | }
101 |
102 | return value;
103 | }
104 |
105 | /*
106 | * @param {Number} value
107 | */
108 | static getUIntFileValue(value) {
109 | let uint = parseInt(value, 10);
110 |
111 | if (uint < 0) {
112 | return (256 + uint);
113 | } else {
114 | return (uint);
115 | }
116 | }
117 |
118 | /*
119 | * sensitivity display is 1 to 8
120 | * @returns {Number}
121 | */
122 | static getSensitivityDisplayValue(sensitivity) {
123 | switch (sensitivity) {
124 | case 0x0b:
125 | return 1;
126 | case 0x0e:
127 | return 2;
128 | case 0x11:
129 | return 3;
130 | case 0x14:
131 | return 4;
132 | case 0x17:
133 | return 5;
134 | case 0x1a:
135 | return 6;
136 | case 0x1d:
137 | return 7;
138 | case 0x20:
139 | return 8;
140 | default:
141 | return 1;
142 | }
143 | }
144 |
145 | /*
146 | * @param {Number} value
147 | */
148 | static getSensitivityFileValue(sensitivity) {
149 | switch (sensitivity) {
150 | case 1:
151 | return 0x0b;
152 | case 2:
153 | return 0x0e;
154 | case 3:
155 | return 0x11;
156 | case 4:
157 | return 0x14;
158 | case 5:
159 | return 0x17;
160 | case 6:
161 | return 0x1a;
162 | case 7:
163 | return 0x1d;
164 | case 8:
165 | return 0x20;
166 | default:
167 | return 0x00;
168 | }
169 | }
170 |
171 | }
172 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Development
2 | This is only tested with the Alesis SampleRack, which should be functioning well. It _should_ work with a Alesis SamplePad Pro but I do not have one to test on. If you have one, please help me test it out!
3 |
4 | # SamplePad Kit Editor
5 | 
6 | 
7 |
8 | ## Getting SamplePad Kit Editor
9 | SamplePad Kit Editor is released via the GitHub [releases](https://github.com/LesserChance/samplepad-editor/releases) page.
10 |
11 | The current version is v.6
12 | [Download v.6 for Mac](https://github.com/LesserChance/samplepad-editor/releases/download/v0.6/SamplePad.Kit.Editor-0.6.0-mac.zip)
13 | [Download v.6 for PC](https://github.com/LesserChance/samplepad-editor/releases/download/v0.6/SamplePad.Kit.Editor.Setup.0.6.0.exe)
14 | [Download v.6 for Linux](https://github.com/LesserChance/samplepad-editor/releases/download/v0.6/samplepad-editor-0.6.0.tar.gz)
15 |
16 | ## Overview
17 | SamplePad Kit Editor is an application that allows you to create and edit custom drum kits for the [Alesis SamplePad Pro](https://www.alesis.com/products/view2/samplepad-pro) or [Alesis SampleRack](https://www.alesis.com/products/view2/samplerack) directly on your Mac or PC. It uses an intuitive drap and drop interface to quickly and easily create custom drum kits. All pad parameters are available to edit, and it's significantly easier than scrolling through all the details on the SamplePad itself.
18 |
19 |
20 |
21 | ## How to use SamplePad Kit Editor
22 | ### Getting Started
23 | 1. Open SamplePad Kit Editor
24 | 1. Insert your SamplePad SD card into your computer
25 | 1. Select your device type (SamplePad Pro or SampleRack) from the Edit menu
26 | 1. Click the "Load SD Card" button (or select "Load SD Card" in the Edit menu) and select the root directory of the SD card
27 |
28 | This will load any existing samples and kits on the SD card for you to edit. It's generally best to start with a blank SD card, as that allows for better filename organization (see "importing samples" below.)
29 |
30 | ### Importing Samples
31 | Imported samples are automatically formatted as appropriate (16bit, 44.1k 8-character wav.) After importing samples through SamplePad Kit Editor you'll still see the original file name (in the case that its greater than 8 characters) allowing for better, more intuitive sample organization.
32 | 1. Click import samples at the bottom of the sample section
33 | 1. Select one or more .wav files, or a directory
34 | 1. If a directory is selected all wav files located within it (or any sub directories) will be imported
35 |
36 | ### Editing a Kit
37 | 1. Select a kit from the dropdown to edit an existing kit. You can also import a kit from an existing `.kit` not on the SD card, or create a kit from scratch
38 | 1. Search through samples on the left, and drag them on to an individual pad to set that pad's sample
39 | 1. Tweak pad parameters by twisting knobs (click, hold, and drag an icon up and down to adjust values)
40 | 1. Change mute groups by selecting 1 of 16 possible groups
41 | 1. Access Layer B sample and velocity parameters by clicking on the dropdown icon on the right
42 |
43 | Once you're satisfied, save your kit. Saving it as a new kit will create a new `.kit` file and not affect the original. Put it into your SamplePad and try it out!
44 |
45 | ### Midi Control
46 | While creating or editing a kit, you can easily test it out by connecting a midi controller to your computer. Once connected, select a device through the menu `Edit > Midi Settings` (or `Scan for Midi Devices` if it does not appear.) Playing midi notes on the selected device will show you which pad is being played and triggers the sample attached to it. Triggers are velocity dependent, and layer B is supported.
47 |
48 | ## Helping Out
49 | ### Testing
50 | Samplepad Kit Editor is currently prerelease. I've only been able to test on my own SampleRack so I'm not sure if the `.kit` file format differs on other machines or firmware versions. If you have a SamplePad, Samplepad Pro, or SampleRack, please let me know if you try it out and experience any issues. Make sure to back up your SD card just in case!
51 |
52 | I'd also love to know if everything works fine for you. You can contact me through my [email on github](https://github.com/LesserChance).
53 |
54 | ### Compiling
55 | Want to build the app yourself?
56 |
57 | ```
58 | git clone https://github.com/LesserChance/samplepad-editor.git
59 | cd samplepad-editor
60 | yarn install
61 | yarn run electron-dev
62 | ```
63 |
--------------------------------------------------------------------------------
/public/mainApi/menu.js:
--------------------------------------------------------------------------------
1 | const { app, Menu } = require('electron')
2 | const rendererProcessEvents = require('../events/rendererProcessEvents')
3 | const { DeviceType } = require('../const')
4 | const isDev = require('electron-is-dev');
5 |
6 | // default, configurable menus
7 | let midiMenu = {
8 | label: 'Midi Settings',
9 | id: 'midi-settings',
10 | submenu: [
11 | {
12 | checked: true,
13 | type: "radio",
14 | label: "-Midi Off-"
15 | },
16 | {
17 | type: 'separator'
18 | },
19 | {
20 | label: "Scan for Midi Devices",
21 | click() {
22 | rendererProcessEvents.selectMidiScan()
23 | }
24 | }
25 | ]
26 | }
27 |
28 | let deviceMenu = {
29 | label: 'Device Type',
30 | id: 'device-settings',
31 | submenu: [
32 | {
33 | checked: true,
34 | type: "radio",
35 | label: "SampleRack",
36 | click() {
37 | rendererProcessEvents.selectDeviceType(DeviceType.SAMPLERACK)
38 | }
39 | },
40 | {
41 | checked: false,
42 | type: "radio",
43 | label: "SamplePad Pro",
44 | click() {
45 | rendererProcessEvents.selectDeviceType(DeviceType.SAMPLEPAD_PRO)
46 | }
47 | }
48 | ]
49 | };
50 |
51 | const getMenuTemplate = () => {
52 | const template = []
53 |
54 | if (process.platform === 'darwin') {
55 | template.push({
56 | label: app.name,
57 | submenu: [
58 | { role: 'about' },
59 | { type: 'separator' },
60 | { role: 'services', submenu: [] },
61 | { type: 'separator' },
62 | { role: 'hide' },
63 | { role: 'hideothers' },
64 | { role: 'unhide' },
65 | { type: 'separator' },
66 | { role: 'quit' }
67 | ]
68 | })
69 | }
70 |
71 | template.push({
72 | label: 'Edit',
73 | submenu: [
74 | {
75 | label: 'Load SD Card',
76 | click() {
77 | rendererProcessEvents.loadSDCard()
78 | }
79 | },
80 | midiMenu,
81 | deviceMenu
82 | ]
83 | });
84 |
85 | let viewSubmenus = [
86 | { type: 'separator' },
87 | { role: 'resetzoom' },
88 | { role: 'zoomin' },
89 | { role: 'zoomout' },
90 | { type: 'separator' },
91 | { role: 'togglefullscreen' }
92 | ]
93 |
94 | if (isDev || !isDev) {
95 | viewSubmenus.unshift({
96 | label: 'Toggle Developer Tools',
97 | accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
98 | click (item, focusedWindow) {
99 | if (focusedWindow) focusedWindow.webContents.toggleDevTools()
100 | }
101 | })
102 |
103 | viewSubmenus.unshift({
104 | label: 'Reload',
105 | accelerator: 'CmdOrCtrl+R',
106 | click (item, focusedWindow) {
107 | if (focusedWindow) focusedWindow.reload()
108 | }
109 | })
110 | }
111 |
112 | template.push({
113 | label: 'View',
114 | submenu: viewSubmenus
115 | });
116 |
117 | template.push({
118 | role: 'window'
119 | });
120 |
121 | template.push({
122 | role: 'help',
123 | submenu: [
124 | {
125 | label: 'Learn More',
126 | click () { require('electron').shell.openExternal('https://github.com/LesserChance/samplepad-editor') }
127 | }
128 | ]
129 | })
130 |
131 | return template
132 | }
133 |
134 | module.exports = {
135 | getMenuTemplate: getMenuTemplate,
136 | regenerateMidiMenu: (midiInputs, currentMidiInput) => {
137 | if (!midiInputs) {
138 | midiInputs = []
139 | }
140 |
141 | midiMenu = {
142 | label: 'Midi Settings',
143 | id: 'midi-settings',
144 | submenu: [
145 | {
146 | checked: (currentMidiInput === null),
147 | type: "radio",
148 | label: "-Midi Off-",
149 | click() {
150 | rendererProcessEvents.selectMidiInput(null)
151 | }
152 | }
153 | ]
154 | }
155 |
156 | for (let i = 0; i < midiInputs.length; i++) {
157 | let midiInput = midiInputs[i]
158 | midiMenu.submenu.push({
159 | checked: (currentMidiInput === midiInput[0]),
160 | type: "radio",
161 | label: midiInput[1],
162 | click() {
163 | rendererProcessEvents.selectMidiInput(midiInput[0])
164 | }
165 | })
166 | }
167 |
168 |
169 | midiMenu.submenu.unshift({
170 | type: 'separator'
171 | });
172 |
173 | midiMenu.submenu.unshift({
174 | label: "Scan for Midi Devices",
175 | click() {
176 | rendererProcessEvents.selectMidiScan()
177 | }
178 | });
179 |
180 | const newMenu = Menu.buildFromTemplate(getMenuTemplate())
181 | Menu.setApplicationMenu(newMenu)
182 | },
183 | rengenerateDeviceMenu: (deviceType) => {
184 | deviceMenu.submenu[0].checked = (deviceType == DeviceType.SAMPLERACK);
185 | deviceMenu.submenu[1].checked = (deviceType == DeviceType.SAMPLEPAD_PRO);
186 |
187 | const newMenu = Menu.buildFromTemplate(getMenuTemplate())
188 | Menu.setApplicationMenu(newMenu)
189 | }
190 | }
--------------------------------------------------------------------------------
/src/state/reducers.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import { combineReducers } from 'redux'
3 | import update from 'immutability-helper';
4 |
5 | /* App imports */
6 | import { Actions, DeviceType } from 'const'
7 | import { getGlobalStateFromDirectory } from 'state/globalState';
8 | import { getSortedKitIds } from 'state/sortModels';
9 | import { getLastLoadedDirectory } from 'util/storage';
10 |
11 | let lastLoadedDirectory = getLastLoadedDirectory();
12 | let initialState = {
13 | modals: {
14 | confirmOverwriteVisible: false,
15 | confirmOverwriteCallback: null,
16 | confirmLoadCardVisible: false,
17 | confirmLoadCardCallback: null
18 | },
19 | notices: [],
20 | drive: {
21 | deviceType: DeviceType.SAMPLERACK
22 | },
23 | kits: {
24 | ids: [],
25 | models: {}
26 | }
27 | }
28 |
29 | if (lastLoadedDirectory) {
30 | try {
31 | let loadState = getGlobalStateFromDirectory(lastLoadedDirectory);
32 | initialState.drive = loadState.drive;
33 | initialState.kits.ids = getSortedKitIds(loadState.kits);
34 | initialState.kits.models = loadState.kits;
35 | } catch (err) {
36 | // ignore failed load
37 | }
38 | }
39 |
40 | const initialModalState = initialState.modals;
41 | const initialAppState = {
42 | selectedKitId: null,
43 | activeKitId: null
44 | };
45 | const initialDriveState = {
46 | deviceId: initialState.drive.deviceId,
47 | deviceType: initialState.drive.deviceType,
48 | rootPath: initialState.drive.rootPath,
49 | kitPath: initialState.drive.kitPath,
50 | samples: initialState.drive.samples
51 | };
52 | const initialKitsState = initialState.kits;
53 |
54 | function app(state = initialAppState, action) {
55 | switch (action.type) {
56 | case Actions.SET_SELECTED_KIT_ID:
57 | return update(state, {
58 | selectedKitId: {$set: action.kitId}
59 | });
60 |
61 | case Actions.SET_ACTIVE_KIT_ID:
62 | return update(state, {
63 | activeKitId: {$set: action.kitId}
64 | });
65 |
66 | default:
67 | return state;
68 | }
69 | }
70 |
71 | function notices(state = [], action) {
72 | switch (action.type) {
73 | case Actions.SHOW_NOTICE:
74 | return update(state, {
75 | $push: [action.notice]
76 | });
77 |
78 | default:
79 | return state;
80 | }
81 | }
82 |
83 | function modals(state = initialModalState, action) {
84 | switch (action.type) {
85 | case Actions.SHOW_MODAL_CONFIRM_OVERWRITE:
86 | return update(state, {
87 | confirmOverwriteVisible: {$set: true},
88 | confirmOverwriteCallback: {$set: action.callback}
89 | });
90 |
91 | case Actions.HIDE_MODAL_CONFIRM_OVERWRITE:
92 | return update(state, {
93 | confirmOverwriteVisible: {$set: false}
94 | });
95 |
96 | case Actions.SHOW_MODAL_CONFIRM_LOAD_CARD:
97 | return update(state, {
98 | confirmLoadCardVisible: {$set: true},
99 | confirmLoadCardCallback: {$set: action.callback}
100 | });
101 |
102 | case Actions.HIDE_MODAL_CONFIRM_LOAD_CARD:
103 | return update(state, {
104 | confirmLoadCardVisible: {$set: false}
105 | });
106 |
107 | default:
108 | return state;
109 | }
110 | }
111 |
112 | function drive(state = initialDriveState, action) {
113 | switch (action.type) {
114 | // load data into state from the SD card
115 | case Actions.LOAD_DRIVE:
116 | return update(state, {
117 | deviceId: {$set: action.drive.deviceId},
118 | deviceType: {$set: action.drive.deviceType},
119 | rootPath: {$set: action.drive.rootPath},
120 | kitPath: {$set: action.drive.kitPath},
121 | samples: {$set: action.drive.samples}
122 | });
123 |
124 | case Actions.SET_DEVICE_TYPE:
125 | return update(state, {
126 | deviceType: {$set: action.deviceType},
127 | });
128 |
129 | case Actions.RESET_SAMPLES:
130 | return update(state, {
131 | samples: {$set: action.samples}
132 | });
133 |
134 | default:
135 | return state;
136 | }
137 | }
138 |
139 | function kits(state = initialKitsState, action) {
140 | switch (action.type) {
141 | // load the list of kits into state from the SD card
142 | case Actions.RESET_KITS:
143 | return update(state, {
144 | ids: {$set: Object.keys(action.kits)},
145 | models: {$set: action.kits}
146 | });
147 |
148 | case Actions.ADD_KIT:
149 | return update(state, {
150 | ids: {$push: [action.kit.id]},
151 | models: {[action.kit.id]: {$set: action.kit}}
152 | });
153 |
154 | case Actions.UPDATE_KIT_PROPERTY:
155 | return update(state, {
156 | models: {
157 | [action.kitId]: {
158 | [action.property]: {$set: action.value}
159 | }
160 | }
161 | });
162 |
163 | case Actions.UPDATE_KIT_STATE:
164 | return update(state, {
165 | models: {
166 | [action.kitId]: {$merge: action.newState}
167 | }
168 | });
169 |
170 | // kits are sorted by name and isNew - if a kit would have one
171 | // of these properties changed, you MUST sort kits afterwards
172 | case Actions.SORT_KITS:
173 | return update(state, {
174 | ids: { $set: getSortedKitIds(state.models)}
175 | });
176 |
177 | default:
178 | return state;
179 | }
180 | }
181 |
182 | function pads(state = {}, action) {
183 | switch (action.type) {
184 | case Actions.ADD_PADS:
185 | return update(state, {$merge: action.pads});
186 |
187 | case Actions.ADD_PAD:
188 | return update(state, {
189 | [action.pad.id]: {$set: action.pad}
190 | });
191 |
192 | case Actions.UPDATE_PAD_PROPERTY:
193 | return update(state, {
194 | [action.padId]: {
195 | [action.property]: {$set: action.value}
196 | }
197 | });
198 |
199 | default:
200 | return state;
201 | }
202 | }
203 |
204 | export default combineReducers({
205 | app,
206 | modals,
207 | notices,
208 | drive,
209 | kits,
210 | pads
211 | });
212 |
--------------------------------------------------------------------------------
/src/component/Pad/LayerA.js:
--------------------------------------------------------------------------------
1 | /* Global imports */
2 | import React from 'react';
3 | import { connect } from 'react-redux'
4 |
5 | /* App imports */
6 | import { MidiMap, PadErrors, PadErrorStrings } from 'const'
7 | import { updatePadIntProperty, updatePadProperty, updatePadSensitivity } from 'actions/pad'
8 |
9 | /* Component imports */
10 | import SamplePlayerComponent from 'component/SamplePlayer'
11 | import SampleComponent from 'component/Sample'
12 | import PadNameComponent from 'component/Pad/PadName'
13 | import MidiNoteSelectComponent from 'component/Pad/MidiNoteSelect'
14 | import KnobComponent from 'component/Pad/KnobControl'
15 | import SlideComponent from 'component/Pad/SlideControl'
16 | import VelocityComponent from 'component/Pad/Velocity'
17 | import MuteGroupComponent from 'component/Pad/MuteGroup'
18 |
19 | const PadLayerAComponent = (props) => {
20 |
21 | let pad = props.pad;
22 | let padName = MidiMap[props.deviceType][pad.padType][0]
23 | let midiProps = {
24 | note: pad.midiNote,
25 | min: pad.velocityMin,
26 | max: pad.velocityMax
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
37 |
38 |
39 | props.updatePadIntProperty("midiNote", midiNote)} />
44 |
45 |
46 |
47 |
50 | {props.removePadSample(null)}}
56 | />
57 |
58 |
59 |
60 |
61 |
62 |
63 |
props.updatePadIntProperty("tune", value)}/>
71 |
72 | props.updatePadSensitivity(value)}/>
81 |
82 | props.updatePadIntProperty("pan", value)}/>
90 |
91 | props.updatePadIntProperty("reverb", value)}/>
100 |
101 | props.updatePadIntProperty("level", value)}/>
110 |
111 |
112 |
113 |
114 |
115 |
126 |
127 |
128 |
129 |
130 |
131 | props.updatePadIntProperty('velocityMin', value)}
137 | onChangeMax={(value) => props.updatePadIntProperty('velocityMax', value)} />
138 |
139 | 0 ? pad.mgrp : 'off')}
143 | onChange={(value) => props.updatePadIntProperty('mgrp', value)} />
144 |
145 |
153 |
154 |
155 |
156 |
157 |
158 |
159 | );
160 | }
161 |
162 | const mapStateToProps = (state, ownProps) => {
163 | let pad = state.pads[ownProps.padId];
164 | let velocityTooltip = "Velocity: " + pad.velocityMin + "-" + pad.velocityMax;
165 |
166 | let hasVelocityError = false;
167 | if (pad.errors.indexOf(PadErrors.VELOCITY_SWAPPED_A) > -1) {
168 | hasVelocityError = true;
169 | velocityTooltip = "Error: " + PadErrorStrings.VELOCITY_SWAPPED_A;
170 | } else if (pad.errors.indexOf(PadErrors.VELOCITY_TOO_HIGH_A) > -1) {
171 | hasVelocityError = true;
172 | velocityTooltip = "Error: " + PadErrorStrings.VELOCITY_TOO_HIGH_A;
173 | }
174 |
175 | let hasLayerBError = false;
176 | if (pad.errors.indexOf(PadErrors.VELOCITY_SWAPPED_B) > -1) {
177 | hasLayerBError = true;
178 | } else if (pad.errors.indexOf(PadErrors.VELOCITY_TOO_HIGH_B) > -1) {
179 | hasLayerBError = true;
180 | }
181 |
182 | return {
183 | pad: pad,
184 | deviceType: state.drive.deviceType,
185 | hasVelocityError: hasVelocityError,
186 | velocityTooltip: velocityTooltip,
187 | hasLayerBError: hasLayerBError
188 | }
189 | }
190 |
191 | const mapDispatchToProps = (dispatch, ownProps) => {
192 | return {
193 | updatePadIntProperty: (property, value) => {
194 | dispatch(updatePadIntProperty(ownProps.padId, property, value));
195 | },
196 | removePadSample: (value) => {
197 | dispatch(updatePadProperty(ownProps.padId, 'fileName', null));
198 | },
199 | updatePadSensitivity: (value) => {
200 | dispatch(updatePadSensitivity(ownProps.padId, value));
201 | }
202 | }
203 | }
204 |
205 | export default connect(mapStateToProps, mapDispatchToProps)(PadLayerAComponent)
206 |
--------------------------------------------------------------------------------
/src/actions/kit.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { Actions, MidiMap, KitErrors } from 'const';
3 | import { confirmFileOverwrite } from 'actions/modal';
4 | import { showNotice } from 'actions/notice';
5 | import { KitModel, PadModel } from 'state/models';
6 | import { openKitFileDialog } from 'util/fileDialog';
7 | import { getKitAndPadsFromFile } from 'util/kitFile';
8 | import { saveKitToFile, kitWillOverwriteExisting } from 'util/storage';
9 |
10 | /** KIT ACTION CREATORS */
11 | /**
12 | * Open a file dialog, and parse the resulting file as a SamplePad kit
13 | */
14 | export function importKitFromFile() {
15 | return (dispatch, getState) => {
16 | openKitFileDialog()
17 | .then(result => {
18 | if (result.canceled) {
19 | return null;
20 | }
21 |
22 | // catch an error, maybe an invalid file?
23 | let state = getState();
24 | try {
25 | let {kit, pads} = getKitAndPadsFromFile(state.drive, result.filePaths[0]);
26 |
27 | // remove the filename, as a new one will get created
28 | kit.filePath = state.drive.kitPath;
29 | kit.fileName = null;
30 |
31 | dispatch({ type: Actions.ADD_PADS, pads: pads });
32 | dispatch({ type: Actions.ADD_KIT, kit: kit });
33 | dispatch({ type: Actions.SET_SELECTED_KIT_ID, kitId: kit.id });
34 | dispatch({ type: Actions.SET_ACTIVE_KIT_ID, kitId: kit.id });
35 | dispatch({ type: Actions.SORT_KITS });
36 | } catch (err) {
37 | console.error(err);
38 | dispatch(
39 | showNotice("is-danger", "There was a problem importing this kit.")
40 | );
41 | return null;
42 | }
43 |
44 | })
45 | }
46 | }
47 | /**
48 | * load kit details from kit file
49 | * @param {String} kitId
50 | */
51 | export function loadKitDetails(kitId) {
52 | return (dispatch, getState) => {
53 | let state = getState();
54 | let kit = state.kits.models[kitId];
55 |
56 | if (!kit.isLoaded) {
57 | let kitFile = kit.filePath + "/" + kit.fileName;
58 |
59 | try {
60 | let result = getKitAndPadsFromFile(state.drive, kitFile);
61 |
62 | dispatch({ type: Actions.ADD_PADS, pads: result.pads });
63 | dispatch(updateKitState(kitId, {
64 | "isLoaded": true,
65 | "kitName": result.kit.kitName,
66 | "pads": result.kit.pads
67 | }));
68 | dispatch({ type: Actions.SORT_KITS });
69 | } catch (err) {
70 | // failed kit load - load a default empty kit
71 | dispatch(loadNewKit({
72 | id: kit.id,
73 | isNew: kit.isNew,
74 | isExisting: kit.isExisting,
75 | isLoaded: kit.isLoaded,
76 | filePath: kit.filePath,
77 | fileName: kit.fileName,
78 | kitName: kit.kitName,
79 | originalKitName: kit.kitName,
80 | }));
81 | }
82 | }
83 | }
84 | }
85 | /**
86 | * create an empty kit and add it to the kit list
87 | */
88 | export function loadNewKit(presetData=null) {
89 | return (dispatch, getState) => {
90 | let state = getState();
91 |
92 | // create a default set of samples
93 | let pads = {};
94 |
95 | Object.keys(MidiMap[state.drive.deviceType]).forEach((padType) => {
96 | let midiNote = MidiMap[state.drive.deviceType][padType][1];
97 | let pad = PadModel.getPad(padType)
98 | pad.midiNote = midiNote;
99 | pads[pad.id] = pad;
100 | });
101 |
102 | let kit = KitModel(
103 | state.drive.kitPath,
104 | null,
105 | true,
106 | false,
107 | true,
108 | "",
109 | Object.keys(pads)
110 | );
111 |
112 | if (presetData) {
113 | // right now this is only used in the case where an existing kit load failure occurs
114 | kit = {...kit, ...presetData};
115 | }
116 |
117 | dispatch({ type: Actions.ADD_PADS, pads: pads });
118 | dispatch({ type: Actions.ADD_KIT, kit: kit });
119 | dispatch({ type: Actions.SET_SELECTED_KIT_ID, kitId: kit.id });
120 | dispatch({ type: Actions.SET_ACTIVE_KIT_ID, kitId: kit.id });
121 | dispatch({ type: Actions.SORT_KITS });
122 | }
123 | }
124 | /**
125 | * save a kit to disk
126 | * @param {boolean} asNew=false - if true, save this kit as a new kit on disk, do not overwrite the exitings
127 | */
128 | export function saveKit(kitId, asNew=false, confirmedOverwrite=false) {
129 | return (dispatch, getState) => {
130 | let state = getState();
131 | let kit = state.kits.models[kitId];
132 |
133 | // validate the kit
134 | if (kit.errors.length) {
135 | dispatch(
136 | showNotice("is-danger", "Cannot Save. Please correct all errors before saving the kit.")
137 | );
138 | return;
139 | }
140 |
141 | // validate the pads
142 | for (let i = 0; i < kit.pads.length; i++) {
143 | let pad = state.pads[kit.pads[i]];
144 | if (pad.errors.length) {
145 | dispatch(
146 | showNotice("is-danger", "Cannot Save. Please correct all errors before saving the kit.")
147 | );
148 | return;
149 | }
150 | }
151 |
152 | // if this kit would overwrite an existing one - confirm first
153 | if (!confirmedOverwrite) {
154 | let confirm = kitWillOverwriteExisting(kit, asNew);
155 |
156 | if (confirm) {
157 | // show the warning modal - on confirm, save the kit
158 | dispatch(confirmFileOverwrite((result) => {
159 | return (dispatch, getState) => {
160 | if (result) {
161 | dispatch(saveKit(kitId, asNew, true));
162 | }
163 | }
164 | }));
165 | return;
166 | }
167 | }
168 |
169 | let fileName = "";
170 | try {
171 | fileName = saveKitToFile(state.drive, kit, state.pads, asNew);
172 | } catch (err) {
173 | dispatch(
174 | showNotice("is-danger", "There was a problem saving the kit.")
175 | );
176 | return;
177 | }
178 |
179 | dispatch(updateKitState(kitId, {
180 | isNew: false,
181 | isExisting: true,
182 | originalKitName: kit.kitName,
183 | fileName: fileName
184 | }));
185 | dispatch({ type: Actions.SORT_KITS });
186 | dispatch(
187 | showNotice("is-success", "Kit saved.")
188 | );
189 | }
190 | }
191 | /**
192 | * Update the kit name and filename
193 | * @param {String} kitId
194 | * @param {?} value
195 | */
196 | export function updateKitName(kitId, value) {
197 | return (dispatch, getState) => {
198 | dispatch(updateKitProperty(kitId, 'kitName', value));
199 | dispatch({ type: Actions.SORT_KITS });
200 | }
201 | }
202 | /**
203 | * Update an individual property of a kit
204 | * @param {String} kitId
205 | * @param {String} property
206 | * @param {?} value
207 | */
208 | export function updateKitProperty(kitId, property, value) {
209 | return (dispatch, getState) => {
210 | dispatch({ type: Actions.UPDATE_KIT_PROPERTY, kitId: kitId, property: property, value: value });
211 | dispatch(validateKit(kitId));
212 | }
213 | }
214 | /**
215 | * Update multiple properties of a kit
216 | * @param {String} kitId
217 | * @param {json} newState
218 | */
219 | export function updateKitState(kitId, newState) {
220 | return (dispatch, getState) => {
221 | dispatch({ type: Actions.UPDATE_KIT_STATE, kitId: kitId, newState: newState });
222 | dispatch(validateKit(kitId));
223 | }
224 | }
225 | /**
226 | * Validate all kit params, update the kit state with any new errors
227 | * @param {String} kitId
228 | */
229 | export function validateKit(kitId) {
230 | return (dispatch, getState) => {
231 | let state = getState();
232 | let kit = state.kits.models[kitId];
233 | let prevErrors = kit.errors;
234 | let errors = [];
235 |
236 | if (!/^[a-z0-9]+$/i.test(kit.kitName)) {
237 | errors.push(KitErrors.INVALID_KIT_NAME)
238 | }
239 |
240 | if (prevErrors.length || errors.length) {
241 | dispatch({ type: Actions.UPDATE_KIT_PROPERTY, kitId: kitId, property: 'errors', value: errors });
242 | }
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/src/const.js:
--------------------------------------------------------------------------------
1 | export const Actions = {
2 | /* drive action types */
3 | LOAD_DRIVE: 'LOAD_DRIVE',
4 | SET_DEVICE_TYPE: 'SET_DEVICE_TYPE',
5 | RESET_SAMPLES: 'RESET_SAMPLES',
6 |
7 | /* kit action types */
8 | SORT_KITS: 'SORT_KITS',
9 | RESET_KITS: 'RESET_KITS',
10 | ADD_KIT: 'ADD_KIT',
11 | UPDATE_KIT_PROPERTY: 'UPDATE_KIT_PROPERTY',
12 | UPDATE_KIT_STATE: 'UPDATE_KIT_STATE',
13 |
14 | /* pad action types */
15 | ADD_PADS: 'ADD_PADS',
16 | ADD_PAD: 'ADD_PAD',
17 | UPDATE_PAD_PROPERTY: 'UPDATE_PAD_PROPERTY',
18 |
19 | /* app action types */
20 | SET_SELECTED_KIT_ID: 'SET_SELECTED_KIT_ID',
21 | SET_ACTIVE_KIT_ID: 'SET_ACTIVE_KIT_ID',
22 |
23 | /* modal action types */
24 | SHOW_MODAL_CONFIRM_OVERWRITE: 'SHOW_MODAL_CONFIRM_OVERWRITE',
25 | HIDE_MODAL_CONFIRM_OVERWRITE: 'HIDE_MODAL_CONFIRM_OVERWRITE',
26 | SHOW_MODAL_CONFIRM_LOAD_CARD: 'SHOW_MODAL_CONFIRM_LOAD_CARD',
27 | HIDE_MODAL_CONFIRM_LOAD_CARD: 'HIDE_MODAL_CONFIRM_LOAD_CARD',
28 |
29 | /* notice action types */
30 | SHOW_NOTICE: 'SHOW_NOTICE'
31 | };
32 |
33 | export const PadErrors = {
34 | VELOCITY_SWAPPED_A: 'VELOCITY_SWAPPED_A',
35 | VELOCITY_TOO_HIGH_A: 'VELOCITY_TOO_HIGH_A',
36 | VELOCITY_SWAPPED_B: 'VELOCITY_SWAPPED_B',
37 | VELOCITY_TOO_HIGH_B: 'VELOCITY_TOO_HIGH_B',
38 | DUPLICATE_MIDI_NOTE: 'DUPLICATE_MIDI_NOTE',
39 | };
40 |
41 | export const PadErrorStrings = {
42 | VELOCITY_SWAPPED_A: 'Velocity must be in order (min-max)',
43 | VELOCITY_TOO_HIGH_A: 'Velocity values must be 127 or lower',
44 | VELOCITY_SWAPPED_B: 'Velocity must be in order (min-max)',
45 | VELOCITY_TOO_HIGH_B: 'Velocity values must be 127 or lower',
46 | DUPLICATE_MIDI_NOTE: 'Midi Note must be unique'
47 | };
48 |
49 | export const KitErrors = {
50 | INVALID_KIT_NAME: 'INVALID_KIT_NAME',
51 | };
52 |
53 | export const KitErrorStrings = {
54 | INVALID_KIT_NAME: 'Kit name contains invalid characters (A-Z, a-z, 0-9 only)',
55 | };
56 |
57 | export const DragItemTypes = {
58 | SAMPLE: 'sample'
59 | };
60 |
61 | export const Drive = {
62 | DEVICE_ID_FILE: ".sampleeditordevice",
63 | SAMPLE_FILE_TYPE: "wav",
64 | SAMPLE_EXTENSION: ".wav",
65 | KIT_EXTENSION: ".KIT",
66 | KIT_FILE_TYPE: "KIT",
67 | KIT_DIRECTORY: "KITS",
68 | MAX_SAMPLES: 511,
69 | MAX_FILENAME_LENGTH: 8
70 | };
71 |
72 | export const DeviceType = {
73 | SAMPLEPAD_PRO: 'samplepad_pro',
74 | SAMPLERACK: 'samplerack'
75 | }
76 |
77 | export const KitBuffer = {
78 | CHECKSUM_BYTE: 0x08,
79 |
80 | // pads are written to the kit file in this order
81 | PAD_FILE_ORDER: {
82 | [DeviceType.SAMPLEPAD_PRO]: [
83 | 'pad_01', 'pad_02', 'pad_03', 'pad_04', 'pad_05', 'pad_06', 'pad_07', 'pad_08', 'ext_1a', 'ext_1b',
84 | 'ext_2', 'kick', 'hh_ope', 'hh_mid', 'hh_clo', 'hh_chk', 'hh_spl'
85 | ],
86 | [DeviceType.SAMPLERACK]: [
87 | 'snr_a', 'snr_b', 'tom1a', 'tom1b', 'tom2a', 'tom2b', 'tom3a', 'tom3b', 'cr1a', 'cr1b', 'cr2a', 'cr2b', 'ridea',
88 | 'ride2', 'rideb', 'kick', 'hha_op', 'hhb_op', 'hha_md', 'hhb_md', 'hha_cl', 'hhb_cl', 'hh_chk', 'hh_spl'
89 | ],
90 | },
91 |
92 | /* Map of each pad to its memory block start location: {pad: [block1_start, block2_start]} */
93 | PAD_MEMORY_BLOCK_LOCATIONS: {
94 | [DeviceType.SAMPLERACK]: {
95 | kick: [0x0F80, 0x2780],
96 | snr_a: [0x0080, 0x1880],
97 | snr_b: [0x0180, 0x1980],
98 | tom1a: [0x0280, 0x1A80],
99 | tom1b: [0x0380, 0x1B80],
100 | tom2a: [0x0480, 0x1C80],
101 | tom2b: [0x0580, 0x1D80],
102 | tom3a: [0x0680, 0x1E80],
103 | tom3b: [0x0780, 0x1F80],
104 | cr1a: [0x0880, 0x2080],
105 | cr1b: [0x0980, 0x2180],
106 | cr2a: [0x0A80, 0x2280],
107 | cr2b: [0x0B80, 0x2380],
108 | ridea: [0x0C80, 0x2480],
109 | ride2: [0x0D80, 0x2580],
110 | rideb: [0x0E80, 0x2680],
111 | hha_op: [0x1080, 0x2880],
112 | hha_md: [0x1280, 0x2A80],
113 | hha_cl: [0x1480, 0x2C80],
114 | hhb_op: [0x1180, 0x2980],
115 | hhb_md: [0x1380, 0x2B80],
116 | hhb_cl: [0x1580, 0x2D80],
117 | hh_chk: [0x1680, 0x2E80],
118 | hh_spl: [0x1780, 0x2F80]
119 | },
120 |
121 | [DeviceType.SAMPLEPAD_PRO]: {
122 | pad_01: [0x0080, 0x1180],
123 | pad_02: [0x0180, 0x1280],
124 | pad_03: [0x0280, 0x1380],
125 | pad_04: [0x0380, 0x1480],
126 | pad_05: [0x0480, 0x1580],
127 | pad_06: [0x0580, 0x1680],
128 | pad_07: [0x0680, 0x1780],
129 | pad_08: [0x0780, 0x1880],
130 | ext_1a: [0x0880, 0x1980],
131 | ext_1b: [0x0980, 0x1A80],
132 | ext_2: [0x0A80, 0x1B80],
133 | kick: [0x0B80, 0x1C80],
134 | hh_ope: [0x0C80, 0x1D80],
135 | hh_mid: [0x0D80, 0x1E80],
136 | hh_clo: [0x0E80, 0x1F80],
137 | hh_chk: [0x0F80, 0x2080],
138 | hh_spl: [0x1080, 0x2180]
139 | }
140 | },
141 |
142 | /* Map of each parameters memory start location in its param block */
143 | PAD_PARAM_MEMORY_LOCATION: [
144 | {
145 | 'location': 0x07,
146 | 'level': 0x29,
147 | 'tune': 0x2d,
148 | 'pan': 0x31,
149 | 'reverb': 0x35,
150 | 'midiNote': 0x39,
151 | 'mode': 0x3d,
152 | 'sensitivity':0x41,
153 | 'mgrp': 0x49
154 | },
155 | {
156 | 'level': 0x29,
157 | 'tune': 0x2d,
158 | 'pan': 0x31,
159 | 'reverb': 0x35,
160 | 'midiNote': 0x39,
161 | 'mode': 0x3d,
162 | 'sensitivity': 0x41,
163 | 'mgrp': 0x49,
164 | 'velocityMin': 0x82,
165 | 'velocityMax': 0x83,
166 | 'fileNameLength': 0x87,
167 | 'fileName': 0x90,
168 | 'velocityMinB': 0xa2,
169 | 'velocityMaxB': 0xa3,
170 | 'fileNameLengthB':0xa7,
171 | 'fileNameB': 0xb0,
172 | }
173 | ],
174 |
175 | /*
176 | * Map of which params to read from which blocks
177 | * leaves out filenames, since they require the length to read
178 | */
179 | PAD_PARAM_READ_BLOCKS: [
180 | ['location','level','tune','pan','reverb','midiNote','mode','sensitivity','mgrp'],
181 | ['velocityMin','velocityMax','fileNameLength','velocityMinB','velocityMaxB','fileNameLengthB']
182 | ]
183 | };
184 |
185 | /*
186 | * Map pad types to the pad name and default midi note
187 | * This is also used to determine the order we want to display the pads
188 | */
189 | export const MidiMap = {
190 | [DeviceType.SAMPLERACK]: {
191 | kick: ["Bass Drum 1", 36],
192 | snr_a: ["Snare Drum 1", 38],
193 | snr_b: ["Snare Drum 2", 40],
194 | tom1a: ["High Tom 2", 48],
195 | tom1b: ["High Tom 1", 50],
196 | tom2a: ["Mid Tom 2", 45],
197 | tom2b: ["Mid Tom 1", 47],
198 | tom3a: ["Low Tom 1", 43],
199 | tom3b: ["Low Tom 2", 58],
200 | cr1a: ["Crash Cymbal 1", 49],
201 | cr1b: ["Splash Cymbal", 55],
202 | cr2a: ["Crash Cymbal 2", 57],
203 | cr2b: ["Chinese Cymbal", 52],
204 | ridea: ["Ride Cymbal 1", 51],
205 | rideb: ["Ride Bell", 53],
206 | ride2: ["Ride Cymbal 2", 59],
207 | hha_op: ["Open Hi-hat 1", 46],
208 | hha_md: ["Mid Hi-hat 1", 23],
209 | hha_cl: ["Closed Hi-hat 1", 42],
210 | hhb_op: ["Open Hi-hat 2", 26],
211 | hhb_md: ["Mid Hi-hat 2", 24],
212 | hhb_cl: ["Closed Hi-hat 2", 22],
213 | hh_chk: ["Pedal Hi-hat", 44],
214 | hh_spl: ["Spl Hi-hat 1", 21]
215 | },
216 |
217 | [DeviceType.SAMPLEPAD_PRO]: {
218 | pad_01: ["Pad 1", 49],
219 | pad_02: ["Pad 2", 51],
220 | pad_03: ["Pad 3", 48],
221 | pad_04: ["Pad 4", 45],
222 | pad_05: ["Pad 5", 46],
223 | pad_06: ["Pad 6", 36],
224 | pad_07: ["Pad 7", 38],
225 | pad_08: ["Pad 8", 42],
226 | ext_1a: ["External Pad 1A", 38],
227 | ext_1b: ["External Pad 1B", 40],
228 | ext_2: ["External Pad 2", 43],
229 | kick: ["Kick Pedal", 36],
230 | hh_ope: ["Open Hi-hat", 46],
231 | hh_mid: ["Mid Hi-hat", 23],
232 | hh_clo: ["Closed Hi-hat", 42],
233 | hh_chk: ["Pedal Hi-hat", 44],
234 | hh_spl: ["Spl Hi-hat", 21]
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/docs/kit-format-notes.txt:
--------------------------------------------------------------------------------
1 | Example Pad: Kick
2 | 00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
3 |
4 | BLOCK ONE
5 | 00 00000f80 4b 49 54 49 01 00 00 XX ?? ?? 00 00 00 00 00 ?? |KITI............| (07)location, (08-09)either 00 or aa, (0f)length of string in f90-f97
6 | 01 00000f90 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? |????????........| (00-0f)references a file either internal or external - can be all 0’s if f8f is 0
7 | 02 00000fa0 00 00 00 18 00 09 00 00 00 XX 00 0a 01 XX 04 08 |................| (09)level, (0d)tune
8 | 03 00000fb0 02 XX 04 08 03 XX 00 0a 08 XX 00 7f 09 XX 00 05 |.........%......| (01)pan, (05)reverb, (09)midi_note, (0d)mode
9 | 04 00000fc0 0c XX 00 08 0d 00 00 09 0e XX 00 10 00 00 00 7f |................| (01)sensitivity, (09)mgrp
10 | 05 00000fd0 09 00 00 00 00 00 00 02 10 00 00 00 00 00 03 e7 |................|
11 | 06 00000fe0 11 00 00 00 00 00 03 e7 12 00 03 e7 00 00 03 e7 |................|
12 | 07 00000ff0 13 00 00 00 00 00 00 64 14 00 00 00 00 00 03 e7 |.......d........|
13 | 08 00001000 15 00 00 00 00 00 03 e7 16 00 00 00 00 00 03 e7 |................|
14 | 09 00001010 17 00 00 64 00 00 00 64 18 00 00 00 00 00 00 64 |...d...d.......d|
15 | 0a 00001020 19 00 00 00 00 00 00 64 1a 00 00 00 00 00 00 01 |.......d........|
16 | 0b 00001030 1b 00 00 00 00 00 00 01 0e 00 00 00 00 00 00 10 |................|
17 | 0c 00001040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
18 | 0d 00001050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
19 | 0e 00001060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
20 | 0f 00001170 00 00 00 00 00 00 00 00 ?? ?? ?? ?? ?? ?? ?? ?? |................| (08-0f)sometimes this has a file name in it -- seems like when f88,f89 are aa
21 |
22 | ALTERNATIVE BLOCK ONE, LINE 06-0C - I THINK THIS NEEDS TO BE THIS WHEN THAT OTHER STUFF IS EMPTY
23 | 06 000010d0 09 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 |................|
24 | 07 000010e0 11 00 00 00 00 00 00 00 12 00 00 00 00 00 00 00 |................|
25 | 08 000010f0 13 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00 |................|
26 | 09 00001100 15 00 00 00 00 00 00 00 16 00 00 00 00 00 00 00 |................|
27 | 0a 00001110 17 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00 |................|
28 | 0b 00001120 19 00 00 00 00 00 00 00 1a 00 00 00 00 00 00 00 |................|
29 | 0c 00001130 1b 00 00 00 00 00 00 00 0e 00 00 00 00 00 00 00 |................|
30 |
31 | BLOCK TWO
32 | 00 00002780 4b 49 54 49 01 00 00 00 00 00 00 00 00 00 00 00 |KITI............|
33 | 01 00002790 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
34 | 02 000027a0 00 00 00 18 00 09 00 00 00 XX 00 0a 01 XX 04 08 |................| (09)level, (0d)tune
35 | 03 000027b0 02 XX 04 08 03 XX 00 0a 08 XX 00 7f 09 XX 00 05 |.........%......| (01)pan, (05)reverb, (09)midi_note, (0d)mode
36 | 04 000027c0 0c XX 00 08 0d 00 00 09 0e XX 00 10 00 00 00 00 |................| (01)sensitivity, (09)mgrp
37 | 05 000027d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
38 | 06 000027e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
39 | 07 000027f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
40 | 08 00002800 aa aa XX XX 00 00 00 XX XX XX XX XX XX XX XX XX |........XXXXXXXX| (00-01)either aa or ff, (02)vel_min, vel_max, (07)filename_length, (08-0f)filename_caps
41 | 09 00002810 XX XX XX XX XX XX XX XX 00 00 00 00 00 00 00 00 |xxxxxxxx........| (00-07)filename_on_dir - pad right with 0’s
42 | 0a 00002820 aa aa XX XX 00 00 00 XX XX XX XX XX XX XX XX XX |........XXXXXXXX| (00-01)either aa or ff, (02)vel_min_b, vel_max_b, (07)filename_length_b, (08-0f)filename_caps_b
43 | 0b 00002830 XX XX XX XX XX XX XX XX 00 00 00 00 00 00 00 00 |xxxxxxxx........| (00-07)filename_on_dir_b - pad right with 0’s
44 | 0c 00002840 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
45 | 0d 00002850 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
46 | 0e 00002860 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
47 | 0f 00002870 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
48 |
49 | NOTES
50 | 00000f80 bytes 8 and 9 - these look like aa when 00001170 has a file name - probably just use 00’s
51 | 00000f80 byte f - just use 00
52 | 00000f90 Ive seen strings longer than 8 chars in this space, but theyre not used if f8f is 0 - fill it with 0’s
53 | 00002800, 00002820 bytes 0 and 1 - these look like theyre supposed to be aa when using an external file
54 |
55 |
56 |
57 |
58 |
59 | FILE ORGANIZATION:
60 | [HEADER - 8 rows]
61 | 00000000 4b 49 54 48 00 80 00 00 eb 00 00 18 00 00 00 00 |KITH............| (checksum is byte 8 - sum bytes 9+ of the file, mod by 256)
62 | 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
63 | 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
64 | 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
65 | 00000040 00 00 00 00 00 00 00 08 41 63 6f 75 73 74 69 63 |........Acoustic| (this is probably string length at byte 7, then the kit name)
66 | 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
67 | 00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
68 | 00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
69 |
70 | [pads - block 1 in this order]
71 | snr_a,snr_b,tom1a,tom1b,tom2a,tom2b,tom3a,tom3b,cr1a,cr1b,cr2a,cr2b,ridea
72 |
73 | [SOME MYSTERY PAD OR SOMETHING - 16 rows - this looks like a normal pad, can probably zero stuff out as in a pad]
74 | 00000d80 4b 49 54 49 01 00 00 00 00 00 00 00 00 00 00 04 |KITI............|
75 | 00000d90 4d 75 74 65 00 48 20 4f 00 00 00 00 00 00 00 00 |Mute.H O........|
76 | 00000da0 00 c7 00 18 00 09 00 00 00 08 00 0a 01 00 04 08 |................|
77 | 00000db0 02 02 04 08 03 03 00 0a 08 3b 00 7f 09 01 00 05 |.........;......|
78 | 00000dc0 0c 14 00 08 0d 00 00 09 0e 00 00 10 00 00 00 7f |................|
79 | 00000dd0 09 00 00 00 00 00 00 02 10 00 00 00 00 00 03 e7 |................|
80 | 00000de0 11 00 00 00 00 00 03 e7 12 00 03 e7 00 00 03 e7 |................|
81 | 00000df0 13 00 00 00 00 00 00 64 14 00 00 00 00 00 03 e7 |.......d........|
82 | 00000e00 15 00 00 00 00 00 03 e7 16 00 00 00 00 00 03 e7 |................|
83 | 00000e10 17 00 00 64 00 00 00 64 18 00 00 00 00 00 00 64 |...d...d.......d|
84 | 00000e20 19 00 00 00 00 00 00 64 1a 00 00 00 00 00 00 01 |.......d........|
85 | 00000e30 1b 00 00 00 00 00 00 01 0e 00 00 00 00 00 00 10 |................|
86 | 00000e40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
87 | 00000e50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
88 | 00000e60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
89 | 00000e70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
90 |
91 | [pads - block 1 in this order]
92 | rideb,kick,hha_op,hhb_op,hha_md,hhb_md,hha_cl,hhb_cl,hh_chk,hh_spl
93 |
94 | [pads - block 2 in this order]
95 | snr_a,snr_b,tom1a,tom1b,tom2a,tom2b,tom3a,tom3b,cr1a,cr1b,cr2a,cr2b,ridea
96 |
97 | [I DONT KNOW WHAT THIS PART IS - 16 rows]
98 | 00002580 4b 49 54 49 01 00 00 00 00 00 00 00 00 00 00 00 |KITI............|
99 | 00002590 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
100 | 000025a0 00 00 00 18 00 09 00 00 00 08 00 0a 01 00 04 08 |................|
101 | 000025b0 02 02 04 08 03 03 00 0a 08 3b 00 7f 09 01 00 05 |.........;......|
102 | 000025c0 0c 14 00 08 0d 00 00 09 0e 00 00 10 00 00 00 00 |................|
103 | 000025d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
104 | 000025e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
105 | 000025f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
106 | 00002600 ff ff 00 7f 00 00 00 00 00 00 00 00 00 00 00 00 |................|
107 | 00002610 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
108 | 00002620 ff ff 00 7f 00 00 00 00 00 00 00 00 00 00 00 00 |................|
109 | 00002630 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
110 | 00002640 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
111 | 00002650 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
112 | 00002660 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
113 | 00002670 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
114 |
115 | [pads - block 2 in this order]
116 | rideb,kick,hha_op,hhb_op,hha_md,hhb_md,hha_cl,hhb_cl,hh_chk,hh_spl
117 |
--------------------------------------------------------------------------------
/src/util/sampleStore.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { Actions, Drive } from 'const'
3 | import { showNotice } from 'actions/notice'
4 | import { Sample } from 'state/models'
5 | import { openSampleFileDialog } from 'util/fileDialog'
6 | import { copySample } from 'util/storage'
7 |
8 | /* Electron imports */
9 | const { store, fs, path } = window.api
10 |
11 | /**
12 | * responsible for managing sample file and display names
13 | *
14 | * if a display name would be overwritten, its ok to overwrite its equivalent filename
15 | * : kick.wav (stored as kick1.wav), when uploading another kick.wav should overwrite
16 | *
17 | * but dont overwrite files that have been truncated
18 | * : TR-909-KICK.wav (stored as TR-909-1.wav), when uploading a file called TR-909-1.wav
19 | * it'd have to get renamed to not overwrite on disk
20 | */
21 | class SampleStore {
22 |
23 | constructor(settings) {
24 | /** @var deviceId => path */
25 | this.devicePaths = store.get('devicePaths') || {}
26 |
27 | /** @var deviceId => {fileName => fileNameOnDisk} */
28 | this.samples = store.get('samples') || {}
29 |
30 | /** @var all the samples on the current device {fileName => fileNameOnDisk} */
31 | this.deviceSamples = {}
32 | }
33 |
34 | getSamples() {
35 | return this.deviceSamples
36 | }
37 |
38 | getFileNameOnDisk(fileName) {
39 | return this.devicePath + "/" + this.deviceSamples[fileName]
40 | }
41 |
42 | /**
43 | * Get the string to write to a kit file for the given file
44 | * @param {String} fileName
45 | * @return {String}
46 | */
47 | getWriteFileName(fileName) {
48 | if (!fileName) {
49 | return ""
50 | }
51 |
52 | return this.deviceSamples[fileName].replace(Drive.SAMPLE_EXTENSION, "")
53 | }
54 |
55 | getFileNameFromKitFile(fileName) {
56 | if (!fileName) {
57 | return ""
58 | }
59 |
60 | let reverseList = this._getFlippedDeviceSamples()
61 |
62 | if (reverseList[fileName]) {
63 | return reverseList[fileName]
64 | }
65 |
66 | return fileName
67 | }
68 |
69 | /**
70 | * Open a file dialog, and move the selected files into the drive
71 | */
72 | importSamples() {
73 | return (dispatch, getState) => {
74 | openSampleFileDialog()
75 | .then(result => {
76 | if (result.canceled) {
77 | return null
78 | }
79 |
80 | dispatch(showNotice("is-warning", "Import processing..."))
81 |
82 | let state = getState()
83 | let fileCount = state.drive.samples.length
84 |
85 | // run the import on a timeout to let the notice above show
86 | setTimeout(() => {
87 | let hadCopyError = false
88 |
89 | let parseDirectory = (fileList) => {
90 | fileList.forEach((file) => {
91 | if (fileCount >= Drive.MAX_SAMPLES) {
92 | return
93 | }
94 |
95 | if (fs.isDirectory(file)) {
96 | // read directories recursively
97 | parseDirectory(fs.getFileListFromDirectory(file))
98 | } else {
99 | let samplePath = path.parse(file)
100 |
101 | if (samplePath.ext === Drive.SAMPLE_EXTENSION) {
102 | let sample = this.addSample(samplePath.name, false)
103 |
104 | try {
105 | copySample(file, this.devicePath, sample.fileNameOnDisk)
106 |
107 | // add the sample to the filename lists, so any subsequent copies will know its there
108 | this._addFileReference(sample.fileName)
109 | fileCount++
110 | } catch (err) {
111 | hadCopyError = true
112 | }
113 | }
114 | }
115 | })
116 | }
117 |
118 | parseDirectory(result.filePaths)
119 | this._saveSamples()
120 |
121 | dispatch({ type: Actions.RESET_SAMPLES, samples: Object.keys(this.deviceSamples) })
122 |
123 | if (hadCopyError) {
124 | dispatch(
125 | showNotice("is-warning", "There was a problem importing one or more samples.")
126 | )
127 | } else {
128 | dispatch(
129 | showNotice("is-success", "Samples successfully imported.")
130 | )
131 | }
132 | },100)
133 | })
134 | }
135 | }
136 |
137 | /**
138 | * Given a directory, load the sample list from disk if it already exists
139 | * otherwise read all the samples and store it to disk
140 | * @param {String} devicePath
141 | */
142 | loadSamplesFromDirectory(deviceId, devicePath) {
143 | if (this.samples[deviceId]) {
144 | // if we already have reference to this device, use the stored state
145 | this._loadDevice(deviceId)
146 | } else {
147 | // otherwise load state from disk
148 | this.deviceId = deviceId
149 | this.devicePath = devicePath
150 |
151 | this.deviceSamples = Object.fromEntries(
152 | fs.getSampleFiles(devicePath)
153 | .map((dirent) => {
154 | return [dirent.name, dirent.name]
155 | })
156 | )
157 |
158 | this
159 | ._saveSamples()
160 | ._saveDevicePath(devicePath)
161 | }
162 | }
163 |
164 | /**
165 | * Add a new file to the current device
166 | * @param {String} fileName
167 | */
168 | addSample(fileName, save=true) {
169 | // if the sample already exists, do we want to overwrite it?
170 | if (this._fileExists(fileName)) {
171 | // overwriting a sample - remove references to it so it can be overwritten
172 | this._removeFileReference(fileName)
173 | }
174 |
175 | this.deviceSamples[fileName] = this._getFormattedFileName(fileName)
176 |
177 | if (save) {
178 | this._saveSamples()
179 | }
180 |
181 | return Sample(fileName, this.deviceSamples[fileName])
182 | }
183 |
184 | _getFlippedDeviceSamples() {
185 | let ret = {}
186 | Object.keys(this.deviceSamples).forEach(key => {
187 | ret[this.deviceSamples[key]] = key
188 | })
189 | return ret
190 | }
191 |
192 | _saveSamples() {
193 | this.samples[this.deviceId] = this.deviceSamples
194 | store.save('samples', this.samples)
195 | this._loadFilenames()
196 |
197 | return this
198 | }
199 |
200 | _saveDevicePath(devicePath) {
201 | this.devicePaths[this.deviceId] = devicePath
202 | store.save('devicePaths', this.devicePaths)
203 |
204 | return this
205 | }
206 |
207 | _reset() {
208 | store.save('devicePaths', {})
209 | store.save('samples', {})
210 | }
211 |
212 | _loadDevice(deviceId) {
213 | this.deviceId = deviceId
214 |
215 | // todo: sort the samples by display name here
216 | this.devicePath = this.devicePaths[deviceId]
217 | this.deviceSamples = this.samples[deviceId]
218 | this._loadFilenames()
219 | }
220 |
221 | _loadFilenames() {
222 | /** the file names as stored on disk - this is used for r/w only, not file uniqueness */
223 | this.fileNamesOnDisk = Object.fromEntries(
224 | Object.entries(this.deviceSamples).map(([fileName, fileNameOnDisk]) => [fileNameOnDisk.toLowerCase(), true])
225 | )
226 |
227 | /** the file names displayed to the user - this is used for uniqueness */
228 | this.fileNames = Object.fromEntries(
229 | Object.entries(this.deviceSamples).map(([fileName, fileNameOnDisk]) => [fileName.toLowerCase(), true])
230 | )
231 | }
232 |
233 | _fileExists(fileName) {
234 | return this.fileNames[(fileName).toLowerCase()] || false
235 | }
236 |
237 | _fileExistsOnDisk(fileName) {
238 | return this.fileNamesOnDisk[(fileName).toLowerCase()] || false
239 | }
240 |
241 | _removeFileReference(fileName) {
242 | delete this.fileNamesOnDisk[this.deviceSamples[fileName].toLowerCase()]
243 | delete this.fileNames[(fileName).toLowerCase()]
244 | }
245 |
246 | _addFileReference(fileName) {
247 | this.fileNamesOnDisk[this.deviceSamples[fileName].toLowerCase()] = true
248 | this.fileNames[(fileName).toLowerCase()] = true
249 | }
250 |
251 | _getFormattedFileName(displayName) {
252 | // drop the extension
253 | let fileName = displayName.substr(0, displayName.length - Drive.SAMPLE_EXTENSION.length)
254 |
255 | // remove any non-alphanumeric characters
256 | fileName = fileName.replace(/[^0-9a-z]/gi, '')
257 |
258 | // ideally, use the actual name or some portion of it, truncate it to a max length
259 | fileName = fileName.substr(0, Drive.MAX_FILENAME_LENGTH)
260 | if (!this._fileExistsOnDisk(fileName + Drive.SAMPLE_EXTENSION)) {
261 | return fileName + Drive.SAMPLE_EXTENSION
262 | }
263 |
264 | // the best filename is already taken
265 | // start counting up from 0 and replacing characters at the end of the string
266 | // e.g. bassdrum.wav->bassdru1.wav...bassdr99.wav or kick.wav -> kick1.wav...kick3456.wav
267 | let count = 0
268 | let insertPos = fileName.length
269 |
270 | while (true) {
271 | count = count + 1
272 | let fileCount = '' + count
273 |
274 | while (insertPos > 0 && insertPos + fileCount.length > Drive.MAX_FILENAME_LENGTH) {
275 | insertPos--
276 | }
277 |
278 | if (insertPos === 0) {
279 | //shit, thats a lot of files with the same name, like d9999999.wav and they want to add more?
280 | throw new Error("Cannot import sample file")
281 | }
282 |
283 | let testFileName = fileName.substr(0, insertPos) + fileCount
284 | if (!this._fileExistsOnDisk(testFileName + Drive.SAMPLE_EXTENSION)) {
285 | return testFileName + Drive.SAMPLE_EXTENSION
286 | }
287 | }
288 | }
289 | }
290 |
291 | export default new SampleStore()
292 |
--------------------------------------------------------------------------------
/src/util/kitFile.js:
--------------------------------------------------------------------------------
1 | /* App imports */
2 | import { KitBuffer, Drive } from 'const'
3 | import { KitModel, PadModel } from 'state/models'
4 | import { getSortedPadIds } from 'state/sortModels'
5 | import { getBuffer } from 'util/buffer'
6 | import SampleStore from 'util/sampleStore'
7 |
8 | /* Electron imports */
9 | const { fs, path } = window.api;
10 |
11 | /** @var { Map } memory location within individual memory blocks for each parameter */
12 | const MEMLOC = {
13 | midiNote: 0x39,
14 | location: 0x07,
15 | level: 0x29,
16 | tune: 0x2d,
17 | pan: 0x31,
18 | reverb: 0x35,
19 | mode: 0x3d,
20 | sensitivity: 0x41,
21 | mgrp: 0x49,
22 | velocityMin: 0x82,
23 | velocityMax: 0x83,
24 | velocityMinB: 0xa2,
25 | velocityMaxB: 0xa4,
26 |
27 | hasFileLayerA: 0x80,
28 | fileNameLength: 0x87,
29 | displayName: 0x88,
30 | fileName: 0x90,
31 |
32 | hasFileLayerB: 0xa0,
33 | fileNameBLength: 0xa7,
34 | displayNameB: 0xa8,
35 | fileNameB: 0xb0
36 | }
37 |
38 | /**
39 | * Given a file, get the kit and pads
40 | * @param {RootModel} drive
41 | * @param {String} kitFile - the file to parse
42 | * @return {KitModel, PadModel[]} kit, pads
43 | */
44 | export const getKitAndPadsFromFile = (drive, kitFile) => {
45 | if(!fs.exists(kitFile)) {
46 | return null;
47 | }
48 |
49 | let kitPath = path.parse(kitFile);
50 | let buffer = getBuffer(kitFile);
51 | let checksum = buffer.readUInt8(KitBuffer.CHECKSUM_BYTE);
52 |
53 | if (checksum !== calculateChecksumFromBuffer(buffer)) {
54 | throw new Error("Invalid .kit file")
55 | }
56 |
57 | // need to get the global device
58 | let pads = getPadsFromBuffer(drive, buffer);
59 |
60 | var kit = KitModel(
61 | kitPath.dir,
62 | kitPath.base,
63 | true,
64 | false,
65 | true,
66 | kitPath.name,
67 | getSortedPadIds(drive, pads)
68 | );
69 |
70 | return {kit, pads};
71 | }
72 |
73 | /**
74 | * given a kit and pads, get a file buffer
75 | * @param {KitModel} kit
76 | * @param {PadModel[]} pads
77 | * @returns {Buffer}
78 | */
79 | export const getKitFileBuffer = (drive, kit, pads) => {
80 | let buffer = [];
81 |
82 | // header
83 | buffer = buffer.concat(getHeader(kit.kitName));
84 |
85 | KitBuffer.PAD_FILE_ORDER[drive.deviceType].forEach((padType) => {
86 | let pad = getPadWithType(kit, pads, padType);
87 | buffer = buffer.concat(getPadBlock1(pad));
88 | });
89 |
90 | KitBuffer.PAD_FILE_ORDER[drive.deviceType].forEach((padType) => {
91 | let pad = getPadWithType(kit, pads, padType);
92 | buffer = buffer.concat(getPadBlock2(pad));
93 | });
94 |
95 | // splice in the checksum
96 | let checksum = calculateChecksumFromBuffer(buffer);
97 | buffer.splice(KitBuffer.CHECKSUM_BYTE, 1, checksum);
98 |
99 | return Buffer.from(buffer);
100 | }
101 |
102 | /**
103 | * @param {Buffer}
104 | * @returns {Byte}
105 | */
106 | const calculateChecksumFromBuffer = (buffer) => {
107 | return buffer
108 | .slice(KitBuffer.CHECKSUM_BYTE + 1)
109 | .reduce((a, b) => a + b) % 256;
110 | }
111 |
112 | /*********************
113 | * GET KIT FROM FILE *
114 | *********************/
115 | /**
116 | * @param {RootModel} drive
117 | * @param {Buffer} buffer
118 | * @return {Map} padId => PadModel
119 | */
120 | const getPadsFromBuffer = (drive, buffer) => {
121 | let headerLength = 128;
122 | let blockLength = 256;
123 | let offset = headerLength;
124 |
125 | let block1 = {};
126 | KitBuffer.PAD_FILE_ORDER[drive.deviceType].forEach((padType) => {
127 | block1[padType] = buffer.slice(offset, offset+blockLength);
128 | offset += blockLength;
129 | });
130 |
131 | let block2 = {};
132 | KitBuffer.PAD_FILE_ORDER[drive.deviceType].forEach((padType) => {
133 | block2[padType] = buffer.slice(offset, offset+blockLength);
134 | offset += blockLength;
135 | });
136 |
137 | let pads = {};
138 | KitBuffer.PAD_FILE_ORDER[drive.deviceType].forEach((padType) => {
139 | let pad = getPadFromBufferBlocks(padType, block1[padType], block2[padType]);
140 | pads[pad.id] = pad;
141 | });
142 |
143 | return pads;
144 | }
145 |
146 | /**
147 | * @param {String} padType
148 | * @param {Buffer} block1
149 | * @param {Buffer} block2
150 | * @return {PadModel}
151 | */
152 | const getPadFromBufferBlocks = (padType, block1, block2) => {
153 | // block 1
154 | let midiNote = block1.readUInt8(MEMLOC.midiNote);
155 | let location = block1.readUInt8(MEMLOC.location);
156 | let level = block1.readUInt8(MEMLOC.level);
157 | let tune = block1.readUInt8(MEMLOC.tune);
158 | let pan = block1.readUInt8(MEMLOC.pan);
159 | let reverb = block1.readUInt8(MEMLOC.reverb);
160 | let mode = block1.readUInt8(MEMLOC.mode);
161 | let sens = block1.readUInt8(MEMLOC.sensitivity);
162 | let mgrp = block1.readUInt8(MEMLOC.mgrp);
163 |
164 | // block 2
165 | let velocityMin = block2.readUInt8(MEMLOC.velocityMin);
166 | let velocityMax = block2.readUInt8(MEMLOC.velocityMax);
167 | let velocityMinB = block2.readUInt8(MEMLOC.velocityMinB);
168 | let velocityMaxB = block2.readUInt8(MEMLOC.velocityMaxB);
169 |
170 | let hasFileLayerA = (block2.readUInt8(MEMLOC.hasFileLayerA) === 0xaa);
171 | let fileName = ""
172 | if (hasFileLayerA) {
173 | let fileLength = block2.readUInt8(MEMLOC.fileNameLength);
174 | fileName = block2.toString("utf-8", MEMLOC.fileName, MEMLOC.fileName + fileLength) + Drive.SAMPLE_EXTENSION;
175 | fileName = SampleStore.getFileNameFromKitFile(fileName)
176 | }
177 |
178 | let hasFileLayerB = (block2.readUInt8(MEMLOC.hasFileLayerB) === 0xaa);
179 | let fileNameB = ""
180 | if (hasFileLayerB) {
181 | let fileLength = block2.readUInt8(MEMLOC.fileNameBLength);
182 | fileNameB = block2.toString("utf-8", MEMLOC.fileNameB, MEMLOC.fileNameB + fileLength) + Drive.SAMPLE_EXTENSION;
183 | fileNameB = SampleStore.getFileNameFromKitFile(fileNameB)
184 | }
185 |
186 | return PadModel.fromFile(padType, location, level, tune, pan, reverb, midiNote, mode, sens, mgrp, velocityMin, velocityMax, fileName, velocityMinB, velocityMaxB, fileNameB);
187 | }
188 |
189 | /**
190 | * @param {KitModel} kit
191 | * @param {PadModel[]} pads
192 | * @param {String} padType
193 | * @returns {PadModel|null}
194 | */
195 | export const getPadWithType = (kit, pads, padType) => {
196 | let returnPad = null;
197 |
198 | kit.pads.forEach(function(padId) {
199 | let pad = pads[padId];
200 | if (pad.padType === padType) {
201 | returnPad = pad;
202 | }
203 | })
204 |
205 | if (!returnPad) {
206 | // for some reason we couldnt find a valid pad, return a default one
207 | return PadModel.getPad(padType);
208 | }
209 |
210 | return returnPad;
211 | }
212 |
213 | /********************
214 | * SAVE KIT TO FILE *
215 | ********************/
216 | /**
217 | * Turn a string into an array of bytes with a given length, padding the right as necessary
218 | * @param {String} str
219 | * @param {Number} padLength
220 | * @param {Byte} padByte
221 | * @returns {Byte[]}
222 | */
223 | const unpack = (str, padLength, padByte) => {
224 | let strBuffer = Array(padLength).fill(padByte);
225 | strBuffer.splice(0, str.length, ...Buffer.from(str));
226 |
227 | return strBuffer;
228 | }
229 |
230 | /**
231 | * Kit File Header - one per kit file
232 | * Header contains checksum at byte 0x08
233 | * And kit name (which seems unecessary since kit name is read from the file) starting at byte 0x48
234 | * Kit Name String length is at byte 0x47
235 | * @return {Byte[]}
236 | */
237 | const getHeader = (kitName) => {
238 | let block = [
239 | 0x4b, 0x49, 0x54, 0x48, 0x00, 0x80, 0x00, 0x00, 0xeb, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00,
240 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
241 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
242 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
243 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
244 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
245 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
246 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
247 | ];
248 |
249 | return block;
250 | }
251 |
252 | /**
253 | * Pad Block 1 - one per pad
254 | * @param {PadModel} pad
255 | * @return {Byte[]}
256 | */
257 | const getPadBlock1 = (pad) => {
258 | // note: sensitivity here is the INTERNAL value
259 | // 1=11 (0x0c), 2=14 3=17 4=20 5=23 6=26 7=29 8=32
260 | let block = [
261 | 0x4b, 0x49, 0x54, 0x49, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
262 | 0x32, 0x32, 0x41, 0x63, 0x42, 0x64, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
263 | 0x00, 0x00, 0x00, 0x18, 0x00, 0x09, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0a, 0x01, 0x00, 0x04, 0x08,
264 | 0x02, 0x00, 0x04, 0x08, 0x03, 0x00, 0x00, 0x0a, 0x08, 0x00, 0x00, 0x7f, 0x09, 0x01, 0x00, 0x05,
265 | 0x0c, 0x0c, 0x00, 0x08, 0x0d, 0x00, 0x00, 0x09, 0x0e, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x7f,
266 | 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
267 | 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
268 | 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
269 | 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
270 | 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
271 | 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
272 | 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
273 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
274 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
275 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
276 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
277 | ];
278 |
279 | // splice in the parameters
280 | block.splice(MEMLOC.midiNote, 1, pad.midiNote);
281 | block.splice(MEMLOC.location, 1, pad.location);
282 | block.splice(MEMLOC.level, 1, pad.level);
283 | block.splice(MEMLOC.tune, 1, PadModel.getUIntFileValue(pad.tune));
284 | block.splice(MEMLOC.pan, 1, PadModel.getUIntFileValue(pad.pan));
285 | block.splice(MEMLOC.reverb, 1, pad.reverb);
286 | block.splice(MEMLOC.mode, 1, pad.mode);
287 | block.splice(MEMLOC.sensitivity, 1, PadModel.getSensitivityFileValue(pad.sensitivity));
288 | block.splice(MEMLOC.mgrp, 1, pad.mgrp);
289 |
290 | return block;
291 | }
292 |
293 | /**
294 | * Pad Block 2 - one per pad
295 | * @param {PadModel} pad
296 | * @return {Byte[]}
297 | */
298 | const getPadBlock2 = (pad) => {
299 | let block = [
300 | 0x4b, 0x49, 0x54, 0x49, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
301 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
302 | 0x00, 0x00, 0x00, 0x18, 0x00, 0x09, 0x00, 0x00, 0x00, 0x05, 0x00, 0x0a, 0x01, 0x00, 0x04, 0x08,
303 | 0x02, 0x00, 0x04, 0x08, 0x03, 0x00, 0x00, 0x0a, 0x08, 0x00, 0x00, 0x7f, 0x09, 0x01, 0x00, 0x05,
304 | 0x0c, 0x0c, 0x00, 0x08, 0x0d, 0x00, 0x00, 0x09, 0x0e, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
305 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
306 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
307 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
308 | 0xff, 0xaa, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
309 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
310 | 0xff, 0xaa, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
311 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
312 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
313 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
314 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
315 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
316 | ];
317 |
318 | // splice in the parameters
319 | block.splice(MEMLOC.midiNote, 1, pad.midiNote);
320 | block.splice(MEMLOC.location, 1, pad.location);
321 | block.splice(MEMLOC.level, 1, pad.level);
322 | block.splice(MEMLOC.tune, 1, PadModel.getUIntFileValue(pad.tune));
323 | block.splice(MEMLOC.pan, 1, PadModel.getUIntFileValue(pad.pan));
324 | block.splice(MEMLOC.reverb, 1, pad.reverb);
325 | block.splice(MEMLOC.mode, 1, pad.mode);
326 | block.splice(MEMLOC.sensitivity, 1, PadModel.getSensitivityFileValue(pad.sensitivity));
327 | block.splice(MEMLOC.mgrp, 1, pad.mgrp);
328 | block.splice(MEMLOC.velocityMin, 1, pad.velocityMin);
329 | block.splice(MEMLOC.velocityMax, 1, pad.velocityMax);
330 | block.splice(MEMLOC.velocityMinB, 1, pad.velocityMinB);
331 | block.splice(MEMLOC.velocityMaxB, 1, pad.velocityMaxB);
332 |
333 | // splice in the file information
334 | let fileName = SampleStore.getWriteFileName(pad.fileName)
335 | let fileNameUpperBytes = unpack(fileName.toUpperCase(), 8, 0x20)
336 | let fileNameBytes = unpack(fileName, 8, 0x00)
337 | block.splice(MEMLOC.fileNameLength, 1, fileName.length);
338 | block.splice(MEMLOC.displayName, 8, ...fileNameUpperBytes);
339 | block.splice(MEMLOC.fileName, 8, ...fileNameBytes);
340 | block.splice(MEMLOC.hasFileLayerA, 1, ((fileName === "") ? 0xff : 0xaa));
341 |
342 | // splice in the file information for layer b
343 | let fileNameB = SampleStore.getWriteFileName(pad.fileNameB)
344 | let fileNameBUpperBytes = unpack(fileNameB.toUpperCase(), 8, 0x20)
345 | let fileNameBBytes = unpack(fileNameB, 8, 0x00)
346 | block.splice(MEMLOC.fileNameBLength, 1, fileNameB.length);
347 | block.splice(MEMLOC.displayNameB, 8, ...fileNameBUpperBytes);
348 | block.splice(MEMLOC.fileNameB, 8, ...fileNameBBytes);
349 | block.splice(MEMLOC.hasFileLayerB, 1, ((fileNameB === "") ? 0xff : 0xaa));
350 |
351 | return block;
352 | }
353 |
--------------------------------------------------------------------------------
/src/css/icons.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Icons
3 | */
4 | @font-face {
5 | font-family: 'Glyphicons Halflings';
6 | src: url("../fonts/glyphicons-halflings-regular.eot");
7 | src: url("../fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"), url("../fonts/glyphicons-halflings-regular.woff2") format("woff2"), url("../fonts/glyphicons-halflings-regular.woff") format("woff"), url("../fonts/glyphicons-halflings-regular.ttf") format("truetype"), url("../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg"); }
8 |
9 | .glyphicon {
10 | position: relative;
11 | top: 1px;
12 | display: inline-block;
13 | font-family: 'Glyphicons Halflings';
14 | font-style: normal;
15 | font-weight: normal;
16 | line-height: 1;
17 | -webkit-font-smoothing: antialiased;
18 | -moz-osx-font-smoothing: grayscale; }
19 |
20 | .glyphicon-asterisk:before {
21 | content: "\002a"; }
22 |
23 | .glyphicon-plus:before {
24 | content: "\002b"; }
25 |
26 | .glyphicon-euro:before,
27 | .glyphicon-eur:before {
28 | content: "\20ac"; }
29 |
30 | .glyphicon-minus:before {
31 | content: "\2212"; }
32 |
33 | .glyphicon-cloud:before {
34 | content: "\2601"; }
35 |
36 | .glyphicon-envelope:before {
37 | content: "\2709"; }
38 |
39 | .glyphicon-pencil:before {
40 | content: "\270f"; }
41 |
42 | .glyphicon-glass:before {
43 | content: "\e001"; }
44 |
45 | .glyphicon-music:before {
46 | content: "\e002"; }
47 |
48 | .glyphicon-search:before {
49 | content: "\e003"; }
50 |
51 | .glyphicon-heart:before {
52 | content: "\e005"; }
53 |
54 | .glyphicon-star:before {
55 | content: "\e006"; }
56 |
57 | .glyphicon-star-empty:before {
58 | content: "\e007"; }
59 |
60 | .glyphicon-user:before {
61 | content: "\e008"; }
62 |
63 | .glyphicon-film:before {
64 | content: "\e009"; }
65 |
66 | .glyphicon-th-large:before {
67 | content: "\e010"; }
68 |
69 | .glyphicon-th:before {
70 | content: "\e011"; }
71 |
72 | .glyphicon-th-list:before {
73 | content: "\e012"; }
74 |
75 | .glyphicon-ok:before {
76 | content: "\e013"; }
77 |
78 | .glyphicon-remove:before {
79 | content: "\e014"; }
80 |
81 | .glyphicon-zoom-in:before {
82 | content: "\e015"; }
83 |
84 | .glyphicon-zoom-out:before {
85 | content: "\e016"; }
86 |
87 | .glyphicon-off:before {
88 | content: "\e017"; }
89 |
90 | .glyphicon-signal:before {
91 | content: "\e018"; }
92 |
93 | .glyphicon-cog:before {
94 | content: "\e019"; }
95 |
96 | .glyphicon-trash:before {
97 | content: "\e020"; }
98 |
99 | .glyphicon-home:before {
100 | content: "\e021"; }
101 |
102 | .glyphicon-file:before {
103 | content: "\e022"; }
104 |
105 | .glyphicon-time:before {
106 | content: "\e023"; }
107 |
108 | .glyphicon-road:before {
109 | content: "\e024"; }
110 |
111 | .glyphicon-download-alt:before {
112 | content: "\e025"; }
113 |
114 | .glyphicon-download:before {
115 | content: "\e026"; }
116 |
117 | .glyphicon-upload:before {
118 | content: "\e027"; }
119 |
120 | .glyphicon-inbox:before {
121 | content: "\e028"; }
122 |
123 | .glyphicon-play-circle:before {
124 | content: "\e029"; }
125 |
126 | .glyphicon-repeat:before {
127 | content: "\e030"; }
128 |
129 | .glyphicon-refresh:before {
130 | content: "\e031"; }
131 |
132 | .glyphicon-list-alt:before {
133 | content: "\e032"; }
134 |
135 | .glyphicon-lock:before {
136 | content: "\e033"; }
137 |
138 | .glyphicon-flag:before {
139 | content: "\e034"; }
140 |
141 | .glyphicon-headphones:before {
142 | content: "\e035"; }
143 |
144 | .glyphicon-volume-off:before {
145 | content: "\e036"; }
146 |
147 | .glyphicon-volume-down:before {
148 | content: "\e037"; }
149 |
150 | .glyphicon-volume-up:before {
151 | content: "\e038"; }
152 |
153 | .glyphicon-qrcode:before {
154 | content: "\e039"; }
155 |
156 | .glyphicon-barcode:before {
157 | content: "\e040"; }
158 |
159 | .glyphicon-tag:before {
160 | content: "\e041"; }
161 |
162 | .glyphicon-tags:before {
163 | content: "\e042"; }
164 |
165 | .glyphicon-book:before {
166 | content: "\e043"; }
167 |
168 | .glyphicon-bookmark:before {
169 | content: "\e044"; }
170 |
171 | .glyphicon-print:before {
172 | content: "\e045"; }
173 |
174 | .glyphicon-camera:before {
175 | content: "\e046"; }
176 |
177 | .glyphicon-font:before {
178 | content: "\e047"; }
179 |
180 | .glyphicon-bold:before {
181 | content: "\e048"; }
182 |
183 | .glyphicon-italic:before {
184 | content: "\e049"; }
185 |
186 | .glyphicon-text-height:before {
187 | content: "\e050"; }
188 |
189 | .glyphicon-text-width:before {
190 | content: "\e051"; }
191 |
192 | .glyphicon-align-left:before {
193 | content: "\e052"; }
194 |
195 | .glyphicon-align-center:before {
196 | content: "\e053"; }
197 |
198 | .glyphicon-align-right:before {
199 | content: "\e054"; }
200 |
201 | .glyphicon-align-justify:before {
202 | content: "\e055"; }
203 |
204 | .glyphicon-list:before {
205 | content: "\e056"; }
206 |
207 | .glyphicon-indent-left:before {
208 | content: "\e057"; }
209 |
210 | .glyphicon-indent-right:before {
211 | content: "\e058"; }
212 |
213 | .glyphicon-facetime-video:before {
214 | content: "\e059"; }
215 |
216 | .glyphicon-picture:before {
217 | content: "\e060"; }
218 |
219 | .glyphicon-map-marker:before {
220 | content: "\e062"; }
221 |
222 | .glyphicon-adjust:before {
223 | content: "\e063"; }
224 |
225 | .glyphicon-tint:before {
226 | content: "\e064"; }
227 |
228 | .glyphicon-edit:before {
229 | content: "\e065"; }
230 |
231 | .glyphicon-share:before {
232 | content: "\e066"; }
233 |
234 | .glyphicon-check:before {
235 | content: "\e067"; }
236 |
237 | .glyphicon-move:before {
238 | content: "\e068"; }
239 |
240 | .glyphicon-step-backward:before {
241 | content: "\e069"; }
242 |
243 | .glyphicon-fast-backward:before {
244 | content: "\e070"; }
245 |
246 | .glyphicon-backward:before {
247 | content: "\e071"; }
248 |
249 | .glyphicon-play:before {
250 | content: "\e072"; }
251 |
252 | .glyphicon-pause:before {
253 | content: "\e073"; }
254 |
255 | .glyphicon-stop:before {
256 | content: "\e074"; }
257 |
258 | .glyphicon-forward:before {
259 | content: "\e075"; }
260 |
261 | .glyphicon-fast-forward:before {
262 | content: "\e076"; }
263 |
264 | .glyphicon-step-forward:before {
265 | content: "\e077"; }
266 |
267 | .glyphicon-eject:before {
268 | content: "\e078"; }
269 |
270 | .glyphicon-chevron-left:before {
271 | content: "\e079"; }
272 |
273 | .glyphicon-chevron-right:before {
274 | content: "\e080"; }
275 |
276 | .glyphicon-plus-sign:before {
277 | content: "\e081"; }
278 |
279 | .glyphicon-minus-sign:before {
280 | content: "\e082"; }
281 |
282 | .glyphicon-remove-sign:before {
283 | content: "\e083"; }
284 |
285 | .glyphicon-ok-sign:before {
286 | content: "\e084"; }
287 |
288 | .glyphicon-question-sign:before {
289 | content: "\e085"; }
290 |
291 | .glyphicon-info-sign:before {
292 | content: "\e086"; }
293 |
294 | .glyphicon-screenshot:before {
295 | content: "\e087"; }
296 |
297 | .glyphicon-remove-circle:before {
298 | content: "\e088"; }
299 |
300 | .glyphicon-ok-circle:before {
301 | content: "\e089"; }
302 |
303 | .glyphicon-ban-circle:before {
304 | content: "\e090"; }
305 |
306 | .glyphicon-arrow-left:before {
307 | content: "\e091"; }
308 |
309 | .glyphicon-arrow-right:before {
310 | content: "\e092"; }
311 |
312 | .glyphicon-arrow-up:before {
313 | content: "\e093"; }
314 |
315 | .glyphicon-arrow-down:before {
316 | content: "\e094"; }
317 |
318 | .glyphicon-share-alt:before {
319 | content: "\e095"; }
320 |
321 | .glyphicon-resize-full:before {
322 | content: "\e096"; }
323 |
324 | .glyphicon-resize-small:before {
325 | content: "\e097"; }
326 |
327 | .glyphicon-exclamation-sign:before {
328 | content: "\e101"; }
329 |
330 | .glyphicon-gift:before {
331 | content: "\e102"; }
332 |
333 | .glyphicon-leaf:before {
334 | content: "\e103"; }
335 |
336 | .glyphicon-fire:before {
337 | content: "\e104"; }
338 |
339 | .glyphicon-eye-open:before {
340 | content: "\e105"; }
341 |
342 | .glyphicon-eye-close:before {
343 | content: "\e106"; }
344 |
345 | .glyphicon-warning-sign:before {
346 | content: "\e107"; }
347 |
348 | .glyphicon-plane:before {
349 | content: "\e108"; }
350 |
351 | .glyphicon-calendar:before {
352 | content: "\e109"; }
353 |
354 | .glyphicon-random:before {
355 | content: "\e110"; }
356 |
357 | .glyphicon-comment:before {
358 | content: "\e111"; }
359 |
360 | .glyphicon-magnet:before {
361 | content: "\e112"; }
362 |
363 | .glyphicon-chevron-up:before {
364 | content: "\e113"; }
365 |
366 | .glyphicon-chevron-down:before {
367 | content: "\e114"; }
368 |
369 | .glyphicon-retweet:before {
370 | content: "\e115"; }
371 |
372 | .glyphicon-shopping-cart:before {
373 | content: "\e116"; }
374 |
375 | .glyphicon-folder-close:before {
376 | content: "\e117"; }
377 |
378 | .glyphicon-folder-open:before {
379 | content: "\e118"; }
380 |
381 | .glyphicon-resize-vertical:before {
382 | content: "\e119"; }
383 |
384 | .glyphicon-resize-horizontal:before {
385 | content: "\e120"; }
386 |
387 | .glyphicon-hdd:before {
388 | content: "\e121"; }
389 |
390 | .glyphicon-bullhorn:before {
391 | content: "\e122"; }
392 |
393 | .glyphicon-bell:before {
394 | content: "\e123"; }
395 |
396 | .glyphicon-certificate:before {
397 | content: "\e124"; }
398 |
399 | .glyphicon-thumbs-up:before {
400 | content: "\e125"; }
401 |
402 | .glyphicon-thumbs-down:before {
403 | content: "\e126"; }
404 |
405 | .glyphicon-hand-right:before {
406 | content: "\e127"; }
407 |
408 | .glyphicon-hand-left:before {
409 | content: "\e128"; }
410 |
411 | .glyphicon-hand-up:before {
412 | content: "\e129"; }
413 |
414 | .glyphicon-hand-down:before {
415 | content: "\e130"; }
416 |
417 | .glyphicon-circle-arrow-right:before {
418 | content: "\e131"; }
419 |
420 | .glyphicon-circle-arrow-left:before {
421 | content: "\e132"; }
422 |
423 | .glyphicon-circle-arrow-up:before {
424 | content: "\e133"; }
425 |
426 | .glyphicon-circle-arrow-down:before {
427 | content: "\e134"; }
428 |
429 | .glyphicon-globe:before {
430 | content: "\e135"; }
431 |
432 | .glyphicon-wrench:before {
433 | content: "\e136"; }
434 |
435 | .glyphicon-tasks:before {
436 | content: "\e137"; }
437 |
438 | .glyphicon-filter:before {
439 | content: "\e138"; }
440 |
441 | .glyphicon-briefcase:before {
442 | content: "\e139"; }
443 |
444 | .glyphicon-fullscreen:before {
445 | content: "\e140"; }
446 |
447 | .glyphicon-dashboard:before {
448 | content: "\e141"; }
449 |
450 | .glyphicon-paperclip:before {
451 | content: "\e142"; }
452 |
453 | .glyphicon-heart-empty:before {
454 | content: "\e143"; }
455 |
456 | .glyphicon-link:before {
457 | content: "\e144"; }
458 |
459 | .glyphicon-phone:before {
460 | content: "\e145"; }
461 |
462 | .glyphicon-pushpin:before {
463 | content: "\e146"; }
464 |
465 | .glyphicon-usd:before {
466 | content: "\e148"; }
467 |
468 | .glyphicon-gbp:before {
469 | content: "\e149"; }
470 |
471 | .glyphicon-sort:before {
472 | content: "\e150"; }
473 |
474 | .glyphicon-sort-by-alphabet:before {
475 | content: "\e151"; }
476 |
477 | .glyphicon-sort-by-alphabet-alt:before {
478 | content: "\e152"; }
479 |
480 | .glyphicon-sort-by-order:before {
481 | content: "\e153"; }
482 |
483 | .glyphicon-sort-by-order-alt:before {
484 | content: "\e154"; }
485 |
486 | .glyphicon-sort-by-attributes:before {
487 | content: "\e155"; }
488 |
489 | .glyphicon-sort-by-attributes-alt:before {
490 | content: "\e156"; }
491 |
492 | .glyphicon-unchecked:before {
493 | content: "\e157"; }
494 |
495 | .glyphicon-expand:before {
496 | content: "\e158"; }
497 |
498 | .glyphicon-collapse-down:before {
499 | content: "\e159"; }
500 |
501 | .glyphicon-collapse-up:before {
502 | content: "\e160"; }
503 |
504 | .glyphicon-log-in:before {
505 | content: "\e161"; }
506 |
507 | .glyphicon-flash:before {
508 | content: "\e162"; }
509 |
510 | .glyphicon-log-out:before {
511 | content: "\e163"; }
512 |
513 | .glyphicon-new-window:before {
514 | content: "\e164"; }
515 |
516 | .glyphicon-record:before {
517 | content: "\e165"; }
518 |
519 | .glyphicon-save:before {
520 | content: "\e166"; }
521 |
522 | .glyphicon-open:before {
523 | content: "\e167"; }
524 |
525 | .glyphicon-saved:before {
526 | content: "\e168"; }
527 |
528 | .glyphicon-import:before {
529 | content: "\e169"; }
530 |
531 | .glyphicon-export:before {
532 | content: "\e170"; }
533 |
534 | .glyphicon-send:before {
535 | content: "\e171"; }
536 |
537 | .glyphicon-floppy-disk:before {
538 | content: "\e172"; }
539 |
540 | .glyphicon-floppy-saved:before {
541 | content: "\e173"; }
542 |
543 | .glyphicon-floppy-remove:before {
544 | content: "\e174"; }
545 |
546 | .glyphicon-floppy-save:before {
547 | content: "\e175"; }
548 |
549 | .glyphicon-floppy-open:before {
550 | content: "\e176"; }
551 |
552 | .glyphicon-credit-card:before {
553 | content: "\e177"; }
554 |
555 | .glyphicon-transfer:before {
556 | content: "\e178"; }
557 |
558 | .glyphicon-cutlery:before {
559 | content: "\e179"; }
560 |
561 | .glyphicon-header:before {
562 | content: "\e180"; }
563 |
564 | .glyphicon-compressed:before {
565 | content: "\e181"; }
566 |
567 | .glyphicon-earphone:before {
568 | content: "\e182"; }
569 |
570 | .glyphicon-phone-alt:before {
571 | content: "\e183"; }
572 |
573 | .glyphicon-tower:before {
574 | content: "\e184"; }
575 |
576 | .glyphicon-stats:before {
577 | content: "\e185"; }
578 |
579 | .glyphicon-sd-video:before {
580 | content: "\e186"; }
581 |
582 | .glyphicon-hd-video:before {
583 | content: "\e187"; }
584 |
585 | .glyphicon-subtitles:before {
586 | content: "\e188"; }
587 |
588 | .glyphicon-sound-stereo:before {
589 | content: "\e189"; }
590 |
591 | .glyphicon-sound-dolby:before {
592 | content: "\e190"; }
593 |
594 | .glyphicon-sound-5-1:before {
595 | content: "\e191"; }
596 |
597 | .glyphicon-sound-6-1:before {
598 | content: "\e192"; }
599 |
600 | .glyphicon-sound-7-1:before {
601 | content: "\e193"; }
602 |
603 | .glyphicon-copyright-mark:before {
604 | content: "\e194"; }
605 |
606 | .glyphicon-registration-mark:before {
607 | content: "\e195"; }
608 |
609 | .glyphicon-cloud-download:before {
610 | content: "\e197"; }
611 |
612 | .glyphicon-cloud-upload:before {
613 | content: "\e198"; }
614 |
615 | .glyphicon-tree-conifer:before {
616 | content: "\e199"; }
617 |
618 | .glyphicon-tree-deciduous:before {
619 | content: "\e200"; }
620 |
621 | .glyphicon-cd:before {
622 | content: "\e201"; }
623 |
624 | .glyphicon-save-file:before {
625 | content: "\e202"; }
626 |
627 | .glyphicon-open-file:before {
628 | content: "\e203"; }
629 |
630 | .glyphicon-level-up:before {
631 | content: "\e204"; }
632 |
633 | .glyphicon-copy:before {
634 | content: "\e205"; }
635 |
636 | .glyphicon-paste:before {
637 | content: "\e206"; }
638 |
639 | .glyphicon-alert:before {
640 | content: "\e209"; }
641 |
642 | .glyphicon-equalizer:before {
643 | content: "\e210"; }
644 |
645 | .glyphicon-king:before {
646 | content: "\e211"; }
647 |
648 | .glyphicon-queen:before {
649 | content: "\e212"; }
650 |
651 | .glyphicon-pawn:before {
652 | content: "\e213"; }
653 |
654 | .glyphicon-bishop:before {
655 | content: "\e214"; }
656 |
657 | .glyphicon-knight:before {
658 | content: "\e215"; }
659 |
660 | .glyphicon-baby-formula:before {
661 | content: "\e216"; }
662 |
663 | .glyphicon-tent:before {
664 | content: "\26fa"; }
665 |
666 | .glyphicon-blackboard:before {
667 | content: "\e218"; }
668 |
669 | .glyphicon-bed:before {
670 | content: "\e219"; }
671 |
672 | .glyphicon-apple:before {
673 | content: "\f8ff"; }
674 |
675 | .glyphicon-erase:before {
676 | content: "\e221"; }
677 |
678 | .glyphicon-hourglass:before {
679 | content: "\231b"; }
680 |
681 | .glyphicon-lamp:before {
682 | content: "\e223"; }
683 |
684 | .glyphicon-duplicate:before {
685 | content: "\e224"; }
686 |
687 | .glyphicon-piggy-bank:before {
688 | content: "\e225"; }
689 |
690 | .glyphicon-scissors:before {
691 | content: "\e226"; }
692 |
693 | .glyphicon-bitcoin:before {
694 | content: "\e227"; }
695 |
696 | .glyphicon-btc:before {
697 | content: "\e227"; }
698 |
699 | .glyphicon-xbt:before {
700 | content: "\e227"; }
701 |
702 | .glyphicon-yen:before {
703 | content: "\00a5"; }
704 |
705 | .glyphicon-jpy:before {
706 | content: "\00a5"; }
707 |
708 | .glyphicon-ruble:before {
709 | content: "\20bd"; }
710 |
711 | .glyphicon-rub:before {
712 | content: "\20bd"; }
713 |
714 | .glyphicon-scale:before {
715 | content: "\e230"; }
716 |
717 | .glyphicon-ice-lolly:before {
718 | content: "\e231"; }
719 |
720 | .glyphicon-ice-lolly-tasted:before {
721 | content: "\e232"; }
722 |
723 | .glyphicon-education:before {
724 | content: "\e233"; }
725 |
726 | .glyphicon-option-horizontal:before {
727 | content: "\e234"; }
728 |
729 | .glyphicon-option-vertical:before {
730 | content: "\e235"; }
731 |
732 | .glyphicon-menu-hamburger:before {
733 | content: "\e236"; }
734 |
735 | .glyphicon-modal-window:before {
736 | content: "\e237"; }
737 |
738 | .glyphicon-oil:before {
739 | content: "\e238"; }
740 |
741 | .glyphicon-grain:before {
742 | content: "\e239"; }
743 |
744 | .glyphicon-sunglasses:before {
745 | content: "\e240"; }
746 |
747 | .glyphicon-text-size:before {
748 | content: "\e241"; }
749 |
750 | .glyphicon-text-color:before {
751 | content: "\e242"; }
752 |
753 | .glyphicon-text-background:before {
754 | content: "\e243"; }
755 |
756 | .glyphicon-object-align-top:before {
757 | content: "\e244"; }
758 |
759 | .glyphicon-object-align-bottom:before {
760 | content: "\e245"; }
761 |
762 | .glyphicon-object-align-horizontal:before {
763 | content: "\e246"; }
764 |
765 | .glyphicon-object-align-left:before {
766 | content: "\e247"; }
767 |
768 | .glyphicon-object-align-vertical:before {
769 | content: "\e248"; }
770 |
771 | .glyphicon-object-align-right:before {
772 | content: "\e249"; }
773 |
774 | .glyphicon-triangle-right:before {
775 | content: "\e250"; }
776 |
777 | .glyphicon-triangle-left:before {
778 | content: "\e251"; }
779 |
780 | .glyphicon-triangle-bottom:before {
781 | content: "\e252"; }
782 |
783 | .glyphicon-triangle-top:before {
784 | content: "\e253"; }
785 |
786 | .glyphicon-console:before {
787 | content: "\e254"; }
788 |
789 | .glyphicon-superscript:before {
790 | content: "\e255"; }
791 |
792 | .glyphicon-subscript:before {
793 | content: "\e256"; }
794 |
795 | .glyphicon-menu-left:before {
796 | content: "\e257"; }
797 |
798 | .glyphicon-menu-right:before {
799 | content: "\e258"; }
800 |
801 | .glyphicon-menu-down:before {
802 | content: "\e259"; }
803 |
804 | .glyphicon-menu-up:before {
805 | content: "\e260"; }
806 |
--------------------------------------------------------------------------------