├── README.md ├── lpd8-editor-test-preset ├── lpd8-protocol.md └── lpd8-web-editor-preact ├── .babelrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── preact.config.js ├── size-plugin.json ├── src ├── LPD8Preset.js ├── assets │ ├── favicon.ico │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── mstile-150x150.png ├── components │ ├── MidiDeviceSelectors.js │ ├── PresetLoader.js │ ├── Uploader.js │ ├── app.js │ ├── console │ │ ├── index.js │ │ └── style.css │ ├── header │ │ ├── index.js │ │ └── style.css │ ├── knobSettingsController │ │ ├── KnobSettingsController.js │ │ └── style.css │ ├── midiChannelController │ │ ├── MidiChannelController.js │ │ └── style.css │ └── padSettingsController │ │ ├── PadSettingsController.js │ │ └── style.css ├── index.js ├── manifest.json ├── routes │ └── home │ │ ├── index.js │ │ └── style.css ├── static │ └── protocol.txt ├── style │ └── index.css ├── sw.js └── template.html └── tests ├── __mocks__ ├── browserMocks.js ├── fileMocks.js └── setupTests.js └── header.test.js /README.md: -------------------------------------------------------------------------------- 1 | # lpd8-web-editor 2 | 3 | This is a Preact-based editor for the AKAI LPD8 USB Midi controller. This allows you to edit the MIDI notes/CC numbers that the devices uses when hitting pads/moving knobs. 4 | 5 | It's currently (2022) hosted at https://lpd8.bngf.space/. This repository is primarily meant to be a backup and archive of the code. If someone feels like improving things, I'm most likely open to accept PRs – let's discuss this when it comes up. 6 | 7 | It's in no way affiliated to AKAI. 8 | -------------------------------------------------------------------------------- /lpd8-editor-test-preset: -------------------------------------------------------------------------------- 1 | 240 71 127 117 97 0 58 1 15 1 17 33 1 2 18 34 0 3 19 35 1 4 20 36 0 5 21 37 1 6 22 38 0 7 23 39 1 8 24 40 0 65 0 9 66 16 25 67 32 41 68 48 57 69 64 73 70 80 89 71 96 105 72 112 121 247 -------------------------------------------------------------------------------- /lpd8-protocol.md: -------------------------------------------------------------------------------- 1 | # LPD8 MIDI sysex protocol 2 | 3 | ## Request preset from device: 9 bytes 4 | 5 | To request preset data from the AKAI LPD8, send a specific Sysex command to the device: 6 | 7 | 7 bytes command header: `F0 47 7F 75 63 00 01` 8 | 1 byte preset-nr (01-04) `01` 9 | 1 byte "EOL" `F7` 10 | 11 | `F0 47 7F 75 63 00 01 01 F7` 12 | 13 | LPD8 answers with a preset dataset formatted similar to what you use to write settings to the device which is described in the next section, i.e. 14 | 15 | ``` 16 | 00 F0 47 7F 75 63 00 3A 04 00 50 06 01 00 4F 07 02 17 | 10 00 53 08 03 00 52 09 04 00 24 00 05 00 26 01 06 18 | 20 00 2A 02 09 00 2E 03 08 00 00 00 7F 01 00 7F 02 19 | 30 00 7F 03 00 7F 04 00 7F 05 00 7F 06 00 7F 08 00 20 | 40 7F F7 21 | ``` 22 | 23 | ## Write preset to device: 66 bytes 24 | 25 | 7 bytes: Sysex header 26 | `F0 47 7F 75 61 00 3A` 27 | 28 | 1 byte: preset number, 1-4 29 | `01` 30 | 31 | 1 byte: MIDI channel of preset, 0-15 (aka 00-0f) 32 | `0f` 33 | 34 | For 8 pads: 4 bytes each (=32 bytes) with 35 | - 1 byte note# 0-x 36 | - 1 byte PC# 0-x 37 | - 1 byte CC# 0-x 38 | - 1 byte "Toggle" vs. "Momentary" (0-1) 39 | i.e. 40 | `01 11 21 01 02 12 22 00 03 13 23 01 04 14 24 00 05 15 25 01 06 16 26 00 07 17 27 01 08 18 28 00` 41 | 42 | For 8 CCs: 3 bytes each (=24 bytes) with 43 | - 1 byte CC# 0-x 44 | - 1 byte CC value "from" 45 | - 1 byte CC value "to" 46 | i.e. 47 | `41 00 09 42 10 19 43 20 29 44 30 39 45 40 49 46 50 59 47 60 69 48 70 79` 48 | 49 | 1 byte "EOL": 50 | `F7` 51 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | ["preact-cli/babel", { "modules": "commonjs" }] 6 | ] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | /*.log 4 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/README.md: -------------------------------------------------------------------------------- 1 | # lpd8-web-editor-preact 2 | 3 | ## CLI Commands 4 | 5 | ``` bash 6 | # install dependencies 7 | npm install 8 | 9 | # serve with hot reload at localhost:8080 10 | npm run dev 11 | 12 | # build for production with minification 13 | npm run build 14 | 15 | # test the production build locally 16 | npm run serve 17 | 18 | # run tests with jest and enzyme 19 | npm run test 20 | ``` 21 | 22 | For detailed explanation on how things work, checkout the [CLI Readme](https://github.com/developit/preact-cli/blob/master/README.md). 23 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "lpd8-web-editor-preact", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "preact build", 8 | "serve": "sirv build --port 8080 --cors --single", 9 | "dev": "preact watch", 10 | "lint": "eslint src", 11 | "test": "jest", 12 | "ship": "preact build && scp -r build/* bngf@amalthea.uberspace.de:/var/www/virtual/bngf/lpd8.bngf.space" 13 | }, 14 | "eslintConfig": { 15 | "extends": "preact", 16 | "ignorePatterns": [ 17 | "build/" 18 | ] 19 | }, 20 | "devDependencies": { 21 | "enzyme": "^3.10.0", 22 | "enzyme-adapter-preact-pure": "^2.0.0", 23 | "eslint": "^6.0.1", 24 | "eslint-config-preact": "^1.1.0", 25 | "jest": "^24.9.0", 26 | "jest-preset-preact": "^1.0.0", 27 | "preact-cli": "^3.0.0", 28 | "sirv-cli": "1.0.3" 29 | }, 30 | "dependencies": { 31 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 32 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 33 | "@fortawesome/react-fontawesome": "^0.1.13", 34 | "bulma": "^0.9.1", 35 | "lodash": "^4.17.20", 36 | "preact": "^10.3.2", 37 | "preact-render-to-string": "^5.1.4", 38 | "preact-router": "^3.2.1", 39 | "webmidi": "^2.5.1" 40 | }, 41 | "jest": { 42 | "preset": "jest-preset-preact", 43 | "setupFiles": [ 44 | "/tests/__mocks__/browserMocks.js", 45 | "/tests/__mocks__/setupTests.js" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/preact.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // docs here: https://github.com/preactjs/preact-cli#webpack 3 | 4 | /** 5 | * Function that mutates the original webpack config. 6 | * Supports asynchronous changes when a promise is returned (or it's an async function). 7 | * 8 | * @param {object} config - original webpack config. 9 | * @param {object} env - options passed to the CLI. 10 | * @param {WebpackConfigHelpers} helpers - object with useful helpers for working with the webpack config. 11 | * @param {object} options - this is mainly relevant for plugins (will always be empty in the config), default to an empty object 12 | **/ 13 | // webpack(config, env, helpers, options) { 14 | // config.output.publicPath = '/lpd8-web-editor'; 15 | // }, 16 | }; -------------------------------------------------------------------------------- /lpd8-web-editor-preact/size-plugin.json: -------------------------------------------------------------------------------- 1 | [{"timestamp":1669847796066,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":0,"diff":-27577},{"filename":"bundle.*****.esm.js","previous":8428,"size":8425,"diff":-3},{"filename":"polyfills.*****.esm.js","previous":2177,"size":2177,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":50691,"size":50809,"diff":118},{"filename":"index.html","previous":4764,"size":4869,"diff":105},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"sw-esm.js","previous":8474,"size":8474,"diff":0},{"filename":"sw.js","previous":8471,"size":8471,"diff":0},{"filename":"polyfills.03bf6.js","previous":2175,"size":2175,"diff":0},{"filename":"route-home.chunk.8b3f6.css","previous":270,"size":270,"diff":0},{"filename":"route-home.chunk.32eba.js","previous":51132,"size":0,"diff":-51132},{"filename":"bundle.d7594.js","previous":8397,"size":0,"diff":-8397},{"filename":"bundle.a2ac1.css","previous":0,"size":27688,"diff":27688},{"filename":"bundle.f4fc3.js","previous":0,"size":8396,"diff":8396},{"filename":"route-home.chunk.0aad5.js","previous":0,"size":51246,"diff":51246}]},{"timestamp":1607385389217,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8431,"size":8428,"diff":-3},{"filename":"polyfills.*****.esm.js","previous":2177,"size":2177,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":50691,"size":50691,"diff":0},{"filename":"index.html","previous":4766,"size":4764,"diff":-2},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"sw-esm.js","previous":8474,"size":8474,"diff":0},{"filename":"sw.js","previous":8471,"size":8471,"diff":0},{"filename":"polyfills.03bf6.js","previous":2175,"size":2175,"diff":0},{"filename":"route-home.chunk.8b3f6.css","previous":270,"size":270,"diff":0},{"filename":"route-home.chunk.32eba.js","previous":51132,"size":51132,"diff":0},{"filename":"bundle.aaba1.js","previous":8400,"size":0,"diff":-8400},{"filename":"bundle.d7594.js","previous":0,"size":8397,"diff":8397}]},{"timestamp":1607384855504,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8516,"size":8431,"diff":-85},{"filename":"polyfills.*****.esm.js","previous":2177,"size":2177,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":50691,"size":50691,"diff":0},{"filename":"index.html","previous":4764,"size":4766,"diff":2},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"sw-esm.js","previous":8474,"size":8474,"diff":0},{"filename":"sw.js","previous":8471,"size":8471,"diff":0},{"filename":"polyfills.03bf6.js","previous":2175,"size":2175,"diff":0},{"filename":"route-home.chunk.8b3f6.css","previous":270,"size":270,"diff":0},{"filename":"route-protocol_documentation.chunk.*****.esm.js","previous":258,"size":0,"diff":-258},{"filename":"bundle.c7889.js","previous":8490,"size":0,"diff":-8490},{"filename":"route-home.chunk.32eba.js","previous":51132,"size":51132,"diff":0},{"filename":"route-protocol_documentation.chunk.fb04d.js","previous":331,"size":0,"diff":-331},{"filename":"bundle.aaba1.js","previous":0,"size":8400,"diff":8400}]},{"timestamp":1607384262836,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8349,"size":8516,"diff":167},{"filename":"polyfills.*****.esm.js","previous":2177,"size":2177,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":50700,"size":50691,"diff":-9},{"filename":"index.html","previous":4656,"size":4764,"diff":108},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"sw-esm.js","previous":8474,"size":8474,"diff":0},{"filename":"sw.js","previous":8471,"size":8471,"diff":0},{"filename":"polyfills.03bf6.js","previous":2175,"size":2175,"diff":0},{"filename":"route-home.chunk.8b3f6.css","previous":270,"size":270,"diff":0},{"filename":"bundle.132b9.js","previous":8322,"size":0,"diff":-8322},{"filename":"route-home.chunk.b7676.js","previous":51142,"size":0,"diff":-51142},{"filename":"route-protocol_documentation.chunk.*****.esm.js","previous":0,"size":258,"diff":258},{"filename":"bundle.c7889.js","previous":0,"size":8490,"diff":8490},{"filename":"route-home.chunk.32eba.js","previous":0,"size":51132,"diff":51132},{"filename":"route-protocol_documentation.chunk.fb04d.js","previous":0,"size":331,"diff":331}]},{"timestamp":1607094335982,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8349,"size":8349,"diff":0},{"filename":"polyfills.*****.esm.js","previous":2177,"size":2177,"diff":0},{"filename":"route-home.chunk.8b99b.css","previous":249,"size":0,"diff":-249},{"filename":"route-home.chunk.*****.esm.js","previous":50477,"size":50700,"diff":223},{"filename":"index.html","previous":4608,"size":4656,"diff":48},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"sw-esm.js","previous":8474,"size":8474,"diff":0},{"filename":"sw.js","previous":8471,"size":8471,"diff":0},{"filename":"polyfills.03bf6.js","previous":2175,"size":2175,"diff":0},{"filename":"bundle.c3bc0.js","previous":8320,"size":0,"diff":-8320},{"filename":"route-home.chunk.74b2c.js","previous":50914,"size":0,"diff":-50914},{"filename":"route-home.chunk.8b3f6.css","previous":0,"size":270,"diff":270},{"filename":"bundle.132b9.js","previous":0,"size":8322,"diff":8322},{"filename":"route-home.chunk.b7676.js","previous":0,"size":51142,"diff":51142}]},{"timestamp":1607047810006,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8351,"size":8349,"diff":-2},{"filename":"polyfills.*****.esm.js","previous":2177,"size":2177,"diff":0},{"filename":"route-home.chunk.8b99b.css","previous":249,"size":249,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":50476,"size":50477,"diff":1},{"filename":"index.html","previous":4600,"size":4608,"diff":8},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"sw-esm.js","previous":8474,"size":8474,"diff":0},{"filename":"sw.js","previous":8471,"size":8471,"diff":0},{"filename":"bundle.3915c.js","previous":8323,"size":0,"diff":-8323},{"filename":"polyfills.03bf6.js","previous":2175,"size":2175,"diff":0},{"filename":"route-home.chunk.5d42f.js","previous":50913,"size":0,"diff":-50913},{"filename":"bundle.c3bc0.js","previous":0,"size":8320,"diff":8320},{"filename":"route-home.chunk.74b2c.js","previous":0,"size":50914,"diff":50914}]},{"timestamp":1607047544566,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8346,"size":8351,"diff":5},{"filename":"polyfills.*****.esm.js","previous":2181,"size":2177,"diff":-4},{"filename":"route-home.chunk.8b99b.css","previous":249,"size":249,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":49948,"size":50476,"diff":528},{"filename":"index.html","previous":4219,"size":4600,"diff":381},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"route-home.chunk.d0ed0.js","previous":50369,"size":0,"diff":-50369},{"filename":"sw-esm.js","previous":8474,"size":8474,"diff":0},{"filename":"sw.js","previous":8471,"size":8471,"diff":0},{"filename":"bundle.09698.js","previous":8322,"size":0,"diff":-8322},{"filename":"polyfills.090b4.js","previous":2177,"size":0,"diff":-2177},{"filename":"bundle.3915c.js","previous":0,"size":8323,"diff":8323},{"filename":"polyfills.03bf6.js","previous":0,"size":2175,"diff":2175},{"filename":"route-home.chunk.5d42f.js","previous":0,"size":50913,"diff":50913}]},{"timestamp":1607045394720,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8319,"size":8346,"diff":27},{"filename":"polyfills.*****.esm.js","previous":2192,"size":2181,"diff":-11},{"filename":"route-home.chunk.8b99b.css","previous":249,"size":249,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":49948,"size":49948,"diff":0},{"filename":"index.html","previous":4225,"size":4219,"diff":-6},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"bundle.2ff62.js","previous":8295,"size":0,"diff":-8295},{"filename":"polyfills.1a40e.js","previous":2188,"size":0,"diff":-2188},{"filename":"route-home.chunk.d0ed0.js","previous":50369,"size":50369,"diff":0},{"filename":"sw-esm.js","previous":0,"size":8474,"diff":8474},{"filename":"sw.js","previous":0,"size":8471,"diff":8471},{"filename":"bundle.09698.js","previous":0,"size":8322,"diff":8322},{"filename":"polyfills.090b4.js","previous":0,"size":2177,"diff":2177}]},{"timestamp":1607045095873,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8356,"size":8319,"diff":-37},{"filename":"polyfills.*****.esm.js","previous":2191,"size":2192,"diff":1},{"filename":"route-home.chunk.8b99b.css","previous":249,"size":249,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":49921,"size":49948,"diff":27},{"filename":"sw.js","previous":8483,"size":0,"diff":-8483},{"filename":"sw-esm.js","previous":8486,"size":0,"diff":-8486},{"filename":"index.html","previous":4225,"size":4225,"diff":0},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"route-home.chunk.06476.js","previous":50349,"size":0,"diff":-50349},{"filename":"bundle.eda24.js","previous":8334,"size":0,"diff":-8334},{"filename":"polyfills.15d66.js","previous":2189,"size":0,"diff":-2189},{"filename":"bundle.2ff62.js","previous":0,"size":8295,"diff":8295},{"filename":"polyfills.1a40e.js","previous":0,"size":2188,"diff":2188},{"filename":"route-home.chunk.d0ed0.js","previous":0,"size":50369,"diff":50369}]},{"timestamp":1607044798623,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8360,"size":8356,"diff":-4},{"filename":"polyfills.*****.esm.js","previous":2191,"size":2191,"diff":0},{"filename":"route-home.chunk.8b99b.css","previous":249,"size":249,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":49921,"size":49921,"diff":0},{"filename":"sw.js","previous":8483,"size":8483,"diff":0},{"filename":"sw-esm.js","previous":8486,"size":8486,"diff":0},{"filename":"index.html","previous":4226,"size":4225,"diff":-1},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"route-home.chunk.06476.js","previous":50349,"size":50349,"diff":0},{"filename":"bundle.5d515.js","previous":8337,"size":0,"diff":-8337},{"filename":"polyfills.ce1c3.js","previous":2187,"size":0,"diff":-2187},{"filename":"bundle.eda24.js","previous":0,"size":8334,"diff":8334},{"filename":"polyfills.15d66.js","previous":0,"size":2189,"diff":2189}]},{"timestamp":1607044695854,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8356,"size":8360,"diff":4},{"filename":"polyfills.*****.esm.js","previous":2190,"size":2191,"diff":1},{"filename":"route-home.chunk.8b99b.css","previous":249,"size":249,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":49921,"size":49921,"diff":0},{"filename":"sw.js","previous":8482,"size":8483,"diff":1},{"filename":"sw-esm.js","previous":8485,"size":8486,"diff":1},{"filename":"index.html","previous":4225,"size":4226,"diff":1},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"route-home.chunk.06476.js","previous":50349,"size":50349,"diff":0},{"filename":"bundle.7d64f.js","previous":8335,"size":0,"diff":-8335},{"filename":"polyfills.e33ca.js","previous":2186,"size":0,"diff":-2186},{"filename":"bundle.5d515.js","previous":0,"size":8337,"diff":8337},{"filename":"polyfills.ce1c3.js","previous":0,"size":2187,"diff":2187}]},{"timestamp":1607044602158,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8347,"size":8356,"diff":9},{"filename":"polyfills.*****.esm.js","previous":2177,"size":2190,"diff":13},{"filename":"route-home.chunk.8b99b.css","previous":249,"size":249,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":49921,"size":49921,"diff":0},{"filename":"sw.js","previous":8471,"size":8482,"diff":11},{"filename":"sw-esm.js","previous":8474,"size":8485,"diff":11},{"filename":"polyfills.03bf6.js","previous":2175,"size":0,"diff":-2175},{"filename":"index.html","previous":4228,"size":4225,"diff":-3},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"bundle.54f1f.js","previous":8324,"size":0,"diff":-8324},{"filename":"route-home.chunk.06476.js","previous":50349,"size":50349,"diff":0},{"filename":"bundle.7d64f.js","previous":0,"size":8335,"diff":8335},{"filename":"polyfills.e33ca.js","previous":0,"size":2186,"diff":2186}]},{"timestamp":1607044238339,"files":[{"filename":"bundle.cc289.css","previous":27577,"size":27577,"diff":0},{"filename":"bundle.*****.esm.js","previous":8407,"size":8347,"diff":-60},{"filename":"polyfills.*****.esm.js","previous":2177,"size":2177,"diff":0},{"filename":"route-home.chunk.8b99b.css","previous":249,"size":249,"diff":0},{"filename":"route-home.chunk.*****.esm.js","previous":50041,"size":49921,"diff":-120},{"filename":"route-profile.chunk.62c75.css","previous":77,"size":0,"diff":-77},{"filename":"route-profile.chunk.*****.esm.js","previous":1410,"size":0,"diff":-1410},{"filename":"sw.js","previous":8471,"size":8471,"diff":0},{"filename":"sw-esm.js","previous":8474,"size":8474,"diff":0},{"filename":"bundle.dadd3.js","previous":8373,"size":0,"diff":-8373},{"filename":"polyfills.03bf6.js","previous":2175,"size":2175,"diff":0},{"filename":"route-home.chunk.2e5ab.js","previous":50478,"size":0,"diff":-50478},{"filename":"route-profile.chunk.1d299.js","previous":1415,"size":0,"diff":-1415},{"filename":"index.html","previous":4219,"size":4228,"diff":9},{"filename":"sw-debug.js","previous":393,"size":393,"diff":0},{"filename":"bundle.54f1f.js","previous":0,"size":8324,"diff":8324},{"filename":"route-home.chunk.06476.js","previous":0,"size":50349,"diff":50349}]},{"timestamp":1607043726732,"files":[{"filename":"ssr-build/ssr-bundle.1973d.css","previous":27994,"size":0,"diff":-27994},{"filename":"ssr-build/ssr-bundle.js","previous":54178,"size":0,"diff":-54178},{"filename":"bundle.cc289.css","previous":0,"size":27577,"diff":27577},{"filename":"bundle.*****.esm.js","previous":0,"size":8407,"diff":8407},{"filename":"polyfills.*****.esm.js","previous":0,"size":2177,"diff":2177},{"filename":"route-home.chunk.8b99b.css","previous":0,"size":249,"diff":249},{"filename":"route-home.chunk.*****.esm.js","previous":0,"size":50041,"diff":50041},{"filename":"route-profile.chunk.62c75.css","previous":0,"size":77,"diff":77},{"filename":"route-profile.chunk.*****.esm.js","previous":0,"size":1410,"diff":1410},{"filename":"sw.js","previous":0,"size":8471,"diff":8471},{"filename":"sw-esm.js","previous":0,"size":8474,"diff":8474},{"filename":"bundle.dadd3.js","previous":0,"size":8373,"diff":8373},{"filename":"polyfills.03bf6.js","previous":0,"size":2175,"diff":2175},{"filename":"route-home.chunk.2e5ab.js","previous":0,"size":50478,"diff":50478},{"filename":"route-profile.chunk.1d299.js","previous":0,"size":1415,"diff":1415},{"filename":"index.html","previous":0,"size":4219,"diff":4219},{"filename":"sw-debug.js","previous":0,"size":393,"diff":393}]}] 2 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/LPD8Preset.js: -------------------------------------------------------------------------------- 1 | 2 | class LPD8Preset { 3 | 4 | channel = 1; 5 | preset_nr = 1; 6 | pads = []; 7 | knobs = []; 8 | 9 | constructor() { 10 | for (let i = 0; i < 8; i++) { 11 | this.pads.push({ 12 | "number": i + 1, 13 | "note": 60 + i, 14 | "pc": i, 15 | "cc": 8 + i, 16 | "momentary": false 17 | }), 18 | this.knobs.push({ 19 | "number": i + 1, 20 | "cc": i, 21 | "min": 0, 22 | "max": 127 23 | }) 24 | } 25 | } 26 | 27 | setPadData(padNr, data) { 28 | for (let i in this.pads) { 29 | if (this.pads[i].number === padNr) { 30 | this.pads[i].note = data.note; 31 | this.pads[i].pc = data.pc; 32 | this.pads[i].cc = data.cc; 33 | this.pads[i].momentary = data.momentary; 34 | } 35 | } 36 | } 37 | 38 | setKnobData(knobNr, data) { 39 | for (let i in this.knobs) { 40 | if (this.knobs[i].number === knobNr) { 41 | this.knobs[i].cc = data.cc; 42 | this.knobs[i].min = data.min; 43 | this.knobs[i].max = data.max; 44 | return; 45 | } 46 | } 47 | throw Error('Could not set data on knob', knobNr); 48 | } 49 | 50 | toSysex() { 51 | let data = []; 52 | 53 | // we omit the SysEx Start and the manufacturer byte (0x47, Akai) 54 | data = [ 0x7F, 0x75, 0x61, 0x00, 0x3A ]; 55 | 56 | data.push(this.preset_nr); 57 | data.push(this.channel - 1); 58 | 59 | for (let i = 0; i < 8; i++) { 60 | const pad = this.pads.find(pad => { 61 | return pad.number === i + 1; 62 | }); 63 | data.push(pad.note); 64 | data.push(pad.pc); 65 | data.push(pad.cc); 66 | data.push(pad.momentary ? 0 : 1); 67 | } 68 | 69 | for (let i = 0; i < 8; i++) { 70 | const knob = this.knobs.find(knob => { 71 | return knob.number === i + 1; 72 | }); 73 | data.push(knob.cc); 74 | data.push(knob.min); 75 | data.push(knob.max); 76 | } 77 | 78 | console.log('wrote some bytes', data.length); 79 | return data; 80 | } 81 | 82 | static fromSysex(data) { 83 | const preset = new LPD8Preset(); 84 | preset.channel = data[8] + 1; 85 | preset.preset_nr = data[7]; 86 | 87 | const padDataStart = 9; 88 | const knobDataStart = padDataStart + 4 * 8; 89 | 90 | for (let i = 0; i < 8; i++) { 91 | const thisPadDataOffset = padDataStart + i * 4; 92 | preset.pads[i].note = data[thisPadDataOffset]; 93 | preset.pads[i].pc = data[thisPadDataOffset + 1]; 94 | preset.pads[i].cc = data[thisPadDataOffset + 2]; 95 | preset.pads[i].momentary = data[thisPadDataOffset + 3] === 0; 96 | 97 | const thisKnobDataOffset = knobDataStart + i * 3; 98 | preset.knobs[i].cc = data[thisKnobDataOffset]; 99 | preset.knobs[i].min = data[thisKnobDataOffset + 1]; 100 | preset.knobs[i].max = data[thisKnobDataOffset + 2]; 101 | } 102 | 103 | return preset; 104 | } 105 | } 106 | 107 | export default LPD8Preset; -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennigraf/lpd8-web-editor/6352db468a9c6ce2e62ba1a73d3d9fd2b9fa50ce/lpd8-web-editor-preact/src/assets/favicon.ico -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennigraf/lpd8-web-editor/6352db468a9c6ce2e62ba1a73d3d9fd2b9fa50ce/lpd8-web-editor-preact/src/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennigraf/lpd8-web-editor/6352db468a9c6ce2e62ba1a73d3d9fd2b9fa50ce/lpd8-web-editor-preact/src/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennigraf/lpd8-web-editor/6352db468a9c6ce2e62ba1a73d3d9fd2b9fa50ce/lpd8-web-editor-preact/src/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennigraf/lpd8-web-editor/6352db468a9c6ce2e62ba1a73d3d9fd2b9fa50ce/lpd8-web-editor-preact/src/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennigraf/lpd8-web-editor/6352db468a9c6ce2e62ba1a73d3d9fd2b9fa50ce/lpd8-web-editor-preact/src/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennigraf/lpd8-web-editor/6352db468a9c6ce2e62ba1a73d3d9fd2b9fa50ce/lpd8-web-editor-preact/src/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/MidiDeviceSelectors.js: -------------------------------------------------------------------------------- 1 | const { Component } = require("preact"); 2 | 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 4 | import { faSpinner } from '@fortawesome/free-solid-svg-icons' 5 | 6 | class MidiDeviceSelectors extends Component { 7 | 8 | findDeviceByIdFromList = (id, list) => { 9 | for (let index in list) { 10 | if (list[index].id == id) { 11 | return list[index]; 12 | } 13 | } 14 | throw Error('non-existing device id') 15 | } 16 | 17 | selectInDevice = async event => { 18 | const selectedDeviceId = event.target.value; 19 | const inDevice = this.findDeviceByIdFromList(selectedDeviceId, this.props.inDevices); 20 | this.props.onInDeviceSelection(inDevice); 21 | } 22 | 23 | selectOutDevice = async event => { 24 | const selectedDeviceId = event.target.value; 25 | const outDevice = this.findDeviceByIdFromList(selectedDeviceId, this.props.outDevices); 26 | this.props.onOutDeviceSelection(outDevice); 27 | } 28 | 29 | render(props, state) { 30 | const midiInDevices = props.inDevices.map(device => { 31 | return ( 32 | 35 | ); 36 | }) 37 | 38 | const midiOutDevices = props.outDevices.map(device => { 39 | return ( 40 | 43 | ); 44 | }) 45 | 46 | 47 | const loader = ( 48 |

49 | 50 | 51 | 52 | Waiting for MIDI to start… 53 |

54 | ) 55 | 56 | const selectors = ( 57 |
58 |
59 |

60 | Select a device port to read data from: 61 |

62 |
64 | 70 |
71 |
72 | 73 |
74 |

75 | Select a device port to write data to: 76 |

77 |
79 | 85 |
86 |
87 |
88 | ); 89 | 90 | const sectionBody = !props.active ? loader : selectors; 91 | 92 | return (<> 93 |
94 |
95 |

MIDI device selector

96 |
97 |
98 | { sectionBody } 99 |
100 |
101 | ) 102 | } 103 | } 104 | 105 | export default MidiDeviceSelectors; -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/PresetLoader.js: -------------------------------------------------------------------------------- 1 | const { Component } = require("preact"); 2 | 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 4 | import { faSpinner } from '@fortawesome/free-solid-svg-icons' 5 | 6 | class PresetLoader extends Component { 7 | 8 | loadPreset = (event, presetNr) => { 9 | event.target.classList.add('is-primary'); 10 | setTimeout(() => { 11 | event.target.classList.remove('is-primary') 12 | }, 200); 13 | this.props.onLoadPresetFromDevice(presetNr); 14 | } 15 | 16 | loadDefaultPreset = () => { 17 | console.log('load default preset from "disk"'); 18 | this.props.onLoadDefaultPreset(); 19 | } 20 | 21 | render(props, state) { 22 | const loader = ( 23 |

24 | 25 | 26 | 27 | Waiting for MIDI device selection 28 |

29 | ); 30 | 31 | const content = (<> 32 |
33 |

34 | Load preset from device slot: 35 |

36 |
37 | 38 | 39 | 40 | 41 |
42 |
43 |
44 |

45 | ...or: 46 |

47 | 48 |
49 | ) 50 | 51 | const sectionBody = !props.active ? loader : content; 52 | 53 | return (<> 54 |
55 |
56 |

Load settings

57 |
58 |
59 | { sectionBody } 60 |
61 |
62 | ) 63 | } 64 | } 65 | 66 | export default PresetLoader; -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/Uploader.js: -------------------------------------------------------------------------------- 1 | const { Component } = require("preact"); 2 | 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 4 | import { faSpinner } from '@fortawesome/free-solid-svg-icons' 5 | 6 | class Uploader extends Component { 7 | 8 | state = { 9 | selectedSlot: null, 10 | isUploading: false 11 | } 12 | 13 | selectSlot = presetNr => { 14 | this.setState({ 15 | selectedSlot: presetNr 16 | }); 17 | } 18 | 19 | upload = () => { 20 | this.props.onUpload(this.state.selectedSlot); 21 | this.setState({ isUploading: true }, () => { 22 | setTimeout(() => { 23 | this.setState({ 24 | isUploading: false, 25 | selectedSlot: null 26 | }); 27 | }, 280); 28 | }) 29 | } 30 | 31 | render(props, state) { 32 | const loader = ( 33 |

34 | 35 | 36 | 37 | Waiting for MIDI device selection 38 |

39 | ); 40 | 41 | const content = ( 42 |
43 |

44 | Save settings to device in slot: 45 |

46 |
47 | 52 | 57 | 62 | 67 |
68 |
69 | 74 |

75 | Attention: When clicking upload, all settings on the device will be overwritten 76 | by the settings listed below! 77 |

78 |
79 |
80 | ) 81 | 82 | const sectionBody = !props.active ? loader : content; 83 | 84 | return (<> 85 |
86 |
87 |

Save settings to device

88 |
89 |
90 | { sectionBody } 91 |
92 |
93 | ) 94 | } 95 | } 96 | 97 | export default Uploader; 98 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/app.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { Router } from 'preact-router'; 3 | 4 | // import "bulma/css/bulma.min.css"; 5 | import 'bulma/css/bulma.min.css'; 6 | 7 | import Header from './header'; 8 | 9 | // Code-splitting is automated for `routes` directory 10 | import Home from '../routes/home'; 11 | 12 | const App = () => ( 13 |
14 |
15 | 16 | 17 | 18 |
19 | ) 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/console/index.js: -------------------------------------------------------------------------------- 1 | const { Component } = require("preact"); 2 | 3 | import style from './style.css'; 4 | 5 | const welcomeMessage = `Hello World! This is the console. We'll show what's going on behind the scenes here.`; 6 | 7 | const MAX_LINES = 25; 8 | 9 | class Console extends Component { 10 | 11 | lastPostedEvent = null; 12 | 13 | state = { 14 | lines: [ 15 | { message: welcomeMessage } 16 | ] 17 | } 18 | 19 | componentDidMount() { 20 | // this.props.ref(this); 21 | this.props.setConsole(this); 22 | } 23 | 24 | postMessage(message) { 25 | this.lastPostedEvent = null; 26 | // this.state.lines.push({ message: message }) 27 | this.state.lines.splice(0, 0, { message: message }); 28 | this.setState({ 29 | lines: this.state.lines 30 | }); 31 | } 32 | 33 | parseAndPostEvent(event) { 34 | let message = 'An unknown thing happened!'; 35 | 36 | if (event.type === 'controlchange') { 37 | message = `CH ${event.channel}: CC ${event.controller.number} - ${event.value}`; 38 | } else if (event.type === 'programchange') { 39 | message = `CH ${event.channel}: PC ${event.value}`; 40 | } else if (event.type === 'noteon') { 41 | message = `CH ${event.channel}: NoteOn ${event.note.number} - ${event.rawVelocity}`; 42 | } else if (event.type === 'noteoff') { 43 | message = `CH ${event.channel}: NoteOff ${event.note.number} - ${event.rawVelocity}`; 44 | } else if (event.type === 'sysex') { 45 | const someData = event.data.slice(1, 20).join(' '); 46 | message = `Sysex – ${someData}`; 47 | if (event.data.length > 21) { 48 | message += ' …'; 49 | } 50 | } 51 | 52 | this.setState((state, _) => { 53 | let replaceInsteadOfPrepend = this.eventIsSimilarToPreviouslyPostedEvent(event); 54 | 55 | if (replaceInsteadOfPrepend) { 56 | state.lines.splice(0, 1, { message: message }); 57 | } else { 58 | state.lines.splice(0, 0, { message: message }); 59 | } 60 | 61 | if (state.lines.length >= MAX_LINES) { 62 | state.lines.splice(MAX_LINES, state.lines.length - MAX_LINES); 63 | } 64 | 65 | this.lastPostedEvent = event; 66 | return { 67 | lines: state.lines 68 | } 69 | }); 70 | } 71 | 72 | eventIsSimilarToPreviouslyPostedEvent = event => { 73 | if (this.lastPostedEvent === null) { 74 | return false; 75 | } 76 | 77 | if (event.type !== this.lastPostedEvent.type) { 78 | return false; 79 | } 80 | 81 | if (event.channel !== this.lastPostedEvent.channel) { 82 | return false; 83 | } 84 | 85 | if (event.type === 'controlchange') { 86 | return this.lastPostedEvent.controller.number === event.controller.number; 87 | } 88 | 89 | if (event.type === 'programchange') { 90 | return true; 91 | } 92 | 93 | if (event.type === 'noteon' || event.type === 'noteoff') { 94 | return this.lastPostedEvent.note.number === event.note.number; 95 | } 96 | 97 | if (event.type === 'sysex') { 98 | return false; 99 | } 100 | 101 | return false; 102 | // if (event.) 103 | // this.lastPostedEvent !== null && 104 | // this.lastPostedEvent.channel === event.channel && 105 | // this.lastPostedEvent.controller.number === event.controller.number; 106 | } 107 | 108 | render (props, state) { 109 | const lines = state.lines.map(line => { 110 | return ( 111 |

{ line.message }

112 | ) 113 | }); 114 | 115 | return ( 116 |
117 | { lines } 118 |
119 | ); 120 | } 121 | } 122 | 123 | export default Console; 124 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/console/style.css: -------------------------------------------------------------------------------- 1 | .console { 2 | background: #333; 3 | color: #fff; 4 | /* max-height: 95vh; */ 5 | overflow-y: scroll; 6 | 7 | } 8 | 9 | .console p { 10 | border-bottom: 1px dotted #666; 11 | white-space: pre-line; 12 | } 13 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/header/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { Link } from 'preact-router/match'; 3 | import style from './style.css'; 4 | 5 | const Header = () => ( 6 | 20 | ); 21 | 22 | export default Header; 23 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/header/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennigraf/lpd8-web-editor/6352db468a9c6ce2e62ba1a73d3d9fd2b9fa50ce/lpd8-web-editor-preact/src/components/header/style.css -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/knobSettingsController/KnobSettingsController.js: -------------------------------------------------------------------------------- 1 | const { Component } = require("preact"); 2 | import { toInteger } from 'lodash'; 3 | import style from './style.css'; 4 | 5 | class KnobSettingsController extends Component { 6 | state = { 7 | ccValue: 0, 8 | minValue: 0, 9 | maxValue: 0, 10 | } 11 | 12 | ccChange = event => { 13 | const val = toInteger(event.target.value); 14 | this.setState({ ccValue: val }) 15 | } 16 | 17 | minChange = event => { 18 | const val = toInteger(event.target.value); 19 | this.setState({ minValue: val }) 20 | } 21 | 22 | maxChange = event => { 23 | const val = toInteger(event.target.value); 24 | this.setState({ maxValue: val }) 25 | } 26 | 27 | componentDidUpdate(prevProps, prevState) { 28 | if (prevProps.fullSettings !== this.props.fullSettings) { 29 | const padSettings = this.props.fullSettings.knobs.find(knob => { 30 | return knob.number === this.props.knobNr; 31 | }); 32 | this.setState({ 33 | ccValue: padSettings.cc, 34 | minValue: padSettings.min, 35 | maxValue: padSettings.max, 36 | }); 37 | return; 38 | } 39 | 40 | prevProps.onPadSettingsUpdate({ 41 | cc: this.state.ccValue, 42 | min: this.state.minValue, 43 | max: this.state.maxValue, 44 | }); 45 | } 46 | 47 | render (props, state) { 48 | const ccFieldIsValid = state.ccValue >= 0 && state.ccValue <= 127; 49 | const minFieldIsValid = state.minValue >= 0 && state.minValue <= 127; 50 | const maxFieldIsValid = state.maxValue >= 0 && state.maxValue <= 127; 51 | 52 | return ( 53 |
54 |
55 |

56 | Knob K{ props.knobNr } 57 |

58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 |
67 |
68 | 71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 | 79 |
80 |
81 |
82 |
83 | 86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 | 94 |
95 |
96 |
97 |
98 | 101 |
102 |
103 |
104 |
105 |
106 |
107 | ) 108 | } 109 | } 110 | 111 | export default KnobSettingsController -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/knobSettingsController/style.css: -------------------------------------------------------------------------------- 1 | .condensedCard { 2 | line-height: 1; 3 | display: inline-block; 4 | } 5 | 6 | .limitedWidth { 7 | width: 5rem; 8 | } 9 | 10 | .limitedWidthField { 11 | flex-grow: 1; 12 | } 13 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/midiChannelController/MidiChannelController.js: -------------------------------------------------------------------------------- 1 | const { Component } = require("preact"); 2 | import { toInteger } from 'lodash'; 3 | import style from './style.css'; 4 | 5 | class MidiChannelController extends Component { 6 | state = { 7 | channelValue: 1, 8 | } 9 | 10 | channelChange = event => { 11 | const val = toInteger(event.target.value); 12 | this.setState({ channelValue: val }) 13 | } 14 | 15 | componentDidUpdate(prevProps, prevState) { 16 | if (prevProps.fullSettings !== this.props.fullSettings) { 17 | this.setState({ 18 | channelValue: this.props.fullSettings.channel, 19 | }); 20 | return; 21 | } 22 | 23 | prevProps.onChannelUpdate({ 24 | channel: this.state.channelValue, 25 | }); 26 | } 27 | 28 | render (props, state) { 29 | const channelFieldIsValid = state.channelValue >= 1 && state.channelValue <= 16; 30 | 31 | return ( 32 |
33 |
34 |

35 | Midi channel 36 |

37 |
38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 49 |
50 |
51 |
52 |
53 |
54 |
55 | ) 56 | } 57 | } 58 | 59 | export default MidiChannelController -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/midiChannelController/style.css: -------------------------------------------------------------------------------- 1 | .condensedCard { 2 | line-height: 1; 3 | display: inline-block; 4 | } 5 | 6 | .limitedWidth { 7 | width: 5rem; 8 | } 9 | 10 | .limitedWidthField { 11 | flex-grow: 1; 12 | } 13 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/padSettingsController/PadSettingsController.js: -------------------------------------------------------------------------------- 1 | const { Component } = require("preact"); 2 | import { toInteger } from 'lodash'; 3 | import style from './style.css'; 4 | 5 | class PadSettingsController extends Component { 6 | state = { 7 | noteValue: 0, 8 | ccValue: 0, 9 | pcValue: 0, 10 | 11 | isMomentary: true, 12 | } 13 | 14 | noteChange = event => { 15 | const val = toInteger(event.target.value); 16 | this.setState({ noteValue: val }) 17 | } 18 | 19 | ccChange = event => { 20 | const val = toInteger(event.target.value); 21 | this.setState({ ccValue: val }) 22 | } 23 | 24 | pcChange = event => { 25 | const val = toInteger(event.target.value); 26 | this.setState({ pcValue: val }) 27 | } 28 | 29 | componentDidUpdate(prevProps, state) { 30 | if (prevProps.fullSettings !== this.props.fullSettings) { 31 | const padSettings = this.props.fullSettings.pads.find(pad => { 32 | return pad.number === this.props.padNr; 33 | }); 34 | this.setState({ 35 | noteValue: padSettings.note, 36 | pcValue: padSettings.pc, 37 | ccValue: padSettings.cc, 38 | isMomentary: padSettings.momentary 39 | }); 40 | return; 41 | } 42 | 43 | this.props.onPadSettingsUpdate({ 44 | note: this.state.noteValue, 45 | cc: this.state.ccValue, 46 | pc: this.state.pcValue, 47 | momentary: this.state.isMomentary, 48 | }); 49 | } 50 | 51 | render (props, state) { 52 | const noteFieldIsValid = state.noteValue >= 0 && state.noteValue <= 127; 53 | const ccFieldIsValid = state.ccValue >= 0 && state.ccValue <= 127; 54 | const pcFieldIsValid = state.pcValue >= 0 && state.pcValue <= 127; 55 | 56 | return ( 57 |
58 |
59 |

60 | Pad { props.padNr } 61 |

62 |
63 |
64 | 65 |
66 |
67 | 68 |
69 |
70 |
71 |
72 | 75 |
76 |
77 |
78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 |
86 |
87 | 90 |
91 |
92 |
93 |
94 | 95 |
96 |
97 | 98 |
99 |
100 |
101 |
102 | 105 |
106 |
107 |
108 |
109 | 110 |
111 | 115 | 119 |
120 |
121 |
122 | ) 123 | } 124 | } 125 | 126 | export default PadSettingsController -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/components/padSettingsController/style.css: -------------------------------------------------------------------------------- 1 | .condensedCard { 2 | line-height: 1; 3 | display: inline-block; 4 | } 5 | 6 | .limitedWidth { 7 | width: 5rem; 8 | } 9 | 10 | .limitedWidthField { 11 | flex-grow: 1; 12 | } 13 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/index.js: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import App from './components/app'; 3 | 4 | export default App; 5 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lpd8-web-editor-preact", 3 | "short_name": "lpd8-web-editor-preact", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#fff", 8 | "theme_color": "#673ab8", 9 | "icons": [ 10 | { 11 | "src": "/assets/icons/android-chrome-192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "/assets/icons/android-chrome-512x512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/routes/home/index.js: -------------------------------------------------------------------------------- 1 | import { Component, h } from 'preact'; 2 | import WebMidi from 'webmidi'; 3 | import MidiDeviceSelectors from '../../components/MidiDeviceSelectors'; 4 | import PresetLoader from '../../components/PresetLoader'; 5 | import Console from '../../components/console'; 6 | 7 | import style from './style.css'; 8 | 9 | // const isEqual = require('lodash/array/isEqual'); 10 | // const _array = require('lodash/array'); 11 | // import { array } from 'lodash'; 12 | // import array from "lodash/array"; 13 | // console.log(array); 14 | import isEqual from 'lodash'; 15 | import LPD8Preset from '../../LPD8Preset'; 16 | import PadSettingsController from '../../components/padSettingsController/PadSettingsController'; 17 | import KnobSettingsController from '../../components/knobSettingsController/KnobSettingsController'; 18 | import Uploader from '../../components/Uploader'; 19 | import MidiChannelController from '../../components/midiChannelController/MidiChannelController'; 20 | 21 | class WebMidiHelper { 22 | enable() { 23 | return new Promise((resolve, reject) => { 24 | WebMidi.enable(function (err) { 25 | if (err) { 26 | console.log("WebMidi could not be enabled.", err); 27 | reject('could not enable webmidi'); 28 | } else { 29 | console.log("WebMidi enabled with sysex!"); 30 | resolve(); 31 | } 32 | }, true); 33 | }) 34 | } 35 | 36 | static portToDeviceInfo(port) { 37 | return [ 38 | port.manufacturer, port.name, `(${port.type})` 39 | ].filter(item => item && item != '').join(' '); 40 | } 41 | } 42 | const webMidiHelper = new WebMidiHelper(); 43 | 44 | const LPD8MidiHelper = { 45 | parseSysex: data => { 46 | if (data[0] !== 0xf0 || data[data.length-1] !== 0xf7) { 47 | throw Error('invalid sysex packet'); 48 | } 49 | 50 | if (data.length !== 66) { 51 | throw Error('unexpected packet length'); 52 | } 53 | 54 | const packetHeader = data.slice(1, 7); 55 | const expectedPacketHeader = [0x47, 0x7F, 0x75, 0x63, 0x00, 0x3A]; 56 | if (!isEqual(packetHeader, expectedPacketHeader)) { 57 | throw Error('unexpected packet header'); 58 | } 59 | 60 | return LPD8Preset.fromSysex(data); 61 | } 62 | } 63 | 64 | class Home extends Component { 65 | 66 | inDevice = null; 67 | outDevice = null; 68 | 69 | lpd8SettingsToStore = new LPD8Preset(); 70 | 71 | constructor() { 72 | super(); 73 | this.state = { 74 | midiIsConnected: false, 75 | midiInDevices: [], 76 | midiOutDevices: [], 77 | selectedInDevice: null, 78 | selectedOutDevice: null, 79 | 80 | loadedLpd8Settings: new LPD8Preset(), 81 | }; 82 | } 83 | 84 | startMidi = async () => { 85 | try { 86 | await webMidiHelper.enable(); 87 | this.console.postMessage('MIDI started'); 88 | 89 | WebMidi.addListener("connected", e => { 90 | console.log('connected'); 91 | console.log(e); 92 | this.console.postMessage( 93 | `Device found: ${WebMidiHelper.portToDeviceInfo(e.port)}` 94 | ); 95 | if (e.port.type === 'input') { 96 | this.setState({ midiInDevices: WebMidi.inputs }); 97 | } else { 98 | this.setState({ midiOutDevices: WebMidi.outputs }); 99 | } 100 | }); 101 | 102 | // Reacting when a device becomes unavailable 103 | WebMidi.addListener("disconnected", e => { 104 | console.log('disconnected'); 105 | console.log(e); 106 | this.console.postMessage( 107 | `Lost connection to a device: ${WebMidiHelper.portToDeviceInfo(e.port)}` 108 | ); 109 | console.log(e.port); 110 | let stateToUpdate = { }; 111 | if (e.port.type === 'input') { 112 | stateToUpdate = { midiInDevices: WebMidi.inputs }; 113 | } else { 114 | stateToUpdate = { midiOutDevices: WebMidi.outputs }; 115 | } 116 | if (e.port.id === this.state.selectedInDevice?.id) { 117 | stateToUpdate.selectedInDevice = null; 118 | } 119 | if (e.port.id === this.state.selectedOutDevice?.id) { 120 | stateToUpdate.selectedOutDevice = null; 121 | } 122 | this.setState(stateToUpdate); 123 | }); 124 | 125 | this.setState({ 126 | midiIsConnected: true, 127 | midiInDevices: WebMidi.inputs, 128 | midiOutDevices: WebMidi.outputs 129 | }); 130 | 131 | const lpd8InDevice = WebMidi.inputs.find(device => device.name === 'LPD8' ); 132 | if (lpd8InDevice !== undefined) { 133 | this.console.postMessage(`Input device auto-discovery: found ${lpd8InDevice.name}`); 134 | this.inDeviceSelected(lpd8InDevice); 135 | } 136 | 137 | const lpd8OutDevice = WebMidi.outputs.find(device => device.name === 'LPD8' ); 138 | if (lpd8OutDevice !== undefined) { 139 | this.console.postMessage(`Output device auto-discovery: found ${lpd8OutDevice.name}`); 140 | this.outDeviceSelected(lpd8OutDevice); 141 | } 142 | 143 | } catch (e) { 144 | console.log('Could not enable webmidi!'); 145 | console.log(e); 146 | } 147 | } 148 | 149 | inDeviceSelected = device => { 150 | console.log('got in device', device.name); 151 | this.console.postMessage(`Select device for input: ${device.name}`); 152 | if (this.inDevice !== null) { 153 | // remove all handlers 154 | this.inDevice.removeListener(); 155 | } 156 | 157 | this.inDevice = device; 158 | this.setState({ selectedInDevice: device }); 159 | 160 | this.inDevice.on('sysex', undefined, event => { 161 | console.log('got sysex'); 162 | // console.log(event.data); 163 | try { 164 | this.preset = LPD8MidiHelper.parseSysex(event.data); 165 | } catch (e) { 166 | console.log('failed to parse sysex', e); 167 | } 168 | 169 | this.setState({ 170 | loadedLpd8Settings: this.preset 171 | }) 172 | }); 173 | 174 | this.inDevice.on('controlchange', undefined, event => { 175 | this.console.parseAndPostEvent(event); 176 | }); 177 | this.inDevice.on('programchange', undefined, event => { 178 | this.console.parseAndPostEvent(event); 179 | }); 180 | this.inDevice.on('noteon', undefined, event => { 181 | this.console.parseAndPostEvent(event); 182 | }); 183 | this.inDevice.on('noteoff', undefined, event => { 184 | this.console.parseAndPostEvent(event); 185 | }); 186 | this.inDevice.on('sysex', undefined, event => { 187 | this.console.parseAndPostEvent(event); 188 | }); 189 | } 190 | 191 | outDeviceSelected = device => { 192 | console.log('got out device', device.name); 193 | this.console.postMessage(`Select device for output: ${device.name}`); 194 | this.outDevice = device; 195 | this.setState({ selectedOutDevice: device }); 196 | } 197 | 198 | requestPresetFromDevice = presetNr => { 199 | console.log('get preset from device', presetNr); 200 | // const presetData = webMidiHelper.loadPresetFromDevice(this.outDevice, presetNr); 201 | this.outDevice.sendSysex( 202 | 0x47, 203 | [0x7F, 0x75, 0x63, 0x00, 0x01, presetNr] 204 | ); 205 | } 206 | 207 | loadDefaultPreset = () => { 208 | console.log('set default preset'); 209 | this.setState({ 210 | loadedLpd8Settings: new LPD8Preset() 211 | }); 212 | } 213 | 214 | setConsole = console => { 215 | this.console = console; 216 | } 217 | 218 | makePadSettingsUpdateHandler = padNr => { 219 | return data => { 220 | this.lpd8SettingsToStore.setPadData(padNr, data); 221 | } 222 | } 223 | 224 | makeKnobSettingsUpdateHandler = knobNr => { 225 | return data => { 226 | this.lpd8SettingsToStore.setKnobData(knobNr, data); 227 | } 228 | } 229 | 230 | channelUpdateHandler = data => { 231 | this.lpd8SettingsToStore.channel = data.channel; 232 | } 233 | 234 | uploadSettingsToDevice = slotNr => { 235 | console.log('will store settings to device slot', slotNr); 236 | this.lpd8SettingsToStore.preset_nr = slotNr; 237 | const data = this.lpd8SettingsToStore.toSysex(); 238 | this.outDevice.sendSysex(0x47, data); 239 | } 240 | 241 | render(_, state) { 242 | const deviceSelector = ( 243 | 250 | ); 251 | 252 | const presetLoader = ( 253 | 256 | ); 257 | 258 | const uploader = ( 259 | 261 | ); 262 | 263 | const padSettings = [1, 2, 3, 4, 5, 6, 7, 8].map(padNr => { 264 | return ( 265 | 268 | ) 269 | }); 270 | 271 | const knobSettings = [1, 2, 3, 4, 5, 6, 7, 8].map(knobNr => { 272 | return ( 273 | 276 | ) 277 | }); 278 | 279 | return (<> 280 |
281 |
282 |

283 | Welcome to the LPD8 Web Midi Editor. Use this tool to change the MIDI 284 | settings of your AKAI LPD8 controller – even on MacOS Catalina :-). The 285 | code is publicly hosted at 286 | https://github.com/bennigraf/lpd8-web-editor. There you can also find a few 287 | details about the MIDI protocol used by the device. 288 |

289 |

290 | Disclaimer: I am in no way affiliated with AKAI. This tool has been developed completely independently 291 | and there is no official or inofficial support or warranty. Use at your own risk! 292 |

293 |
294 | This tool uses WebMidi to communicate with the LPD8 controller. This 295 | is currently supported best in Google Chrome and Chromium browsers. Supposedly there 296 | are plugins for Firefox and Safari, but I didn't test those – and neither the Edge 297 | browser, which might or might not work. 298 |
299 |

300 | To get started, connect your device 301 | to your computer and click "Start MIDI". 302 |

303 | 308 | 309 | { deviceSelector } 310 | 311 | { presetLoader } 312 | 313 | { uploader } 314 | 315 |
316 | 317 | 318 |
319 |
320 |
321 | 324 |
325 |
326 | { padSettings } 327 |
328 |
329 | { knobSettings } 330 |
331 |
332 | ) 333 | } 334 | } 335 | 336 | export default Home; 337 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/routes/home/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bennigraf/lpd8-web-editor/6352db468a9c6ce2e62ba1a73d3d9fd2b9fa50ce/lpd8-web-editor-preact/src/routes/home/style.css -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/static/protocol.txt: -------------------------------------------------------------------------------- 1 | # LPD8 MIDI sysex protocol 2 | 3 | ## Request preset from device: 9 bytes 4 | 5 | To request preset data from the AKAI LPD8, send a specific Sysex command to the device: 6 | 7 | 7 bytes command header: `F0 47 7F 75 63 00 01` 8 | 1 byte preset-nr (01-04) `01` 9 | 1 byte "EOL" `F7` 10 | 11 | `F0 47 7F 75 63 00 01 01 F7` 12 | 13 | LPD8 answers with a preset dataset formatted similar to what you use to write settings to the device which is described in the next section, i.e. 14 | 15 | ``` 16 | 00 F0 47 7F 75 63 00 3A 04 00 50 06 01 00 4F 07 02 17 | 10 00 53 08 03 00 52 09 04 00 24 00 05 00 26 01 06 18 | 20 00 2A 02 09 00 2E 03 08 00 00 00 7F 01 00 7F 02 19 | 30 00 7F 03 00 7F 04 00 7F 05 00 7F 06 00 7F 08 00 20 | 40 7F F7 21 | ``` 22 | 23 | ## Write preset to device: 66 bytes 24 | 25 | 7 bytes: Sysex header 26 | `F0 47 7F 75 61 00 3A` 27 | 28 | 1 byte: preset number, 1-4 29 | `01` 30 | 31 | 1 byte: MIDI channel of preset, 0-15 (aka 00-0f) 32 | `0f` 33 | 34 | For 8 pads: 4 bytes each (=32 bytes) with 35 | - 1 byte note# 0-x 36 | - 1 byte PC# 0-x 37 | - 1 byte CC# 0-x 38 | - 1 byte "Toggle" vs. "Momentary" (0-1) 39 | i.e. 40 | `01 11 21 01 02 12 22 00 03 13 23 01 04 14 24 00 05 15 25 01 06 16 26 00 07 17 27 01 08 18 28 00` 41 | 42 | For 8 CCs: 3 bytes each (=24 bytes) with 43 | - 1 byte CC# 0-x 44 | - 1 byte CC value "from" 45 | - 1 byte CC value "to" 46 | i.e. 47 | `41 00 09 42 10 19 43 20 29 44 30 39 45 40 49 46 50 59 47 60 69 48 70 79` 48 | 49 | 1 byte "EOL": 50 | `F7` 51 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/style/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | /* height: 100%; */ 3 | width: 100%; 4 | padding: 0; 5 | margin: 0; 6 | background: #FAFAFA; 7 | font-family: 'Helvetica Neue', arial, sans-serif; 8 | font-weight: 400; 9 | color: #444; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | #app { 19 | height: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/sw.js: -------------------------------------------------------------------------------- 1 | import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/'; 2 | 3 | setupRouting(); 4 | setupPrecaching(getFiles()); 5 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LPD8 Web Editor 6 | 7 | 8 | 9 | 10 | <% preact.headEnd %> 11 | 12 | 13 | <% preact.bodyEnd %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/tests/__mocks__/browserMocks.js: -------------------------------------------------------------------------------- 1 | // Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage 2 | /** 3 | * An example how to mock localStorage is given below 👇 4 | */ 5 | 6 | /* 7 | // Mocks localStorage 8 | const localStorageMock = (function() { 9 | let store = {}; 10 | 11 | return { 12 | getItem: (key) => store[key] || null, 13 | setItem: (key, value) => store[key] = value.toString(), 14 | clear: () => store = {} 15 | }; 16 | 17 | })(); 18 | 19 | Object.defineProperty(window, 'localStorage', { 20 | value: localStorageMock 21 | }); */ 22 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/tests/__mocks__/fileMocks.js: -------------------------------------------------------------------------------- 1 | // This fixed an error related to the CSS and loading gif breaking my Jest test 2 | // See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets 3 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /lpd8-web-editor-preact/tests/__mocks__/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-preact-pure'; 3 | 4 | configure({ 5 | adapter: new Adapter() 6 | }); 7 | -------------------------------------------------------------------------------- /lpd8-web-editor-preact/tests/header.test.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import Header from '../src/components/header'; 3 | // See: https://github.com/preactjs/enzyme-adapter-preact-pure 4 | import { shallow } from 'enzyme'; 5 | 6 | describe('Initial Test of the Header', () => { 7 | test('Header renders 3 nav items', () => { 8 | const context = shallow(
); 9 | expect(context.find('h1').text()).toBe('Preact App'); 10 | expect(context.find('Link').length).toBe(3); 11 | }); 12 | }); 13 | --------------------------------------------------------------------------------