├── .gitignore ├── LAYOUT.md ├── LAYOUT.svg ├── LICENSE.md ├── PREVIEW.jpg ├── README.md └── desktop ├── icon.icns ├── icon.ico ├── main.js ├── package.json └── sources ├── index.html ├── links ├── fonts.css ├── main.css └── reset.css ├── media └── fonts │ ├── input_mono_medium.ttf │ └── input_mono_regular.ttf └── scripts ├── arp.js ├── cheatcode.js ├── core ├── jammer.js ├── oscillator.js ├── player-worker.js ├── player.js └── signal.js ├── editor.js ├── follower.js ├── instrument.js ├── lib ├── controller.js ├── history.js └── theme.js ├── loop.js ├── marabu.js ├── sequencer.js ├── song.js ├── track.js └── ui ├── choice.js ├── slider.js └── uv.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | builds/ 3 | *.wav 4 | .vscode/ -------------------------------------------------------------------------------- /LAYOUT.md: -------------------------------------------------------------------------------- 1 | ## default Mode 2 | 3 | ### File 4 | - New: `CmdOrCtrl+N` 5 | - Open: `CmdOrCtrl+O` 6 | - Save: `CmdOrCtrl+S` 7 | - Save As: `CmdOrCtrl+Shift+S` 8 | - Render: `CmdOrCtrl+R` 9 | - Export Ins: `CmdOrCtrl+I` 10 | 11 | ### Edit 12 | - Inc BPM: `>` 13 | - Dec BPM: `<` 14 | - Delete: `Backspace` 15 | - Undo: `CmdOrCtrl+Z` 16 | - Redo: `CmdOrCtrl+Shift+Z` 17 | 18 | ### Select 19 | - 1st Row: `1` 20 | - 4th Row: `2` 21 | - 8th Row: `3` 22 | - 12th Row: `4` 23 | - 16th Row: `5` 24 | - 20th Row: `6` 25 | - 24th Row: `7` 26 | - 28th Row: `8` 27 | 28 | ### Track 29 | - Next Inst: `Right` 30 | - Prev Inst: `Left` 31 | - Next Row: `Down` 32 | - Prev Row: `Up` 33 | - Next Track: `CmdOrCtrl+Down` 34 | - Prev Track: `CmdOrCtrl+Up` 35 | - Next Pattern: `CmdOrCtrl+Right` 36 | - Prev Pattern: `CmdOrCtrl+Left` 37 | 38 | ### Play 39 | - Track: `Space` 40 | - Range: `Enter` 41 | - Stop: `Esc` 42 | 43 | ### Mode 44 | - Cheatcode: `CmdOrCtrl+K` 45 | - Loop: `CmdOrCtrl+L` 46 | - Arp: `CmdOrCtrl+M` 47 | - Composer: `M` 48 | 49 | ### Keyboard 50 | - Inc Octave: `X` 51 | - Dec Octave: `Z` 52 | - C: `A` 53 | - C#: `W` 54 | - D: `S` 55 | - D#: `E` 56 | - E: `D` 57 | - F: `F` 58 | - F#: `T` 59 | - G: `G` 60 | - G#: `Y` 61 | - A: `H` 62 | - A#: `U` 63 | - B: `J` 64 | - (Right)C: `Shift+A` 65 | - (Right)C#: `Shift+W` 66 | - (Right)D: `Shift+S` 67 | - (Right)D#: `Shift+E` 68 | - (Right)E: `Shift+D` 69 | - (Right)F: `Shift+F` 70 | - (Right)F#: `Shift+T` 71 | - (Right)G: `Shift+G` 72 | - (Right)G#: `Shift+Y` 73 | - (Right)A: `Shift+H` 74 | - (Right)A#: `Shift+U` 75 | - (Right)B: `Shift+J` 76 | 77 | ### Instrument 78 | - Next Control: `Shift+Up` 79 | - Prev Control: `Shift+Down` 80 | - Inc Control +10: `Shift+Right` 81 | - Dec Control -10: `Shift+Left` 82 | - Inc Control 1: `}` 83 | - Dec Control -1: `{` 84 | - Inc Control 10(alt): `]` 85 | - Dec Control -10(alt): `[` 86 | - Min: `9` 87 | - Max: `0` 88 | - Keyframe: `/` 89 | 90 | 91 | ## cheatcode Mode 92 | 93 | ### Mode 94 | - Stop: `Esc` 95 | - Copy: `C` 96 | - Paste: `V` 97 | - Erase: `Backspace` 98 | 99 | ### Effect 100 | - Inc Note +12: `]` 101 | - Dec Note -12: `[` 102 | - Inc Note +1: `}` 103 | - Dec Note -1: `{` 104 | 105 | ### Selection 106 | - All: `1` 107 | - 2nd: `2` 108 | - 3rd: `3` 109 | - 4th: `4` 110 | - 5th: `5` 111 | - 6th: `6` 112 | - 7th: `7` 113 | - 8th: `8` 114 | - Offset +1: `Right` 115 | - Offset -1: `Left` 116 | - Length +1: `Down` 117 | - Length -1: `Up` 118 | 119 | ### Keyboard 120 | - C: `A` 121 | - C#: `W` 122 | - D: `S` 123 | - D#: `E` 124 | - E: `D` 125 | - F: `F` 126 | - F#: `T` 127 | - G: `G` 128 | - G#: `Y` 129 | - A: `H` 130 | - A#: `U` 131 | - B: `J` 132 | 133 | 134 | ## loop Mode 135 | 136 | ### Edit 137 | - Clear: `X` 138 | - Copy: `C` 139 | - Paste: `V` 140 | - Delete: `Backspace` 141 | 142 | ### Mode 143 | - Play: `Enter` 144 | - Stop: `Esc` 145 | - render: `CmdOrCtrl+R` 146 | 147 | ### Selection 148 | - Solo: `/` 149 | - 1 Row: `1` 150 | - 2 Rows: `2` 151 | - 3 Rows: `3` 152 | - 4 Rows: `4` 153 | - 5 Rows: `5` 154 | - 6 Rows: `6` 155 | - 7 Rows: `7` 156 | - 8 Rows: `8` 157 | 158 | 159 | ## arp Mode 160 | 161 | ### Mode 162 | - Pause/Stop: `Esc` 163 | 164 | ### Keyboard 165 | - C: `A` 166 | - C#: `W` 167 | - D: `S` 168 | - D#: `E` 169 | - E: `D` 170 | - F: `F` 171 | - F#: `T` 172 | - G: `G` 173 | - G#: `Y` 174 | - A: `H` 175 | - A#: `U` 176 | - B: `J` 177 | - Skip: `Space` 178 | 179 | 180 | -------------------------------------------------------------------------------- /LAYOUT.svg: -------------------------------------------------------------------------------- 1 | ESCSTOPDOCUMENT11ST ROW24TH ROW38TH ROW412TH ROW516TH ROW620TH ROW724TH ROW828TH ROW9MIN0MAX-PLUSBACKSPACEDELETERESETTABQQUITWC#ED#RRENDERTF#YG#UA#IEXPORT IOOPENP[DEC CONT]INC CONT|CAPSACSDSAVEDEFFGGHAHIDEJBKCHEATCODLLOOP;'ENTERRANGEFULLSCRESHIFTZDEC OCTAUNDOXINC OCTACVBNNEWMCOMPOSERARP,ABOUT.INSPECT/KEYFRAMECAPSLOCKCTRLCMDALTSPACETRACKCTRLPNFNALT -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Hundredrabbits 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PREVIEW.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hundredrabbits/Marabu/4e358119fe3792eec37aa478f59728404a1440b4/PREVIEW.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Marabu 2 | 3 | Marabu is a free and open-source music tracker built from [Soundbox](https://github.com/mbitsnbites/soundbox). 4 | 5 | 6 | 7 | ## Guide 8 | 9 | If this is your first time using a tracker, don't worry this quick introduction will cover the basics of writing a little track, and exporting it to an audio file. 10 | 11 | The interface is divided into 3 columns, the *sequencer*, the *pattern editor* and the *instrument*. By default, the application launches with an active pattern, in the first instrument. There is a maximum of 16 instruments that can play at the same time. 12 | 13 | To move the **pattern cursor**, use the `arrowUp`, and `arrowDown`, keys. Pressing the keyboard keys a,s,d,f,g,h & j will record a note in the first row of the selected row. Pressing the `ArrowDown` and `ArrowUp` keys, will move the *cursor* up/down in the sequencer. Allowing you to fill `pattern #1` with notes. Pressing `space` will play the pattern, pressing `esc` will stop and pressing `del`/`backspace` will erase a note. By default, a new Marabu track has the pattern #1 loaded into the first instrument. 14 | 15 | To change the **sequencer patterns**, use the arrow keys while `holding alt`. To add notes to a second instrument, move to the second column and press `alt ArrowRight`, this will set the first row of the second instrument to 1, and allow you to record notes. Press `alt ArrowDown` to move to the second row, and press `alt ArrowRight` again twice, to extend the track to 2 rows, and begin adding notes to the second row of the second instrument. 16 | 17 | To change the **instrument controls**, use the arrow keys while `holding shift`. To save your song, press `ctrl s`, to render an audio file(.wav) press `ctrl r`. 18 | 19 | ## Controls 20 | 21 | ## default Mode 22 | 23 | ### File 24 | - New: `CmdOrCtrl+N` 25 | - Open: `CmdOrCtrl+O` 26 | - Save: `CmdOrCtrl+S` 27 | - Save As: `CmdOrCtrl+Shift+S` 28 | - Render: `CmdOrCtrl+R` 29 | - Export Ins: `CmdOrCtrl+I` 30 | 31 | ### Edit 32 | - Inc BPM: `>` 33 | - Dec BPM: `<` 34 | - Delete: `Backspace` 35 | - Undo: `CmdOrCtrl+Z` 36 | - Redo: `CmdOrCtrl+Shift+Z` 37 | 38 | ### Select 39 | - 1st Row: `1` 40 | - 4th Row: `2` 41 | - 8th Row: `3` 42 | - 12th Row: `4` 43 | - 16th Row: `5` 44 | - 20th Row: `6` 45 | - 24th Row: `7` 46 | - 28th Row: `8` 47 | 48 | ### Track 49 | - Next Inst: `Right` 50 | - Prev Inst: `Left` 51 | - Next Row: `Down` 52 | - Prev Row: `Up` 53 | - Next Track: `CmdOrCtrl+Down` 54 | - Prev Track: `CmdOrCtrl+Up` 55 | - Next Pattern: `CmdOrCtrl+Right` 56 | - Prev Pattern: `CmdOrCtrl+Left` 57 | 58 | ### Play 59 | - Track: `Space` 60 | - Range: `Enter` 61 | - Stop: `Esc` 62 | 63 | ### Mode 64 | - Cheatcode: `CmdOrCtrl+K` 65 | - Loop: `CmdOrCtrl+L` 66 | - Arp: `CmdOrCtrl+M` 67 | - Composer: `M` 68 | 69 | ### Keyboard 70 | - Inc Octave: `X` 71 | - Dec Octave: `Z` 72 | - C: `A` 73 | - C#: `W` 74 | - D: `S` 75 | - D#: `E` 76 | - E: `D` 77 | - F: `F` 78 | - F#: `T` 79 | - G: `G` 80 | - G#: `Y` 81 | - A: `H` 82 | - A#: `U` 83 | - B: `J` 84 | - (Right)C: `Shift+A` 85 | - (Right)C#: `Shift+W` 86 | - (Right)D: `Shift+S` 87 | - (Right)D#: `Shift+E` 88 | - (Right)E: `Shift+D` 89 | - (Right)F: `Shift+F` 90 | - (Right)F#: `Shift+T` 91 | - (Right)G: `Shift+G` 92 | - (Right)G#: `Shift+Y` 93 | - (Right)A: `Shift+H` 94 | - (Right)A#: `Shift+U` 95 | - (Right)B: `Shift+J` 96 | 97 | ### Instrument 98 | - Next Control: `Shift+Up` 99 | - Prev Control: `Shift+Down` 100 | - Inc Control +10: `Shift+Right` 101 | - Dec Control -10: `Shift+Left` 102 | - Inc Control 1: `}` 103 | - Dec Control -1: `{` 104 | - Inc Control 10(alt): `]` 105 | - Dec Control -10(alt): `[` 106 | - Min: `9` 107 | - Max: `0` 108 | - Keyframe: `/` 109 | 110 | ## cheatcode Mode 111 | 112 | ### Mode 113 | - Stop: `Esc` 114 | - Copy: `C` 115 | - Paste: `V` 116 | - Erase: `Backspace` 117 | 118 | ### Effect 119 | - Inc Note +12: `]` 120 | - Dec Note -12: `[` 121 | - Inc Note +1: `}` 122 | - Dec Note -1: `{` 123 | 124 | ### Selection 125 | - All: `1` 126 | - 2nd: `2` 127 | - 3rd: `3` 128 | - 4th: `4` 129 | - 5th: `5` 130 | - 6th: `6` 131 | - 7th: `7` 132 | - 8th: `8` 133 | - Offset +1: `Right` 134 | - Offset -1: `Left` 135 | - Length +1: `Down` 136 | - Length -1: `Up` 137 | 138 | ### Keyboard 139 | - C: `A` 140 | - C#: `W` 141 | - D: `S` 142 | - D#: `E` 143 | - E: `D` 144 | - F: `F` 145 | - F#: `T` 146 | - G: `G` 147 | - G#: `Y` 148 | - A: `H` 149 | - A#: `U` 150 | - B: `J` 151 | 152 | ## loop Mode 153 | 154 | ### Edit 155 | - Clear: `X` 156 | - Copy: `C` 157 | - Paste: `V` 158 | - Delete: `Backspace` 159 | 160 | ### Mode 161 | - Play: `Enter` 162 | - Stop: `Esc` 163 | - render: `CmdOrCtrl+R` 164 | 165 | ### Selection 166 | - Solo: `/` 167 | - 1 Row: `1` 168 | - 2 Rows: `2` 169 | - 3 Rows: `3` 170 | - 4 Rows: `4` 171 | - 5 Rows: `5` 172 | - 6 Rows: `6` 173 | - 7 Rows: `7` 174 | - 8 Rows: `8` 175 | 176 | ## arp Mode 177 | 178 | ### Mode 179 | - Pause/Stop: `Esc` 180 | 181 | ### Keyboard 182 | - C: `A` 183 | - C#: `W` 184 | - D: `S` 185 | - D#: `E` 186 | - E: `D` 187 | - F: `F` 188 | - F#: `T` 189 | - G: `G` 190 | - G#: `Y` 191 | - A: `H` 192 | - A#: `U` 193 | - B: `J` 194 | - Skip: `Space` 195 | 196 | 197 | 198 | ## Extras 199 | 200 | - Download additional [themes](https://github.com/hundredrabbits/Themes). 201 | - Support this project through [Patreon](https://patreon.com/hundredrabbits). 202 | - See the [License](LICENSE.md) file for license rights and limitations (MIT). 203 | -------------------------------------------------------------------------------- /desktop/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hundredrabbits/Marabu/4e358119fe3792eec37aa478f59728404a1440b4/desktop/icon.icns -------------------------------------------------------------------------------- /desktop/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hundredrabbits/Marabu/4e358119fe3792eec37aa478f59728404a1440b4/desktop/icon.ico -------------------------------------------------------------------------------- /desktop/main.js: -------------------------------------------------------------------------------- 1 | const {app, BrowserWindow, webFrame, Menu} = require('electron') 2 | const path = require('path') 3 | const url = require('url') 4 | const shell = require('electron').shell; 5 | 6 | let is_shown = true; 7 | 8 | app.inspect = function() 9 | { 10 | app.win.toggleDevTools(); 11 | } 12 | 13 | app.toggle_fullscreen = function() 14 | { 15 | app.win.setFullScreen(app.win.isFullScreen() ? false : true); 16 | } 17 | 18 | app.toggle_visible = function() 19 | { 20 | if(is_shown){ app.win.hide(); } else{ app.win.show(); } 21 | } 22 | 23 | app.inject_menu = function(m) 24 | { 25 | Menu.setApplicationMenu(Menu.buildFromTemplate(m)); 26 | } 27 | 28 | app.win = null; 29 | 30 | app.on('ready', () => 31 | { 32 | app.win = new BrowserWindow({width: 890, height: 540, backgroundColor:"#000", minWidth: 595, minHeight: 540, frame:false, autoHideMenuBar: true, icon: __dirname + '/icon.ico'}) 33 | 34 | app.win.loadURL(`file://${__dirname}/sources/index.html`) 35 | 36 | app.win.on('closed', () => { 37 | win = null 38 | app.quit() 39 | }) 40 | 41 | app.win.on('hide',function() { 42 | is_shown = false; 43 | }) 44 | 45 | app.win.on('show',function() { 46 | is_shown = true; 47 | }) 48 | }) 49 | 50 | app.on('window-all-closed', () => 51 | { 52 | app.quit() 53 | }) 54 | 55 | app.on('activate', () => { 56 | if (app.win === null) { 57 | createWindow() 58 | } 59 | }) -------------------------------------------------------------------------------- /desktop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Marabu", 3 | "version": "0.1.0", 4 | "main": "main.js", 5 | "scripts": { 6 | "start": "electron .", 7 | "clean" : "rm -r ~/Desktop/Marabu-darwin-x64/ ; rm -r ~/Desktop/Marabu-linux-x64/ ; rm -r ~/Desktop/Marabu-win32-x64/ ; echo 'cleaned build location'", 8 | "build_osx" : "electron-packager . Marabu --platform=darwin --arch=x64 --out ~/Desktop/ --overwrite --icon=icon.icns ; echo 'Built for OSX'", 9 | "build_linux" : "electron-packager . Marabu --platform=linux --arch=x64 --out ~/Desktop/ --overwrite --icon=icon.ico ; echo 'Built for LINUX'", 10 | "build_win" : "electron-packager . Marabu --platform=win32 --arch=x64 --out ~/Desktop/ --overwrite --icon=icon.ico ; echo 'Built for WIN'", 11 | "build" : "npm run clean ; npm run build_osx ; npm run build_linux ; npm run build_win", 12 | "push_osx" : "~/butler push ~/Desktop/Marabu-darwin-x64/ hundredrabbits/marabu:osx-64", 13 | "push_linux" : "~/butler push ~/Desktop/Marabu-linux-x64/ hundredrabbits/marabu:linux-64", 14 | "push_win" : "~/butler push ~/Desktop/Marabu-win32-x64/ hundredrabbits/marabu:windows-64", 15 | "push_theme" : "~/butler push ~/Github/HundredRabbits/Themes/themes/ hundredrabbits/marabu:themes", 16 | "push_status" : "~/butler status hundredrabbits/marabu", 17 | "push" : "npm run build ; npm run push_theme ; npm run push_osx ; npm run push_linux ; npm run push_win ; npm run clean ; npm run push_status" 18 | }, 19 | "devDependencies": { 20 | "electron": "^1.8.1" 21 | }, 22 | "dependencies": { 23 | "electron-packager": "^8.4.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /desktop/sources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Marabu 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /desktop/sources/links/fonts.css: -------------------------------------------------------------------------------- 1 | /* Input */ 2 | 3 | @font-face { 4 | font-family: 'input_mono_regular'; 5 | src: url('../media/fonts/input_mono_regular.ttf') format('truetype'); 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | @font-face { 11 | font-family: 'input_mono_medium'; 12 | src: url('../media/fonts/input_mono_medium.ttf') format('truetype'); 13 | font-weight: normal; 14 | font-style: normal; 15 | } -------------------------------------------------------------------------------- /desktop/sources/links/main.css: -------------------------------------------------------------------------------- 1 | body { background:#000; -webkit-user-select: none;-webkit-app-region: drag; overflow: hidden; transition: background-color 500ms } 2 | yu { display: block; font-family: 'input_mono_regular'; font-size:11px; line-height: 30px; vertical-align: top; -webkit-app-region: no-drag; } 3 | yu:after { display: table; content: ""; clear: both; } 4 | ln { display: block;} 5 | ln i { color:#999; } 6 | hl { line-height: 29px; font-family: 'input_mono_medium'; display: block; border-bottom:1px solid black; } 7 | t { vertical-align: top; display: inline-block; } 8 | svg { display: inline-block; vertical-align: top } 9 | textarea { background:none; padding:px 0px; line-height: 15px; overflow: hidden} 10 | b { font-weight: normal; font-family: 'input_mono_medium'} 11 | 12 | app { display: block; width:30px; height:30px; padding:30px; color:black; overflow: hidden; z-index:20; margin: calc(50vh - 270px) auto; transition: background-color 150ms, opacity 350ms; max-width: 830px; height: 480px; position: relative; opacity:0; width:calc(100% - 60px); } 13 | 14 | .tracks tr td { padding: 0 2.5px; color:#555} 15 | #instrument { width:130px; display:block; vertical-align:top; margin-left:15px; line-height:15px; overflow: hidden; position: absolute; right:25px;} 16 | #instrument > div > * { width:100%; cursor:pointer; } 17 | #instrument .family { margin-bottom:15px; } 18 | #sequencer { display:block; vertical-align:top; float:left; max-height: 465px; overflow:hidden; position: relative;} 19 | #sequencer tr td { padding:0px;} 20 | #sequencer tr td:first-child { padding-left:2.5px;} 21 | #sequencer tr td:last-child { padding-right:2.5px;} 22 | #scrollbar { background: #333;width: 1px;height: 0px;position: absolute;left: 161px;top: 30px; display: none } 23 | #editor { display:block; vertical-align:top; margin-left:15px; float:left; max-width: 571px; overflow: hidden; height:480px; max-width:calc(100% - 270px);} 24 | #editor #composer-table { width:550px; } 25 | #editor #pattern-table { width:550px; } 26 | .tracks * { line-height: 15px } 27 | .tracks tr:hover { color:#999} 28 | .tracks tr td { position:relative} 29 | .tracks td:hover { cursor:pointer} 30 | .tracks tr td:hover { cursor:pointer; } 31 | .tracks tr th { color:#555; font-family: 'input_mono_regular'; padding: 0 2.5px;} 32 | .tracks tr th:hover { cursor:pointer; color:#999} 33 | 34 | #editor.composer #composer-table { display: table; } 35 | #composer-table { width:100%; margin-top: -240px; transition: all 150ms; margin-bottom: 15px } 36 | #composer-table tr td, #composer-table tr th { line-height: 15px; padding:0px; text-align: center } 37 | #editor.composer #composer-table { margin-top:0px; } 38 | #position { position: absolute;bottom: 30px;line-height: 15px;padding: 0px 2.5px;width: 110px;} 39 | 40 | .right { float:right; } 41 | .left { float:left; } 42 | .hide { display: none } 43 | .pointer { cursor:pointer; } 44 | 45 | .control { height: 15px; overflow: hidden; } 46 | .control t.name { width:25px; display:inline-block; padding:0px 2.5px;text-align: center } 47 | .control t.value { display:inline-block; padding:0px 2.5px; } 48 | .control.slider { cursor: pointer; display: block; position:relative; } 49 | .control.slider.selected { position: relative; } 50 | .control.slider .slide { width:60px; height:15px; display: inline-block; position:relative; padding:0px 2.5px;} 51 | .control.uv { height:60px; } 52 | 53 | /* Theme Defaults */ 54 | 55 | :root { --background: "#222"; --f_high: "#fff";--f_med: "#777";--f_low: "#444";--f_inv: "#000";--b_high: "#000";--b_med: "#affec7";--b_low: "#000";--b_inv: "#affec7"; } 56 | 57 | body { background:var(--background) !important } 58 | .fh { color:var(--f_high) !important; stroke:var(--f_high) !important } 59 | .fm { color:var(--f_med) !important ; stroke:var(--f_med) !important } 60 | .fl { color:var(--f_low) !important ; stroke:var(--f_low) !important } 61 | .f_inv { color:var(--f_inv) !important ; stroke:var(--f_inv) !important } 62 | .f_inv { color:var(--f_inv) !important ; stroke:var(--f_inv) !important } 63 | .bh { background:var(--b_high) !important; fill:var(--b_high) !important } 64 | .bm { background:var(--b_med) !important ; fill:var(--b_med) !important } 65 | .bl { background:var(--b_low) !important ; fill:var(--b_low) !important } 66 | .b_inv { background:var(--b_inv) !important ; fill:var(--b_inv) !important } 67 | .b { background:var(--background) !important } 68 | 69 | #editor table tr.selected { background:var(--b_low) !important ; } 70 | #editor table tr.selected td { color:var(--f_med) !important; } 71 | #editor table tr.selected td.selected { color:var(--f_high) !important ; } 72 | #editor table td.beat { color:var(--f_med) !important; } 73 | 74 | .control .name { color:var(--f_med) !important; } 75 | .control .value { color:var(--f_med) !important ; } 76 | .control.slider.min .value { color:var(--f_low) !important ; } 77 | .control.slider.max .value { color:var(--f_high) !important ; } 78 | .control.slider.keyframed .name { background: var(--b_inv) !important; color: var(--f_inv) !important; } 79 | .control.slider .slide { color:var(--f_low) !important; } 80 | .control.slider .slide .fg { color:var(--f_high) !important } 81 | .control.slider.selected { background:var(--b_low) !important; } 82 | -------------------------------------------------------------------------------- /desktop/sources/links/reset.css: -------------------------------------------------------------------------------- 1 | /* Reset */ 2 | 3 | * { margin:0;padding:0;border:0;outline:0;text-decoration:none;font-weight:inherit;font-style:inherit;color:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;list-style:none;border-collapse:separate;border-spacing:0; -webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;} 4 | *:focus { outline: none} 5 | ::selection { background: #eee; opacity:1.0; color:#000; padding:10px; /* Safari */ } -------------------------------------------------------------------------------- /desktop/sources/media/fonts/input_mono_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hundredrabbits/Marabu/4e358119fe3792eec37aa478f59728404a1440b4/desktop/sources/media/fonts/input_mono_medium.ttf -------------------------------------------------------------------------------- /desktop/sources/media/fonts/input_mono_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hundredrabbits/Marabu/4e358119fe3792eec37aa478f59728404a1440b4/desktop/sources/media/fonts/input_mono_regular.ttf -------------------------------------------------------------------------------- /desktop/sources/scripts/arp.js: -------------------------------------------------------------------------------- 1 | function Arp() 2 | { 3 | this.is_active = false; 4 | this.is_recording = false; 5 | this.memory = []; 6 | 7 | this.start = function() 8 | { 9 | console.log("arp","start") 10 | this.is_active = true; 11 | this.is_recording = true; 12 | this.memory = []; 13 | marabu.controller.set("arp"); 14 | marabu.update(); 15 | } 16 | 17 | this.stop = function() 18 | { 19 | if(this.is_recording){ this.is_recording = false; marabu.update(); return; } 20 | 21 | this.is_active = false; 22 | marabu.controller.set("default"); 23 | marabu.update(); 24 | } 25 | 26 | this.ins = function(mod) 27 | { 28 | if(mod == -99){ 29 | this.record(null); 30 | return 31 | } 32 | 33 | var note = (marabu.selection.octave * 12)+mod; 34 | 35 | if(this.is_recording && mod != null){ 36 | this.record(note); 37 | return; 38 | } 39 | else{ 40 | this.play(note); 41 | } 42 | } 43 | 44 | this.play = function(note) 45 | { 46 | var key = this.memory[0]; 47 | for(id in this.memory){ 48 | if(marabu.selection.row+parseInt(id) >= 32){ break; } 49 | var n = this.memory[id]; 50 | var offset = n.distance - key.distance; 51 | var row = marabu.selection.row+parseInt(id); 52 | marabu.song.inject_note_at(marabu.selection.instrument,marabu.selection.track,row,note ? this.offset_note(note,offset) : 0); 53 | } 54 | marabu.update(); 55 | } 56 | 57 | this.record = function(note) 58 | { 59 | this.memory.push(note ? parse_note(note) : 0); 60 | console.log("record",this.memory) 61 | marabu.update(); 62 | } 63 | 64 | this.offset_note = function(note,offset) 65 | { 66 | var dict = this.notes_dict()[parse_note(note).sharp ? "sharp" : "normal"]; 67 | var index = dict.indexOf(note); 68 | 69 | return dict[index+offset]; 70 | } 71 | 72 | this.notes_dict = function() 73 | { 74 | var h = {normal:[],sharp:[]} 75 | var n = 12 * 3; 76 | while(n < 87){ 77 | var note = parse_note(n); 78 | h[note.sharp ? "sharp" : "normal"].push(n) 79 | n += 1 80 | } 81 | return h; 82 | } 83 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/cheatcode.js: -------------------------------------------------------------------------------- 1 | function Cheatcode() 2 | { 3 | this.is_active = false; 4 | this.val = ""; 5 | 6 | this.rate = 1; 7 | this.offset = 0; 8 | this.length = 0; 9 | 10 | this.selection = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; 11 | 12 | this.start = function() 13 | { 14 | console.log("cheatcode","start") 15 | marabu.loop.stop(); 16 | 17 | var active_pattern = marabu.song.pattern_at(marabu.selection.instrument,marabu.selection.track); 18 | 19 | if(active_pattern == 0){ return; } 20 | 21 | this.is_active = true; 22 | this.select(); 23 | marabu.selection.row = 0; 24 | marabu.update(); 25 | marabu.controller.set("cheatcode"); 26 | } 27 | 28 | this.stop = function() 29 | { 30 | console.log("cheatcode","stop") 31 | this.is_active = false; 32 | this.rate = 1; 33 | this.offset = 0; 34 | this.length = 0; 35 | 36 | this.val = ""; 37 | marabu.update(); 38 | marabu.controller.set("default"); 39 | } 40 | 41 | this.set_rate = function(mod) 42 | { 43 | this.rate = mod; 44 | this.select(); 45 | marabu.update(); 46 | } 47 | 48 | this.set_offset = function(mod) 49 | { 50 | this.offset += mod; 51 | this.select(); 52 | marabu.update(); 53 | } 54 | 55 | this.set_length = function(mod) 56 | { 57 | this.length = this.length == 0 ? 1 : this.length; 58 | this.length += mod; 59 | this.select(); 60 | marabu.update(); 61 | } 62 | 63 | this.selection_count = function() 64 | { 65 | var count = 0; 66 | for(var row = 0; row < 32; row++){ 67 | count += this.selection[row]; 68 | } 69 | return count; 70 | } 71 | 72 | this.select = function() 73 | { 74 | this.selection = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; 75 | 76 | this.offset = this.offset == 15 ? 16 : this.offset; 77 | this.length = this.length == 15 ? 16 : this.length; 78 | for(var row = 0; row < 32; row++){ 79 | var key = (row-this.offset) % this.rate; 80 | if(key < 0){ key = this.rate + key; } 81 | if(key == 0 || key < this.length){ this.selection[row] = key+1; } 82 | } 83 | if(this.offset > 0){ 84 | marabu.selection.row = (this.offset)%32; 85 | } 86 | } 87 | 88 | this.ins = function(mod) 89 | { 90 | console.log("cheatcode","ins") 91 | for(var row = 0; row < 32; row++){ 92 | if(!this.selection[row]){ continue; } 93 | var note = (marabu.selection.octave * 12)+mod; 94 | marabu.song.inject_note_at(marabu.selection.instrument,marabu.selection.track,row,note); 95 | this.selection[row] = 0; 96 | marabu.selection.row = (row+1)%32; 97 | marabu.update(); 98 | if(this.selection_count() == 0){ this.stop(); } 99 | return; 100 | } 101 | marabu.history.push(marabu.song.song()); 102 | } 103 | 104 | this.del = function() 105 | { 106 | console.log("cheatcode","del") 107 | for(var row = 0; row < 32; row++){ 108 | if(!this.selection[row]){ continue;} 109 | marabu.song.inject_note_at(marabu.selection.instrument,marabu.selection.track,row,-87); 110 | marabu.song.inject_note_at(marabu.selection.instrument,marabu.selection.track,row+32,-87); 111 | } 112 | marabu.history.push(marabu.song.song()); 113 | this.stop(); 114 | } 115 | 116 | this.mod = function(mod) 117 | { 118 | console.log("cheatcode","mod") 119 | for(var row = 0; row < 32; row++){ 120 | if(!this.selection[row]){ continue;} 121 | var note = marabu.song.note_at(marabu.selection.instrument,marabu.selection.track,row); 122 | if(note == 0){ continue; } 123 | note += mod; 124 | marabu.selection.row = row; 125 | marabu.song.inject_note_at(marabu.selection.instrument,marabu.selection.track,row,note-87); 126 | } 127 | marabu.history.push(marabu.song.song()); 128 | marabu.update(); 129 | } 130 | 131 | this.stash = null; 132 | 133 | this.copy = function() 134 | { 135 | console.log("cheatcode","copy") 136 | var new_stash = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; 137 | 138 | for(var row = 0; row < 32; row++){ 139 | if(!this.selection[row]){ continue;} 140 | var note = marabu.song.note_at(marabu.selection.instrument,marabu.selection.track,row); 141 | new_stash[row] = note; 142 | } 143 | this.stash = new_stash; 144 | this.stop(); 145 | } 146 | 147 | this.paste = function() 148 | { 149 | console.log("cheatcode","paste") 150 | for(var row = 0; row < 32; row++){ 151 | var target_row = (row + marabu.selection.row) % 32; 152 | if(this.stash[row] == 0){ continue; } 153 | marabu.song.inject_note_at(marabu.selection.instrument,marabu.selection.track,target_row,this.stash[row]-87); 154 | } 155 | marabu.history.push(marabu.song.song()); 156 | this.stop(); 157 | } 158 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/core/jammer.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: javascript; tab-width: 2; indent-tabs-mode: nil; -*- 2 | * 3 | * Copyright (c) 2011-2013 Marcus Geelnard 4 | * 5 | * This file is part of SoundBox. 6 | * 7 | * SoundBox is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * SoundBox is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with SoundBox. If not, see . 19 | * 20 | */ 21 | 22 | "use strict"; 23 | 24 | var CJammer = function () 25 | { 26 | var signal_processor = new Signal_Processor(); 27 | 28 | // Currently playing notes. 29 | var MAX_POLYPHONY = 2; 30 | var mPlayingNotes = []; 31 | 32 | // Current instrument. 33 | var mInstr; 34 | 35 | // Current row length (i.e. BPM). 36 | var mRowLen; 37 | 38 | // Effect state. 39 | var mFXState; 40 | 41 | // Delay buffers. 42 | var MAX_DELAY = 131072; // Must be a power of 2. 43 | var mDlyLeft, mDlyRight; 44 | 45 | // Web Audio context. 46 | var mAudioContext; 47 | var mScriptNode; 48 | var mSampleRate; 49 | var mRateScale; 50 | 51 | 52 | //-------------------------------------------------------------------------- 53 | // Sound synthesis engine. 54 | //-------------------------------------------------------------------------- 55 | 56 | var getnotefreq = function (n) 57 | { 58 | return (174.614115728 / mSampleRate) * Math.pow(2, (n-128)/12); 59 | }; 60 | 61 | // Array of oscillator functions. 62 | var mOscillators = 63 | [ 64 | new Oscillator().sin, 65 | new Oscillator().square, 66 | new Oscillator().saw, 67 | new Oscillator().tri, 68 | new Oscillator().noise, 69 | new Oscillator().rev, 70 | new Oscillator().pulse 71 | ]; 72 | 73 | // Fill the buffer with more audio, and advance state accordingly. 74 | var generateTimeSlice = function (leftBuf, rightBuf) 75 | { 76 | var numSamples = rightBuf.length; 77 | 78 | // Local variables 79 | var i, j, k, b, p, row, col, n, cp, 80 | t, lfor, e, x, rsample, rowStartSample, f, da; 81 | 82 | // Clear buffers 83 | for (k = 0; k < numSamples; ++k) { 84 | leftBuf[k] = 0; 85 | rightBuf[k] = 0; 86 | } 87 | 88 | // Generate active notes. 89 | for (i = 0; i < MAX_POLYPHONY; ++i) { 90 | var note = mPlayingNotes[i]; 91 | if (note != undefined) { 92 | 93 | var envelope_shape = note.instr[3]; 94 | var envelope_curve = mInstr[18] / 255.0 95 | 96 | var osc1 = mOscillators[signal_processor.osc_to_waveform(note.instr[0])[0]], 97 | o1vol = 255 - note.instr[1], 98 | o1xenv = note.instr[3], 99 | osc2 = mOscillators[signal_processor.osc_to_waveform(note.instr[0])[1]], 100 | o2vol = 255 - (255 - note.instr[1]), 101 | o2xenv = note.instr[3], 102 | noiseVol = note.instr[13], 103 | attack = Math.round(note.instr[10] * note.instr[10] * 4 * mRateScale), 104 | sustain = Math.round(note.instr[11] * note.instr[11] * 4 * mRateScale), 105 | release = Math.round(note.instr[12] * note.instr[12] * 4 * mRateScale), 106 | releaseInv = 1 / release, 107 | arpInterval = mRowLen * Math.pow(2, 2 - 0); 108 | 109 | // Note frequencies (defined later) and arpeggio 110 | var o1f, o2f; 111 | var arp = note.arp, arpSamples = note.arpSamples; 112 | 113 | // Current oscillator state. 114 | var o1t = note.o1t, o2t = note.o2t; 115 | 116 | // Generate note. 117 | var samplesLeft = attack + sustain + release - note.env; 118 | if (samplesLeft <= numSamples) { 119 | // End of note. 120 | mPlayingNotes[i] = undefined; 121 | } else { 122 | samplesLeft = numSamples; 123 | } 124 | for (j = note.env, k = 0; k < samplesLeft; j++, k++) { 125 | if (arpSamples >= 0 || k == 0) { 126 | if (arpSamples >= 0) { 127 | // Switch arpeggio note 128 | arp = (arp >> 8) | ((arp & 255) << 4); 129 | arpSamples -= arpInterval; 130 | } 131 | 132 | // Calculate note frequencies for the oscillators 133 | o1f = getnotefreq(note.n + (arp & 15) + note.instr[2] - 128); 134 | o2f = getnotefreq(note.n + (arp & 15) + note.instr[2] - 128) * (1 + 0.0008 * note.instr[7]); 135 | } 136 | arpSamples++; 137 | 138 | // Envelope 139 | e = 1; 140 | if (j < attack) { 141 | e = j / attack; 142 | var attack_t = j/attack; 143 | var attack_force = attack_t; 144 | e = e * Math.pow(attack_force,10 * envelope_curve) 145 | } else if (j >= attack + sustain) { 146 | var release_t = (j - attack - sustain) 147 | var release_force = (1 - (release_t/release)); 148 | e = e * Math.pow(release_force,10 * envelope_curve); 149 | } 150 | 151 | // Oscillator 1 152 | t = o1f; 153 | 154 | if(envelope_shape == 1){ t *= e * e; } 155 | else if(envelope_shape == 2){ t *= e * e * e; } 156 | else if(envelope_shape == 3){ t *= e * e * e * e; } 157 | 158 | o1t += t; 159 | rsample = osc1(o1t) * o1vol; 160 | 161 | // Oscillator 2 162 | t = o2f; 163 | 164 | if(envelope_shape == 1){ t *= e * e; } 165 | else if(envelope_shape == 2){ t *= e * e * e; } 166 | else if(envelope_shape == 3){ t *= e * e * e * e; } 167 | 168 | o2t += t; 169 | rsample += osc2(o2t) * o2vol; 170 | 171 | // Noise oscillator 172 | if (noiseVol) { 173 | rsample += (2 * Math.random() - 1) * noiseVol; 174 | } 175 | 176 | // Add to (mono) channel buffer 177 | rightBuf[k] += 0.002441481 * rsample * e; 178 | } 179 | 180 | // Save state. 181 | note.env = j; 182 | note.arp = arp; 183 | note.arpSamples = arpSamples; 184 | note.o1t = o1t; 185 | note.o2t = o2t; 186 | } 187 | } 188 | 189 | // And the effects... 190 | 191 | var pos = mFXState.pos, 192 | low = mFXState.low, 193 | band = mFXState.band, 194 | filterActive = mFXState.filterActive, 195 | dlyPos = mFXState.dlyPos; 196 | 197 | var lsample, high, dlyRead, dlyMask = MAX_DELAY - 1; 198 | 199 | // Put performance critical instrument properties in local variables 200 | var oscLFO = mOscillators[mInstr[15]], 201 | lfoAmt = mInstr[16] / 512, 202 | lfoFreq = Math.pow(2, mInstr[17] - 9) / mRowLen, 203 | fxFilter = mInstr[19], 204 | fxFreq = mInstr[20] * 43.23529 * 3.141592 / mSampleRate, 205 | q = 1 - mInstr[21] / 255, 206 | dlyAmt = mInstr[27] == 0 ? 0 : mInstr[26] / 255, 207 | dly = (signal_processor.delay_conv(mInstr[27]) * mRowLen) >> 1; 208 | 209 | signal_processor.knobs.distortion = mInstr[22] * 1e-5 * 32767; 210 | signal_processor.knobs.pinking = mInstr[28] / 255.0; 211 | signal_processor.knobs.compressor = mInstr[14] / 255.0; 212 | signal_processor.knobs.drive = mInstr[23] / 32.0; 213 | signal_processor.knobs.bit_phaser = 0.5 - (0.49 * (mInstr[9]/255.0)); 214 | signal_processor.knobs.bit_step = 16 - (14 * (mInstr[9]/255.0)); 215 | signal_processor.knobs.pan = mInstr[24] / 255.0; 216 | signal_processor.knobs.delay = mInstr[27] / 255.0; 217 | 218 | // Limit the delay to the delay buffer size. 219 | if (dly >= MAX_DELAY) { 220 | dly = MAX_DELAY - 1; 221 | } 222 | 223 | // Perform effects for this time slice 224 | for (j = 0; j < numSamples; j++) { 225 | k = (pos + j) * 2; 226 | 227 | // Dry mono-sample. 228 | rsample = rightBuf[j]; 229 | 230 | // We only do effects if we have some sound input. 231 | if (rsample || filterActive) { 232 | 233 | // State variable filter. 234 | f = fxFreq; 235 | f *= oscLFO(lfoFreq * k) * lfoAmt + 0.5; 236 | f = 1.5 * Math.sin(f); 237 | 238 | low += f * band; 239 | high = q * (rsample - band) - low; 240 | band += f * high; 241 | rsample = fxFilter == 3 ? band : fxFilter == 1 ? high : low; 242 | 243 | var signal = signal_processor.operate(rsample); 244 | rsample = signal.right; 245 | lsample = signal.left; 246 | 247 | // Is the filter active (i.e. still audiable)? 248 | filterActive = rsample * rsample > 1e-5; 249 | } else { 250 | lsample = 0; 251 | } 252 | 253 | // Delay is always done, since it does not need sound input. 254 | dlyRead = (dlyPos - dly) & dlyMask; 255 | lsample += mDlyRight[dlyRead] * dlyAmt; 256 | rsample += mDlyLeft[dlyRead] * dlyAmt; 257 | mDlyLeft[dlyPos] = lsample; 258 | mDlyRight[dlyPos] = rsample; 259 | dlyPos = (dlyPos + 1) & dlyMask; 260 | 261 | // Store wet stereo sample. 262 | leftBuf[j] = lsample; 263 | rightBuf[j] = rsample; 264 | } 265 | 266 | // Update effect sample position. 267 | pos += numSamples; 268 | 269 | // Prevent rounding problems... 270 | while (pos > mRowLen * 2048) { 271 | pos -= mRowLen * 2048; 272 | } 273 | 274 | // Store filter state. 275 | mFXState.pos = pos; 276 | mFXState.low = low; 277 | mFXState.band = band; 278 | mFXState.filterActive = filterActive; 279 | mFXState.dlyPos = dlyPos; 280 | }; 281 | 282 | //-------------------------------------------------------------------------- 283 | // Public interface. 284 | //-------------------------------------------------------------------------- 285 | 286 | this.start = function () 287 | { 288 | // Create an audio context. 289 | if (window.AudioContext) { 290 | mAudioContext = new AudioContext(); 291 | } else if (window.webkitAudioContext) { 292 | mAudioContext = new webkitAudioContext(); 293 | } else { 294 | mAudioContext = undefined; 295 | return; 296 | } 297 | 298 | // Get actual sample rate (SoundBox is hard-coded to 44100 samples/s). 299 | mSampleRate = mAudioContext.sampleRate; 300 | mRateScale = mSampleRate / 44100; 301 | 302 | // Clear state. 303 | mFXState = { 304 | pos: 0, 305 | low: 0, 306 | band: 0, 307 | filterActive: false, 308 | dlyPos: 0 309 | }; 310 | 311 | // Create delay buffers (lengths must be equal and a power of 2). 312 | mDlyLeft = new Float32Array(MAX_DELAY); 313 | mDlyRight = new Float32Array(MAX_DELAY); 314 | 315 | // Create a script processor node with no inputs and one stereo output. 316 | mScriptNode = mAudioContext.createScriptProcessor(2048, 0, 2); 317 | 318 | mScriptNode.onaudioprocess = function (event){ 319 | var leftBuf = event.outputBuffer.getChannelData(0); 320 | var rightBuf = event.outputBuffer.getChannelData(1); 321 | generateTimeSlice(leftBuf, rightBuf); 322 | }; 323 | 324 | mScriptNode.connect(mAudioContext.destination); 325 | }; 326 | 327 | this.stop = function () 328 | { 329 | 330 | }; 331 | 332 | this.updateInstr = function (instr) 333 | { 334 | mInstr = []; 335 | for (var i = 0; i < instr.length; ++i) { 336 | mInstr.push(instr[i]); 337 | } 338 | }; 339 | 340 | this.updateRowLen = function (rowLen) 341 | { 342 | mRowLen = Math.round(rowLen * mRateScale); 343 | }; 344 | 345 | this.addNote = function (n) 346 | { 347 | var t = (new Date()).getTime(); 348 | 349 | // Create a new note object. 350 | var note = { 351 | startT: t, 352 | env: 0, 353 | arp: 0, 354 | arpSamples: 0, 355 | o1t: 0, 356 | o2t: 0, 357 | n: n, 358 | instr: new Array(15) 359 | }; 360 | 361 | // Copy (snapshot) the oscillator/env/arp part of the current instrument. 362 | for (var i = 0; i < 15; ++i) { 363 | note.instr[i] = mInstr[i]; 364 | } 365 | 366 | // Find an empty channel, or replace the oldest note. 367 | var oldestIdx = 0; 368 | var oldestDt = -100; 369 | for (var i = 0; i < MAX_POLYPHONY; ++i) { 370 | // If the channel is currently free - use it. 371 | if (mPlayingNotes[i] == undefined) { 372 | mPlayingNotes[i] = note; 373 | return; 374 | } 375 | 376 | // Check if this channel has the oldest playing note. 377 | var dt = t - mPlayingNotes[i].startT; 378 | if (dt > oldestDt) { 379 | oldestIdx = i; 380 | oldestDt = dt; 381 | } 382 | } 383 | 384 | // All channels are playing - replace the oldest one. 385 | mPlayingNotes[oldestIdx] = note; 386 | }; 387 | 388 | }; 389 | -------------------------------------------------------------------------------- /desktop/sources/scripts/core/oscillator.js: -------------------------------------------------------------------------------- 1 | var Oscillator = function() 2 | { 3 | function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } 4 | 5 | this.sin = function(value) 6 | { 7 | return Math.sin(value * 2 * Math.PI); 8 | }; 9 | 10 | this.abs = function(value) 11 | { 12 | return 0.5 + Math.sin(value * 2 * Math.PI); 13 | } 14 | 15 | this.pulse = function (value) 16 | { 17 | return clamp(0.05/(Math.sin(2 * Math.PI * value) * Math.tan(2 * Math.PI * value)),-1.0,1.0); 18 | } 19 | 20 | this.saw = function(value) 21 | { 22 | return 2 * (value % 1) - 1; 23 | } 24 | 25 | this.rev = function(value) 26 | { 27 | return 1 - (2 * (value % 1)); 28 | } 29 | 30 | this.square = function(value) 31 | { 32 | return (value % 1) < 0.5 ? 1 : -1; 33 | } 34 | 35 | this.noise = function(value) 36 | { 37 | return (2 * Math.random() - 1); 38 | } 39 | 40 | this.tri = function(value) 41 | { 42 | var v2 = (value % 1) * 4; 43 | return v2 < 2 ? v2 - 1 : 3 - v2; 44 | } 45 | 46 | // generates waveform from custom text 47 | // e.g.: 'sin(x)' 48 | this.custom = function(value, expression) 49 | { 50 | var ret = eval('var x=value;' + expression); 51 | ret = ret > 1.0 ? 1.0 : ret; 52 | ret = ret < -1.0 ? -1.0 : ret; 53 | return ret; 54 | } 55 | 56 | // TODO: complete sin2 - clear idea what I want, but... maths. 57 | // this.osc_sin2 = function (value) { 58 | // return Math.sin(2 * Math.PI * value) * Math.sin(4 * Math.PI * value); 59 | // } 60 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/core/player-worker.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: javascript; tab-width: 2; indent-tabs-mode: nil; -*- 2 | * 3 | * Copyright (c) 2011-2013 Marcus Geelnard 4 | * 5 | * This file is part of SoundBox. 6 | * 7 | * SoundBox is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * SoundBox is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with SoundBox. If not, see . 19 | * 20 | */ 21 | 22 | self.importScripts('signal.js'); 23 | self.importScripts('oscillator.js'); 24 | 25 | 26 | var CPlayerWorker = function() 27 | { 28 | var signal_processor = new Signal_Processor(); 29 | 30 | var getnotefreq = function (n) 31 | { 32 | // 174.61.. / 44100 = 0.003959503758 (F) 33 | return 0.003959503758 * Math.pow(2, (n-128)/12); 34 | }; 35 | 36 | var createNote = function (instr, n, rowLen) 37 | { 38 | var envelope_shape = instr.i[3]; 39 | var envelope_curve = instr.i[18] / 255.0; 40 | 41 | var osc1 = mOscillators[signal_processor.osc_to_waveform(instr.i[0])[0]], 42 | o1vol = 255 - instr.i[1], 43 | o1xenv = instr.i[3], 44 | osc2 = mOscillators[signal_processor.osc_to_waveform(instr.i[0])[1]], 45 | o2vol = 255 - (255 - instr.i[1]), 46 | o2xenv = instr.i[3], 47 | noiseVol = instr.i[13], 48 | attack = instr.i[10] * instr.i[10] * 4, 49 | sustain = instr.i[11] * instr.i[11] * 4, 50 | release = instr.i[12] * instr.i[12] * 4, 51 | releaseInv = 1 / release, 52 | arp = 0, 53 | arpInterval = rowLen * Math.pow(2, 2 - 0); 54 | 55 | var noteBuf = new Int32Array(attack + sustain + release); 56 | 57 | // Re-trig oscillators 58 | var c1 = 0, c2 = 0; 59 | 60 | // Local variables. 61 | var j, j2, e, t, rsample, o1t, o2t; 62 | 63 | // Generate one note (attack + sustain + release) 64 | for (j = 0, j2 = 0; j < attack + sustain + release; j++, j2++) { 65 | if (j2 >= 0) { 66 | // Switch arpeggio note. 67 | arp = (arp >> 8) | ((arp & 255) << 4); 68 | j2 -= arpInterval; 69 | 70 | // Calculate note frequencies for the oscillators 71 | o1t = getnotefreq(n + (arp & 15) + instr.i[2] - 128); 72 | o2t = getnotefreq(n + (arp & 15) + instr.i[2] - 128) * (1 + 0.0008 * instr.i[7]); 73 | } 74 | 75 | // Envelope 76 | e = 1; 77 | if (j < attack) { 78 | e = j / attack; 79 | var attack_t = j/attack; 80 | var attack_force = attack_t; 81 | e = e * Math.pow(attack_force,10 * envelope_curve) 82 | } else if (j >= attack + sustain) { 83 | var release_t = (j - attack - sustain) 84 | var release_force = (1 - (release_t/release)); 85 | e = e * Math.pow(release_force,10 * envelope_curve); 86 | } 87 | 88 | // Oscillator 1 89 | t = o1t; 90 | if(envelope_shape == 1){ t *= e * e; } 91 | else if(envelope_shape == 2){ t *= e * e * e; } 92 | else if(envelope_shape == 3){ t *= e * e * e * e; } 93 | c1 += t; 94 | rsample = osc1(c1) * o1vol; 95 | 96 | // Oscillator 2 97 | t = o2t; 98 | if(envelope_shape == 1){ t *= e * e; } 99 | else if(envelope_shape == 2){ t *= e * e * e; } 100 | else if(envelope_shape == 3){ t *= e * e * e * e; } 101 | c2 += t; 102 | rsample += osc2(c2) * o2vol; 103 | 104 | // Noise oscillator 105 | if(noiseVol){ 106 | rsample += (2 * Math.random() - 1) * noiseVol; 107 | } 108 | 109 | // Add to (mono) channel buffer 110 | noteBuf[j] = (80 * rsample * e) | 0; 111 | } 112 | 113 | return noteBuf; 114 | }; 115 | 116 | 117 | //-------------------------------------------------------------------------- 118 | // Private members 119 | //-------------------------------------------------------------------------- 120 | 121 | // Array of oscillator functions 122 | var mOscillators = 123 | [ 124 | new Oscillator().sin, 125 | new Oscillator().square, 126 | new Oscillator().saw, 127 | new Oscillator().tri, 128 | new Oscillator().noise, 129 | new Oscillator().rev, 130 | new Oscillator().pulse 131 | ]; 132 | 133 | //---------------------------------------------------------------------------- 134 | // Public methods 135 | //---------------------------------------------------------------------------- 136 | 137 | // Initialize buffers etc. 138 | this.init = function (song, opts) 139 | { 140 | // Handle optional arguments 141 | this.firstRow = 0; 142 | this.lastRow = song.endPattern - 1; 143 | this.firstCol = 0; 144 | this.lastCol = 15; 145 | 146 | if(opts) { 147 | this.firstRow = opts.firstRow; 148 | this.lastRow = opts.lastRow; 149 | this.firstCol = opts.firstCol; 150 | this.lastCol = opts.lastCol; 151 | } 152 | 153 | // Prepare song info 154 | this.song = song; 155 | this.numSamples = song.rowLen * song.patternLen * (this.lastRow - this.firstRow + 1); 156 | this.numWords = this.numSamples * 2; 157 | 158 | // Create work buffers (initially cleared) 159 | this.mixBufWork = new Int32Array(this.numWords); 160 | }; 161 | 162 | // Generate audio data for a single track 163 | this.generate = function () 164 | { 165 | 166 | // Local variables 167 | var i, j, b, p, row, col, currentCol, n, cp, 168 | k, t, lfor, e, x, rsample, rowStartSample, f, da; 169 | 170 | for (currentCol = this.firstCol; currentCol <= this.lastCol; currentCol++) { 171 | // Put performance critical items in local variables 172 | var chnBuf = new Int32Array(this.numWords), 173 | mixBuf = this.mixBufWork, 174 | waveSamples = this.numSamples, 175 | waveWords = this.numWords, 176 | instr = this.song.songData[currentCol], 177 | rowLen = this.song.rowLen, 178 | patternLen = this.song.patternLen; 179 | 180 | // Clear effect state 181 | var low = 0, band = 0, high; 182 | var lsample, filterActive = false; 183 | 184 | var mFXState = { 185 | bit_last: 0, 186 | bit_phaser: 0 187 | }; 188 | 189 | // Clear note cache. 190 | var noteCache = []; 191 | 192 | // Patterns 193 | for (p = this.firstRow; p <= this.lastRow; ++p) { 194 | cp = instr.p[p]; 195 | 196 | // Pattern rows 197 | for (row = 0; row < patternLen; ++row) { 198 | // Execute effect command. 199 | var cmdNo = cp ? instr.c[cp - 1].f[row] : 0; 200 | if (cmdNo) { 201 | instr.i[cmdNo - 1] = instr.c[cp - 1].f[row + patternLen] || 0; 202 | 203 | // Clear the note cache since the instrument has changed. 204 | if (cmdNo < 15) { 205 | noteCache = []; 206 | } 207 | } 208 | 209 | // Put performance critical instrument properties in local variables 210 | var oscLFO = mOscillators[instr.i[15]], 211 | lfoAmt = instr.i[16] / 512, 212 | lfoFreq = Math.pow(2, instr.i[17] - 9) / rowLen, 213 | fxFilter = instr.i[19], 214 | fxFreq = instr.i[20] * 43.23529 * 3.141592 / 44100, 215 | q = 1 - instr.i[21] / 255, 216 | dlyAmt = instr.i[27] == 0 ? 0 : instr.i[26] / 255, 217 | dly = signal_processor.delay_conv(instr.i[27]) * rowLen; 218 | 219 | signal_processor.knobs.distortion = instr.i[22] * 1e-5; 220 | signal_processor.knobs.pinking = instr.i[28] / 255.0; 221 | signal_processor.knobs.compressor = instr.i[14] / 255.0; 222 | signal_processor.knobs.drive = instr.i[23] / 32.0; 223 | signal_processor.knobs.bit_phaser = 0.5 - (0.49 * (instr.i[9]/255.0)); 224 | signal_processor.knobs.bit_step = 16 - (14 * (instr.i[9]/255.0)); 225 | signal_processor.knobs.pan = instr.i[24] / 255.0; 226 | 227 | // Calculate start sample number for this row in the pattern 228 | rowStartSample = ((p - this.firstRow) * patternLen + row) * rowLen; 229 | 230 | // Generate notes for this pattern row 231 | for (col = 0; col < 4; ++col) { 232 | n = cp ? instr.c[cp - 1].n[row + col * patternLen] : 0; 233 | if (n) { 234 | if (!noteCache[n]) { 235 | noteCache[n] = createNote(instr, n, rowLen); 236 | } 237 | 238 | // Copy note from the note cache 239 | var noteBuf = noteCache[n]; 240 | for (j = 0, i = rowStartSample * 2; j < noteBuf.length; j++, i += 2) { 241 | chnBuf[i] += noteBuf[j]; 242 | } 243 | } 244 | } 245 | 246 | // Perform effects for this pattern row 247 | for (j = 0; j < rowLen; j++) { 248 | // Dry mono-sample 249 | k = (rowStartSample + j) * 2; 250 | rsample = chnBuf[k]; 251 | 252 | // We only do effects if we have some sound input 253 | if (rsample || filterActive) { 254 | 255 | // State variable filter 256 | f = fxFreq; 257 | f *= oscLFO(lfoFreq * k) * lfoAmt + 0.5; 258 | f = 1.5 * Math.sin(f); 259 | low += f * band; 260 | high = q * (rsample - band) - low; 261 | band += f * high; 262 | rsample = fxFilter == 3 ? band : fxFilter == 1 ? high : low; 263 | 264 | var signal = signal_processor.operate(rsample,true); 265 | rsample = signal.right; 266 | lsample = signal.left; 267 | 268 | // Is the filter active (i.e. still audiable)? 269 | filterActive = rsample * rsample > 1e-5; 270 | } else { 271 | lsample = 0; 272 | } 273 | 274 | // Delay is always done, since it does not need sound input 275 | if (k >= dly) { 276 | lsample += chnBuf[k-dly+1] * dlyAmt; 277 | rsample += chnBuf[k-dly] * dlyAmt; 278 | } 279 | 280 | // Store in stereo channel buffer (needed for the delay effect) 281 | chnBuf[k] = lsample | 0; 282 | chnBuf[k+1] = rsample | 0; 283 | 284 | // ...and add to stereo mix buffer 285 | mixBuf[k] += lsample | 0; 286 | mixBuf[k+1] += rsample | 0; 287 | } 288 | } 289 | 290 | // Post progress to the main thread... 291 | var progress = (currentCol - this.firstCol + (p - this.firstRow) / 292 | (this.lastRow - this.firstRow + 1)) / 293 | (this.lastCol - this.firstCol + 1); 294 | postMessage({ 295 | cmd: "progress", 296 | progress: progress, 297 | buffer: null 298 | }); 299 | } 300 | } 301 | }; 302 | 303 | // Get the final buffer (as generated by the generate() method). 304 | this.getBuf = function () { 305 | // We no longer need the channel working buffer 306 | this.chnBufWork = null; 307 | 308 | return this.mixBufWork; 309 | }; 310 | }; 311 | 312 | var gPlayerWorker = new CPlayerWorker(); 313 | 314 | onmessage = function (event) { 315 | if (event.data.cmd === "generate") { 316 | // Generate the sound data. 317 | gPlayerWorker.init(event.data.song, event.data.opts); 318 | gPlayerWorker.generate(); 319 | 320 | // Signal that we are done, and send the resulting buffer over to the main 321 | // thread. 322 | postMessage({ 323 | cmd: "progress", 324 | progress: 1, 325 | buffer: gPlayerWorker.getBuf() 326 | }); 327 | } 328 | }; -------------------------------------------------------------------------------- /desktop/sources/scripts/core/player.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: javascript; tab-width: 2; indent-tabs-mode: nil; -*- 2 | * 3 | * Copyright (c) 2011-2013 Marcus Geelnard 4 | * 5 | * This file is part of SoundBox. 6 | * 7 | * SoundBox is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * SoundBox is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with SoundBox. If not, see . 19 | * 20 | */ 21 | 22 | "use strict"; 23 | 24 | var CPlayer = function () 25 | { 26 | var mProgressCallback; 27 | 28 | var mGeneratedBuffer; 29 | 30 | var mWorker = new Worker("scripts/core/player-worker.js"); 31 | 32 | mWorker.onmessage = function (event) { 33 | if (event.data.cmd === "progress") { 34 | mGeneratedBuffer = event.data.buffer; 35 | if (mProgressCallback) { 36 | mProgressCallback(event.data.progress); 37 | } 38 | } 39 | }; 40 | 41 | 42 | //-------------------------------------------------------------------------- 43 | // Public methods 44 | //-------------------------------------------------------------------------- 45 | 46 | // Generate the audio data (done in worker). 47 | this.generate = function(song, opts, progressCallback) 48 | { 49 | mProgressCallback = progressCallback; 50 | mWorker.postMessage({ 51 | cmd: "generate", 52 | song: song, 53 | opts: opts 54 | }); 55 | }; 56 | 57 | // Create a WAVE formatted Uint8Array from the generated audio data. 58 | this.createWave = function() 59 | { 60 | // Turn critical object properties into local variables (performance) 61 | var mixBuf = mGeneratedBuffer, 62 | waveWords = mixBuf.length; 63 | 64 | // Create WAVE header 65 | var headerLen = 44; 66 | var l1 = headerLen + waveWords * 2 - 8; 67 | var l2 = l1 - 36; 68 | var wave = new Uint8Array(headerLen + waveWords * 2); 69 | wave.set( 70 | [82,73,70,70, 71 | l1 & 255,(l1 >> 8) & 255,(l1 >> 16) & 255,(l1 >> 24) & 255, 72 | 87,65,86,69,102,109,116,32,16,0,0,0,1,0,2,0, 73 | 68,172,0,0,16,177,2,0,4,0,16,0,100,97,116,97, 74 | l2 & 255,(l2 >> 8) & 255,(l2 >> 16) & 255,(l2 >> 24) & 255] 75 | ); 76 | 77 | // Append actual wave data 78 | for(var i = 0, idx = headerLen; i < waveWords; ++i){ 79 | // Note: We clamp here 80 | var y = mixBuf[i]; 81 | y = y < -32767 ? -32767 : (y > 32767 ? 32767 : y); 82 | wave[idx++] = y & 255; 83 | wave[idx++] = (y >> 8) & 255; 84 | } 85 | 86 | // Return the WAVE formatted typed array 87 | return wave; 88 | }; 89 | 90 | // Get n samples of wave data at time t [s]. Wave data in range [-2,2]. 91 | this.getData = function(t, n) 92 | { 93 | var i = 2 * Math.floor(t * 44100); 94 | var d = new Array(n); 95 | var b = mGeneratedBuffer; 96 | for (var j = 0; j < 2*n; j += 1) { 97 | var k = i + j; 98 | d[j] = t > 0 && k < b.length ? b[k] / 32768 : 0; 99 | } 100 | return d; 101 | }; 102 | }; 103 | -------------------------------------------------------------------------------- /desktop/sources/scripts/core/signal.js: -------------------------------------------------------------------------------- 1 | function Signal_Processor() 2 | { 3 | this.knobs = {distortion:null,pinking:null,compressor:null,drive:null,bit_phaser:null,bit_step:null,pan:null}; 4 | 5 | this.step_last = 0; 6 | this.phase = 0; 7 | this.is_export = false; 8 | 9 | this.operate = function(input,is_export = false) 10 | { 11 | var output = input; 12 | 13 | this.is_export = is_export; 14 | 15 | output = this.effect_bitcrusher(output); 16 | 17 | output = this.effect_distortion(output,this.knobs.distortion); 18 | output = this.effect_pinking(output,this.knobs.pinking); 19 | output = this.effect_compressor(output,this.knobs.compressor); 20 | output = this.effect_drive(output,this.knobs.drive); 21 | 22 | // Pan 23 | var left = output * (1 - this.knobs.pan); 24 | var right = output * (this.knobs.pan); 25 | 26 | return {left:left,right:right}; 27 | } 28 | 29 | this.effect_bitcrusher = function(input) 30 | { 31 | var output = input; 32 | 33 | this.phase += this.knobs.bit_phaser; // Between 0.1 and 1 34 | var step = Math.pow(1/2, this.knobs.bit_step); // between 1 and 16 35 | 36 | if(this.phase >= 1.0) { 37 | this.phase -= 1.0; 38 | this.step_last = step * Math.floor(output / step + 0.5); 39 | } 40 | 41 | output = this.knobs.bit_step < 16 ? this.step_last : output; 42 | 43 | return output; 44 | } 45 | 46 | var b0, b1, b2, b3, b4, b5, b6; b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0; 47 | 48 | this.effect_pinking = function(input,val) 49 | { 50 | b0 = 0.99886 * b0 + input * 0.0555179; 51 | b1 = 0.99332 * b1 + input * 0.0750759; 52 | b2 = 0.96900 * b2 + input * 0.1538520; 53 | b3 = 0.86650 * b3 + input * 0.3104856; 54 | b4 = 0.55000 * b4 + input * 0.5329522; 55 | b5 = -0.7616 * b5 - input * 0.0168980; 56 | var output = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + input * 0.5362) * 0.1; 57 | b6 = input * 0.115926; 58 | 59 | return (output * val) + (input * (1 - val)); 60 | } 61 | 62 | this.effect_compressor = function(input,val) 63 | { 64 | var output = this.is_export ? input/44100 : input; 65 | var offset = 1 - Math.abs(output); 66 | 67 | output = output * offset * offset; 68 | 69 | if(this.is_export){ 70 | output *= 44100 71 | } 72 | 73 | return (output * val) + (input * (1 - val)); 74 | } 75 | 76 | this.effect_distortion = function(input,val) 77 | { 78 | if(!val){ return input; } 79 | 80 | var output = input; 81 | output *= val; 82 | output = output < 1 ? output > -1 ? new Oscillator().sin(output*.25) : -1 : 1; 83 | output /= val; 84 | return output; 85 | } 86 | 87 | this.effect_drive = function(input,val) 88 | { 89 | var output = input; 90 | return output * val; 91 | } 92 | 93 | // Tools 94 | 95 | this.delay_conv = function(val) 96 | { 97 | return [0,32,24,16,12,8,6,4][val]; 98 | } 99 | 100 | this.osc_to_waveform = function(index) 101 | { 102 | if(index == 0 ){ return [0,0]; } // SIN 103 | if(index == 1 ){ return [0,1]; } // SINSQR 104 | if(index == 2 ){ return [0,2]; } // SINSAW 105 | if(index == 3 ){ return [0,3]; } // SINTRI 106 | if(index == 4 ){ return [1,1]; } // SQR 107 | if(index == 5 ){ return [1,0]; } // SQRSIN 108 | if(index == 6 ){ return [1,2]; } // SQRSAW 109 | if(index == 7 ){ return [1,3]; } // SQRTRI 110 | if(index == 8 ){ return [2,2]; } // SAW 111 | if(index == 9 ){ return [2,0]; } // SAWSIN 112 | if(index == 10){ return [2,1]; } // SAWSQR 113 | if(index == 11){ return [2,3]; } // SAWTRI 114 | if(index == 12){ return [3,3]; } // TRI 115 | if(index == 13){ return [3,0]; } // TRISIN 116 | if(index == 14){ return [3,1]; } // TRISQR 117 | if(index == 15){ return [3,2]; } // TRISAW 118 | if(index == 16){ return [4,4]; } // NOISE 119 | if(index == 17){ return [6,0]; } // PULSE 120 | if(index == 18){ return [6,2]; } // REVSAW 121 | } 122 | 123 | function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } 124 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/editor.js: -------------------------------------------------------------------------------- 1 | function Editor(t,b) 2 | { 3 | var target = this; 4 | 5 | this.edit_mode = false; 6 | this.pattern = {id:0,beat:4,length:(t*b),signature:[t,b],effect:-1}; 7 | this.composer = false; 8 | 9 | this.start = function() 10 | { 11 | console.log("Started Editor"); 12 | 13 | // Table 14 | var table = document.getElementById("pattern-table"); 15 | var tr, td; 16 | for(var r = 0; r < 32; r++) { 17 | tr = document.createElement("tr"); 18 | tr.id = "ppr"+r; 19 | // Notes 20 | for (i = 0; i < marabu.channels; i++) { 21 | td = document.createElement("td"); 22 | td.id = "i"+(i < 10 ? "0"+i : i)+"r"+r; 23 | td.textContent = "----"; 24 | td.addEventListener("mousedown", this.pattern_mouse_down, false); 25 | tr.appendChild(td); 26 | } 27 | // Effects 28 | var th = document.createElement("th"); 29 | th.id = "fxr" + r; 30 | th.textContent = "0000"; 31 | th.addEventListener("mousedown", this.effect_mouse_down, false); 32 | tr.appendChild(th); 33 | // End 34 | table.appendChild(tr); 35 | } 36 | 37 | // Composer 38 | 39 | var notes = ['C', 'c', 'D', 'd', 'E', 'F', 'f', 'G', 'g', 'A', 'a', 'B']; 40 | 41 | var table = document.getElementById("composer-table"); 42 | var tr, td, th; 43 | for(var r = 0; r < 15; r++) { 44 | tr = document.createElement("tr"); 45 | // Notes 46 | for (i = 0; i < 32; i++) { 47 | td = document.createElement("td"); 48 | td.textContent = i % 4 == 0 ? ">-" : "--" 49 | td.className = "fl" 50 | td.id = "cpm_"+i+"_"+r; 51 | td.addEventListener("mousedown", this.composer_mouse_down, false); 52 | tr.appendChild(td); 53 | if(i == 31){ 54 | td.textContent = notes[r % notes.length]+""+(parseInt(r /12) + 4); 55 | } 56 | } 57 | // End 58 | table.appendChild(tr); 59 | } 60 | } 61 | 62 | this.build = function() 63 | { 64 | return "
"; 65 | } 66 | 67 | this.pattern_mouse_down = function(e) 68 | { 69 | var i = parseInt(e.target.id.slice(1,3)); 70 | var r = parseInt(e.target.id.slice(4)); 71 | 72 | marabu.selection.instrument = i; 73 | marabu.selection.row = r; 74 | marabu.sequencer.follower.stop(); 75 | marabu.update(); 76 | } 77 | 78 | this.effect_mouse_down = function(e) 79 | { 80 | var row = parseInt(e.target.id.slice(3,5)); 81 | marabu.selection.row = row; 82 | marabu.sequencer.follower.stop(); 83 | marabu.update(); 84 | } 85 | 86 | this.composer_mouse_down = function(e) 87 | { 88 | var row = parseInt(e.target.id.split("_")[1]); 89 | var note = parseInt(e.target.id.split("_")[2]); 90 | var note_val = 24 - note - 11; 91 | 92 | marabu.selection.row = row; 93 | marabu.sequencer.follower.stop(); 94 | marabu.play_note(note_val,e.shiftKey); 95 | marabu.update(); 96 | } 97 | 98 | this.toggle_composer = function() 99 | { 100 | marabu.editor.composer = marabu.editor.composer ? false : true; 101 | 102 | this.editor_el = document.getElementById("editor"); 103 | this.editor_el.className = marabu.editor.composer ? "composer" : ""; 104 | } 105 | 106 | // Parser 107 | 108 | this.cell_data = function(i,t,r,pattern = null, note = null) 109 | { 110 | // Basics 111 | var left_val = marabu.song.note_at(i,t,r); 112 | var right_val = marabu.song.note_at(i,t,r+32); 113 | var values = {left:left_val,right:right_val}; 114 | 115 | var left_note = left_val > 0 ? parse_note(left_val) : null; 116 | var right_note = right_val > 0 ? parse_note(right_val) : null; 117 | var notes = {left:left_note,right:right_note}; 118 | 119 | var cmd = marabu.song.effect_at(i,t,r); 120 | var val = marabu.song.effect_at(i,t,r+32); 121 | var effect = {cmd:cmd,val:val} 122 | 123 | // Strings 124 | var base_str = r == 1 && marabu.song.instrument(i).name ? marabu.song.instrument(i).name : "----" 125 | var left_str = effect.cmd ? to_hex(effect.cmd,2) : (r % 4 == 0 ? ">-" : base_str.substr(0,2)); 126 | var right_str = effect.cmd ? to_hex(effect.val,2) : base_str.substr(2,2); 127 | left_str = notes.left ? ((notes.left.sharp ? notes.left.note.toLowerCase() : notes.left.note)+""+notes.left.octave) : left_str; 128 | right_str = notes.right ? ((notes.right.sharp ? notes.right.note.toLowerCase() : notes.right.note)+""+notes.right.octave) : right_str; 129 | var strings = {left:left_str,right:right_str,any:left_str}; 130 | 131 | // Class 132 | var classes = {fg:"fl",bg:""}; 133 | if(marabu.cheatcode.is_active && i == marabu.selection.instrument && marabu.cheatcode.selection[r] || effect.cmd){ classes.bg = "b_inv f_inv"; } 134 | else if(marabu.arp.is_active && marabu.arp.is_recording && i == marabu.selection.instrument && r >= marabu.selection.row && r <= marabu.selection.row+marabu.arp.memory.length){ classes.bg = "b_inv"; } 135 | else if(marabu.arp.is_active && !marabu.arp.is_recording && i == marabu.selection.instrument && r == marabu.selection.row){ classes.bg = "b_inv f_inv"; } 136 | 137 | if(marabu.cheatcode.is_active && i == marabu.selection.instrument && marabu.cheatcode.selection[r] || effect.cmd){ classes.fg = "f_inv"; } 138 | else if(marabu.arp.is_active && marabu.arp.is_recording && i == marabu.selection.instrument && r >= marabu.selection.row && r <= marabu.selection.row+marabu.arp.memory.length){ classes.fg = "f_inv"; } 139 | else if(marabu.arp.is_active && !marabu.arp.is_recording && i == marabu.selection.instrument && r == marabu.selection.row){ classes.fg = "b_inv f_inv"; } 140 | else if(values.left || values.right){ classes.fg = "fm"; } 141 | else if(pattern > 0 && r % 4 == 0){ classes.fg = "fm";} 142 | 143 | // Highlights 144 | if(r == marabu.selection.row && i == marabu.selection.instrument){ classes.fg = "selected";} 145 | 146 | // Compositor 147 | if(note){ 148 | if(values.left == note || values.right == note){ 149 | strings.any = values.left == note ? strings.left : strings.right 150 | classes.fg = r == marabu.selection.row ? "fh" : "fm" 151 | } 152 | else{ 153 | strings.any = r % 4 == 0 ? ">-" : "--"; 154 | classes.fg = r % 4 == 0 ? "beat" : "" 155 | } 156 | } 157 | 158 | return {notes:notes,effect:effect,values:values,strings:strings,classes:classes}; 159 | } 160 | 161 | // Update 162 | 163 | this.update = function() 164 | { 165 | // Editor 166 | for(var r = 0; r < 32; r++){ 167 | var row = document.getElementById("ppr"+r); 168 | row.className = marabu.selection.row == r ? "selected" : "" 169 | } 170 | for(var i = 0; i < marabu.channels; i++){ 171 | var pattern = marabu.song.pattern_at(i,marabu.selection.track); 172 | for(var r = 0; r < 32; r++){ 173 | var cell = document.getElementById("i"+(i < 10 ? '0'+i : i)+"r"+r); 174 | var c = marabu.editor.cell_data(i,marabu.selection.track,r,pattern); 175 | cell.className = c.classes.fg+" "+c.classes.bg; 176 | cell.textContent = c.strings.left+c.strings.right; 177 | } 178 | } 179 | 180 | // Composer 181 | for(var r = 0; r < 32; r++){ 182 | for(var n = 0; n < 15; n++){ 183 | var cell = document.getElementById("cpm_"+r+"_"+n); 184 | var offset = (marabu.selection.octave * 12); 185 | var note = -(n - offset - 87) + 13; 186 | var c = marabu.editor.cell_data(marabu.selection.instrument,marabu.selection.track,r,pattern,note); 187 | cell.textContent = c.strings.any; 188 | cell.className = c.classes.fg+" "+c.classes.bg; 189 | } 190 | } 191 | 192 | // Effects 193 | for(var r = 0; r < 32; r++){ 194 | var cell = document.getElementById("fxr"+r); 195 | var effect_cmd = marabu.song.effect_at(marabu.selection.instrument,marabu.selection.track,r); 196 | var effect_val = marabu.song.effect_at(marabu.selection.instrument,marabu.selection.track,r+32) 197 | cell.textContent = effect_cmd > 0 ? (to_hex(effect_cmd,2) + "" + to_hex(effect_val,2)) : "0000"; 198 | cell.className = effect_cmd > 0 ? "fh" : (r % 4 == 0 ? "fm" : "fl"); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /desktop/sources/scripts/follower.js: -------------------------------------------------------------------------------- 1 | function Follower() 2 | { 3 | this.timer = -1; 4 | this.prev = -1; 5 | this.offset = 0; 6 | 7 | this.start = function(offset = 0) 8 | { 9 | this.timer = setInterval(this.update, 16); 10 | this.offset = offset; 11 | console.log("follower","start -> "+this.offset); 12 | } 13 | 14 | this.update = function() 15 | { 16 | var t = marabu.song.mAudio_timer().currentTime(); 17 | 18 | if (marabu.song.mAudio().ended || (marabu.song.mAudio().duration && ((marabu.song.mAudio().duration - t) < 0.001))) { 19 | clearInterval(this.timer); 20 | this.timer = -1; 21 | marabu.instrument.controls.uv.monitor.draw(-1); 22 | return; 23 | } 24 | 25 | marabu.instrument.controls.uv.monitor.draw(t); 26 | 27 | var n = Math.floor(t * 44100 / marabu.song.song().rowLen); 28 | var r = n % 32; 29 | 30 | if(n != this.prev){ 31 | marabu.selection.row = r; 32 | marabu.selection.track = parseInt(n/32) + marabu.sequencer.follower.offset; 33 | marabu.update(); 34 | this.prev = n; 35 | } 36 | } 37 | 38 | this.stop = function() 39 | { 40 | if(this.timer == -1){ return; } 41 | console.log("follower","stop"); 42 | clearInterval(this.timer); 43 | this.timer = -1; 44 | marabu.update(); 45 | marabu.instrument.controls.uv.monitor.draw(-1); 46 | } 47 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/instrument.js: -------------------------------------------------------------------------------- 1 | function Instrument() 2 | { 3 | var target = this; 4 | 5 | this.el = null; 6 | 7 | this.start = function() 8 | { 9 | this.el = document.getElementById("instrument"); 10 | 11 | this.controls = { 12 | envelope : { 13 | type : new UI_Choice({name: "ENV", choices: ["NONE","WEAK","AVRG","HARD"] }), 14 | attack : new UI_Slider({name: "ATK", min: 0, max: 255 }), 15 | sustain : new UI_Slider({name: "SUS", min: 0, max: 255 }), 16 | release : new UI_Slider({name: "REL", min: 0, max: 255 }), 17 | curve : new UI_Slider({name: "POW", min: 12, max: 255 }) 18 | }, 19 | osc: { 20 | shape : new UI_Choice({name: "OSC", choices: ["SIN","SINSQR","SINSAW","SINTRI","SQR","SQRSIN","SQRSAW","SQRTRI","SAW","SAWSIN","SAWSQR","SAWTRI","TRI","TRISIN","TRISQR","TRISAW","NOISE","PULSE","REVSAW"]}), 21 | frequency : new UI_Slider({name: "FRQ", min: 92, max: 164 }), 22 | mix : new UI_Slider({name: "MIX", min: 0, max: 255, center:true }), 23 | detune : new UI_Slider({name: "DET", min: 0, max: 255 }) 24 | }, 25 | lfo : { 26 | shape : new UI_Choice({name: "LFO", choices: ["SIN","SQR","SAW","TRI","NOISE","REVSAW","PULSE"] }), 27 | frequency : new UI_Slider({name: "FRQ", min: 2, max: 12 }), 28 | amount : new UI_Slider({name: "AMT", min: 0, max: 255 }) 29 | }, 30 | filter : { 31 | shape : new UI_Choice({name: "FLT", choices: ["LP","HP","LP","BP"] }), 32 | frequency : new UI_Slider({name: "FRQ", min: 0, max: 255 }), 33 | resonance : new UI_Slider({name: "RES", min: 0, max: 254 }) 34 | }, 35 | delay : { 36 | rate : new UI_Choice({name: "DLY", choices: ["OFF","1/2","1/3","1/4","1/6","1/8","1/12","1/16"] }), 37 | amount : new UI_Slider({name: "AMT", min: 0, max: 255 }) 38 | }, 39 | effect : { 40 | noise : new UI_Slider({name: "NOI", min: 0, max: 255 }), 41 | bit : new UI_Slider({name: "BIT", min: 0, max: 255 }), 42 | distortion : new UI_Slider({name: "DIS", min: 0, max: 64 }), 43 | pinking : new UI_Slider({name: "PIN", min: 0, max: 255 }), 44 | compressor : new UI_Slider({name: "CMP", min: 0, max: 255 }), 45 | drive : new UI_Slider({name: "DRV", min: 0, max: 255 }), 46 | pan : new UI_Slider({name: "PAN", min: 0, max: 255, center:true }) 47 | }, 48 | uv : { 49 | monitor : new UI_Uv() 50 | } 51 | }; 52 | 53 | this.install(); 54 | } 55 | 56 | this.install = function() 57 | { 58 | var selection_id = 0; 59 | for(family_id in this.controls){ 60 | var family_el = document.createElement("div"); 61 | family_el.id = family_id; 62 | family_el.className = "family"; 63 | for(control_id in this.controls[family_id]){ 64 | var ctrl = this.controls[family_id][control_id]; 65 | ctrl.family = family_id; 66 | ctrl.id = control_id; 67 | ctrl.control = selection_id; 68 | ctrl.install(family_el); 69 | selection_id += 1; 70 | } 71 | this.el.appendChild(family_el); 72 | } 73 | } 74 | 75 | this.control_target = function(target_control) 76 | { 77 | for(family_id in this.controls){ 78 | for(control_id in this.controls[family_id]){ 79 | if(this.controls[family_id][control_id].control == target_control){ return this.controls[family_id][control_id]; } 80 | } 81 | } 82 | } 83 | 84 | this.update = function() 85 | { 86 | for(family_id in this.controls){ 87 | for(control_id in this.controls[family_id]){ 88 | var ctrl = this.controls[family_id][control_id]; 89 | var ctrl_storage = this.get_storage(family_id+"_"+control_id); 90 | var value = marabu.song.control_at(marabu.selection.instrument,ctrl_storage); 91 | ctrl.override(value); 92 | } 93 | } 94 | marabu.song.mJammer_update(); 95 | } 96 | 97 | this.get_storage = function(id) 98 | { 99 | // Env 100 | switch (id){ 101 | case 'envelope_type' : return 3 102 | case 'envelope_attack' : return 10 103 | case 'envelope_sustain' : return 11 104 | case 'envelope_release' : return 12 105 | case 'envelope_curve' : return 18 106 | 107 | case 'osc_shape' : return 0 108 | case 'osc_frequency' : return 2 109 | case 'osc_mix' : return 1 110 | case 'osc_detune' : return 7 111 | 112 | case 'lfo_shape' : return 15 113 | case 'lfo_frequency' : return 17 114 | case 'lfo_amount' : return 16 115 | 116 | case 'filter_shape' : return 19 117 | case 'filter_frequency' : return 20 118 | case 'filter_resonance' : return 21 119 | 120 | case 'delay_rate' : return 27 121 | case 'delay_amount' : return 26 122 | 123 | case 'effect_noise' : return 13 124 | case 'effect_bit' : return 9 125 | case 'effect_distortion' : return 22 126 | case 'effect_pinking' : return 28 127 | case 'effect_compressor' : return 14 128 | case 'effect_drive' : return 23 129 | case 'effect_pan' : return 24 130 | 131 | case 'uv_monitor' : return null 132 | } 133 | 134 | console.log("Unknown",id); 135 | return -1; 136 | } 137 | 138 | this.build = function() 139 | { 140 | return "
"; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /desktop/sources/scripts/lib/controller.js: -------------------------------------------------------------------------------- 1 | function Controller() 2 | { 3 | this.menu = {default:{}}; 4 | this.mode = "default"; 5 | 6 | this.app = require('electron').remote.app; 7 | 8 | this.start = function() 9 | { 10 | } 11 | 12 | this.add = function(mode,cat,label,fn,accelerator) 13 | { 14 | if(!this.menu[mode]){ this.menu[mode] = {}; } 15 | if(!this.menu[mode][cat]){ this.menu[mode][cat] = {}; } 16 | this.menu[mode][cat][label] = {fn:fn,accelerator:accelerator}; 17 | console.log(`${mode}/${cat}/${label} <${accelerator}>`); 18 | } 19 | 20 | this.add_role = function(mode,cat,label) 21 | { 22 | if(!this.menu[mode]){ this.menu[mode] = {}; } 23 | if(!this.menu[mode][cat]){ this.menu[mode][cat] = {}; } 24 | this.menu[mode][cat][label] = {role:label}; 25 | } 26 | 27 | this.set = function(mode = "default") 28 | { 29 | this.mode = mode; 30 | this.commit(); 31 | } 32 | 33 | this.format = function() 34 | { 35 | var f = []; 36 | var m = this.menu[this.mode]; 37 | for(cat in m){ 38 | var submenu = []; 39 | for(name in m[cat]){ 40 | var option = m[cat][name]; 41 | if(option.role){ 42 | submenu.push({role:option.role}) 43 | } 44 | else{ 45 | submenu.push({label:name,accelerator:option.accelerator,click:option.fn}) 46 | } 47 | } 48 | f.push({label:cat,submenu:submenu}); 49 | } 50 | return f; 51 | } 52 | 53 | this.commit = function() 54 | { 55 | this.app.inject_menu(this.format()); 56 | } 57 | 58 | this.docs = function() 59 | { 60 | console.log("Generating docs.."); 61 | var svg = this.generate_svg(this.format()) 62 | var txt = this.documentation(this.format()); 63 | dialog.showSaveDialog((fileName) => { 64 | if (fileName === undefined){ return; } 65 | fileName = fileName.substr(-4,4) != ".svg" ? fileName+".svg" : fileName; 66 | fs.writeFile(fileName,svg); 67 | fs.writeFile(fileName.replace(".svg",".md"),txt); 68 | }); 69 | } 70 | 71 | this.generate_svg = function(m) 72 | { 73 | var svg_html = ""; 74 | 75 | for(id in this.layout){ 76 | var key = this.layout[id]; 77 | var acc = this.accelerator_for_key(key.name,m); 78 | svg_html += ``; 79 | svg_html += ``; 80 | svg_html += `${key.name.toUpperCase()}`; 81 | svg_html += acc && acc.basic ? `${acc.basic}` : ''; 82 | svg_html += acc && acc.ctrl ? `${acc.ctrl}` : ''; 83 | } 84 | return `${svg_html}`; 85 | } 86 | 87 | this.documentation = function() 88 | { 89 | var txt = ""; 90 | 91 | txt += this.documentation_for_mode("default",this.menu.default); 92 | 93 | for(name in this.menu){ 94 | if(name == "default"){ continue; } 95 | txt += this.documentation_for_mode(name,this.menu[name]); 96 | } 97 | return txt; 98 | } 99 | 100 | this.documentation_for_mode = function(name,mode) 101 | { 102 | var txt = `## ${name} Mode\n\n`; 103 | 104 | for(id in mode){ 105 | if(id == "*"){ continue; } 106 | txt += `### ${id}\n`; 107 | for(name in mode[id]){ 108 | var option = mode[id][name]; 109 | txt += `- ${name}: \`${option.accelerator}\`\n`; 110 | } 111 | txt += "\n" 112 | } 113 | 114 | return txt+"\n"; 115 | } 116 | 117 | this.accelerator_for_key = function(key,menu) 118 | { 119 | var acc = {basic:null,ctrl:null} 120 | for(cat in menu){ 121 | var options = menu[cat]; 122 | for(id in options.submenu){ 123 | var option = options.submenu[id]; if(option.role){ continue; } 124 | acc.basic = (option.accelerator.toLowerCase() == key.toLowerCase()) ? option.label.toUpperCase().replace("TOGGLE ","").substr(0,8).trim() : acc.basic; 125 | acc.ctrl = (option.accelerator.toLowerCase() == ("CmdOrCtrl+"+key).toLowerCase()) ? option.label.toUpperCase().replace("TOGGLE ","").substr(0,8).trim() : acc.ctrl; 126 | } 127 | } 128 | return acc; 129 | } 130 | 131 | this.layout = [ 132 | {x:0, y:0, width:60, height:60, name:"esc"}, 133 | {x:60, y:0, width:60, height:60, name:"1"}, 134 | {x:120, y:0, width:60, height:60, name:"2"}, 135 | {x:180, y:0, width:60, height:60, name:"3"}, 136 | {x:240, y:0, width:60, height:60, name:"4"}, 137 | {x:300, y:0, width:60, height:60, name:"5"}, 138 | {x:360, y:0, width:60, height:60, name:"6"}, 139 | {x:420, y:0, width:60, height:60, name:"7"}, 140 | {x:480, y:0, width:60, height:60, name:"8"}, 141 | {x:540, y:0, width:60, height:60, name:"9"}, 142 | {x:600, y:0, width:60, height:60, name:"0"}, 143 | {x:660, y:0, width:60, height:60, name:"-"}, 144 | {x:720, y:0, width:60, height:60, name:"plus"}, 145 | {x:780, y:0, width:120, height:60, name:"backspace"}, 146 | {x:0, y:60, width:90, height:60, name:"tab"}, 147 | {x:90, y:60, width:60, height:60, name:"q"}, 148 | {x:150, y:60, width:60, height:60, name:"w"}, 149 | {x:210, y:60, width:60, height:60, name:"e"}, 150 | {x:270, y:60, width:60, height:60, name:"r"}, 151 | {x:330, y:60, width:60, height:60, name:"t"}, 152 | {x:390, y:60, width:60, height:60, name:"y"}, 153 | {x:450, y:60, width:60, height:60, name:"u"}, 154 | {x:510, y:60, width:60, height:60, name:"i"}, 155 | {x:570, y:60, width:60, height:60, name:"o"}, 156 | {x:630, y:60, width:60, height:60, name:"p"}, 157 | {x:690, y:60, width:60, height:60, name:"["}, 158 | {x:750, y:60, width:60, height:60, name:"]"}, 159 | {x:810, y:60, width:90, height:60, name:"|"}, 160 | {x:0, y:120, width:105, height:60, name:"caps"}, 161 | {x:105, y:120, width:60, height:60, name:"a"}, 162 | {x:165, y:120, width:60, height:60, name:"s"}, 163 | {x:225, y:120, width:60, height:60, name:"d"}, 164 | {x:285, y:120, width:60, height:60, name:"f"}, 165 | {x:345, y:120, width:60, height:60, name:"g"}, 166 | {x:405, y:120, width:60, height:60, name:"h"}, 167 | {x:465, y:120, width:60, height:60, name:"j"}, 168 | {x:525, y:120, width:60, height:60, name:"k"}, 169 | {x:585, y:120, width:60, height:60, name:"l"}, 170 | {x:645, y:120, width:60, height:60, name:";"}, 171 | {x:705, y:120, width:60, height:60, name:"'"}, 172 | {x:765, y:120, width:135, height:60, name:"enter"}, 173 | {x:0, y:180, width:135, height:60, name:"shift"}, 174 | {x:135, y:180, width:60, height:60, name:"z"}, 175 | {x:195, y:180, width:60, height:60, name:"x"}, 176 | {x:255, y:180, width:60, height:60, name:"c"}, 177 | {x:315, y:180, width:60, height:60, name:"v"}, 178 | {x:375, y:180, width:60, height:60, name:"b"}, 179 | {x:435, y:180, width:60, height:60, name:"n"}, 180 | {x:495, y:180, width:60, height:60, name:"m"}, 181 | {x:555, y:180, width:60, height:60, name:","}, 182 | {x:615, y:180, width:60, height:60, name:"."}, 183 | {x:675, y:180, width:60, height:60, name:"/"}, 184 | {x:735, y:180, width:165, height:60, name:"capslock"}, 185 | {x:0, y:240, width:90, height:60, name:"ctrl"}, 186 | {x:90, y:240, width:90, height:60, name:"cmd"}, 187 | {x:180, y:240, width:90, height:60, name:"alt"}, 188 | {x:270, y:240, width:270, height:60, name:"space"}, 189 | {x:810, y:240, width:90, height:60, name:"ctrl"}, 190 | {x:720, y:240, width:90, height:60, name:"pn"}, 191 | {x:630, y:240, width:90, height:60, name:"fn"}, 192 | {x:540, y:240, width:90, height:60, name:"alt"} 193 | ]; 194 | } 195 | 196 | module.exports = new Controller(); -------------------------------------------------------------------------------- /desktop/sources/scripts/lib/history.js: -------------------------------------------------------------------------------- 1 | function History() 2 | { 3 | this.index = 0; 4 | this.a = []; 5 | 6 | this.clear = function() 7 | { 8 | this.a = []; 9 | this.index = 0; 10 | } 11 | 12 | this.push = function(data) 13 | { 14 | if(this.index < this.a.length-1){ 15 | this.fork(); 16 | } 17 | this.index = this.a.length; 18 | this.a = this.a.slice(0,this.index); 19 | this.a.push(copy(data)); 20 | 21 | if(this.a.length > 20){ 22 | this.a.shift(); 23 | } 24 | } 25 | 26 | this.fork = function() 27 | { 28 | this.a = this.a.slice(0,this.index+1); 29 | } 30 | 31 | this.pop = function() 32 | { 33 | return this.a.pop(); 34 | } 35 | 36 | this.prev = function() 37 | { 38 | this.index = clamp(this.index-1,0,this.a.length-1); 39 | return copy(this.a[this.index]); 40 | } 41 | 42 | this.next = function() 43 | { 44 | this.index = clamp(this.index+1,0,this.a.length-1); 45 | return copy(this.a[this.index]); 46 | } 47 | 48 | function copy(data){ return data ? JSON.parse(JSON.stringify(data)) : []; } 49 | function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } 50 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/lib/theme.js: -------------------------------------------------------------------------------- 1 | function Theme() 2 | { 3 | var app = this; 4 | 5 | this.el = document.createElement("style"); 6 | this.el.type = 'text/css'; 7 | this.default = {meta:{}, data: { background: "#222", f_high: "#fff", f_med: "#777", f_low: "#444", f_inv: "#000", b_high: "#000", b_med: "#affec7", b_low: "#000", b_inv: "#affec7" }} 8 | this.active = this.default; 9 | 10 | this.start = function() 11 | { 12 | this.load(localStorage.theme ? localStorage.theme : this.default, this.default); 13 | window.addEventListener('dragover',this.drag_enter); 14 | window.addEventListener('drop', this.drag); 15 | document.head.appendChild(this.el) 16 | } 17 | 18 | this.load = function(t, fall_back) 19 | { 20 | var theme = is_json(t) ? JSON.parse(t).data : t.data; 21 | 22 | if(!theme || !theme.background){ 23 | if(fall_back) { 24 | theme = fall_back.data; 25 | } else { 26 | return; 27 | } 28 | } 29 | 30 | var css = ` 31 | :root { 32 | --background: ${theme.background}; 33 | --f_high: ${theme.f_high}; 34 | --f_med: ${theme.f_med}; 35 | --f_low: ${theme.f_low}; 36 | --f_inv: ${theme.f_inv}; 37 | --b_high: ${theme.b_high}; 38 | --b_med: ${theme.b_med}; 39 | --b_low: ${theme.b_low}; 40 | --b_inv: ${theme.b_inv}; 41 | }`; 42 | 43 | this.active = theme; 44 | this.el.textContent = css; 45 | localStorage.setItem("theme", JSON.stringify({data: theme})); 46 | } 47 | 48 | this.reset = function() 49 | { 50 | this.load(this.default); 51 | } 52 | 53 | this.drag_enter = function(e) 54 | { 55 | e.stopPropagation(); 56 | e.preventDefault(); 57 | e.dataTransfer.dropEffect = 'copy'; 58 | } 59 | 60 | this.drag = function(e) 61 | { 62 | e.preventDefault(); 63 | e.stopPropagation(); 64 | 65 | var file = e.dataTransfer.files[0]; 66 | 67 | if(!file.name || !file.name.indexOf(".thm") < 0){ console.log("Theme","Not a theme"); return; } 68 | 69 | var reader = new FileReader(); 70 | reader.onload = function(e){ 71 | app.load(e.target.result); 72 | }; 73 | reader.readAsText(file); 74 | } 75 | 76 | function is_json(text) 77 | { 78 | try{ 79 | JSON.parse(text); 80 | return true; 81 | } 82 | catch (error){ 83 | return false; 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/loop.js: -------------------------------------------------------------------------------- 1 | function Loop() 2 | { 3 | this.is_active = false; 4 | 5 | this.x = 0; 6 | this.width = 0; 7 | this.y = 0; 8 | this.height = 0; 9 | 10 | this.reset = function() 11 | { 12 | this.x = 0; 13 | this.width = 15; 14 | this.y = marabu.selection.track; 15 | this.height = 0; 16 | } 17 | 18 | this.start = function() 19 | { 20 | marabu.cheatcode.stop(); 21 | this.is_active = true; 22 | this.reset(); 23 | marabu.update(); 24 | marabu.controller.set("loop"); 25 | } 26 | 27 | this.stop = function() 28 | { 29 | this.is_active = false; 30 | marabu.stop(); 31 | this.reset(); 32 | marabu.update(); 33 | marabu.controller.set("default"); 34 | } 35 | 36 | this.mod = function(m) 37 | { 38 | this.height = clamp(this.height+m,0,(marabu.song.length-this.y)+3); 39 | marabu.update(); 40 | } 41 | 42 | this.buffer = []; 43 | 44 | this.copy = function() 45 | { 46 | this.buffer = []; 47 | for(var i = 0; i < 16; i++){ 48 | this.buffer[i] = marabu.song.song().songData[i].p.slice(this.y,this.y+this.height+1); 49 | } 50 | this.stop(); 51 | } 52 | 53 | this.paste = function() 54 | { 55 | for(var i = 0; i < 16; i++){ 56 | marabu.song.song().songData[i].p.splice(this.y, 0, ...this.buffer[i]); 57 | } 58 | marabu.history.push(marabu.song.song()); 59 | this.stop(); 60 | } 61 | 62 | this.cut = function() 63 | { 64 | // Copy 65 | this.buffer = []; 66 | for(var i = 0; i < 16; i++){ 67 | this.buffer[i] = marabu.song.song().songData[i].p.slice(this.y,this.y+this.height+1); 68 | } 69 | // Erase 70 | for(var y = this.y; y < this.y+(this.height+1); y++){ 71 | for(var i = 0; i < 16; i++){ 72 | marabu.song.song().songData[i].p[y] = 0; 73 | } 74 | } 75 | marabu.history.push(marabu.song.song()); 76 | this.stop(); 77 | } 78 | 79 | this.erase = function() 80 | { 81 | for(var i = 0; i < 16; i++){ 82 | marabu.song.song().songData[i].p.splice(this.y,this.height+1); 83 | } 84 | marabu.history.push(marabu.song.song()); 85 | this.stop(); 86 | } 87 | 88 | this.solo = function() 89 | { 90 | this.x = marabu.selection.instrument; 91 | this.width = 0; 92 | this.y = marabu.selection.track; 93 | this.height = 0; 94 | marabu.update(); 95 | } 96 | 97 | this.play = function() 98 | { 99 | this.is_active = false; 100 | 101 | marabu.song.play_loop(this.range()); 102 | } 103 | 104 | this.range = function() 105 | { 106 | return { 107 | firstCol: this.x, 108 | lastCol: this.x + this.width, 109 | firstRow: this.y, 110 | lastRow: this.y + this.height 111 | }; 112 | } 113 | 114 | this.render = function() 115 | { 116 | this.stop(); 117 | marabu.song.export_wav(this.range()); 118 | } 119 | 120 | this.set_height = function(mod) 121 | { 122 | this.height = mod; 123 | marabu.update(); 124 | } 125 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/marabu.js: -------------------------------------------------------------------------------- 1 | function Marabu() 2 | { 3 | this.history = new History(); 4 | this.theme = new Theme(); 5 | this.controller = new Controller(); 6 | 7 | this.el = document.createElement("app"); 8 | this.el.style.opacity = 0; 9 | this.el.id = "marabu"; 10 | 11 | this.wrapper_el = document.createElement("yu"); 12 | this.wrapper_el.className = "wrapper"; 13 | 14 | this.el.appendChild(this.wrapper_el); 15 | 16 | document.body.appendChild(this.el); 17 | 18 | this.selection = {instrument:0,track:0,row:0,octave:5,control:0}; 19 | this.channels = 16; 20 | 21 | this.song = new Song(); 22 | this.sequencer = new Sequencer(); 23 | this.editor = new Editor(8,4); 24 | this.instrument = new Instrument(); 25 | 26 | this.cheatcode = new Cheatcode(); 27 | this.loop = new Loop(); 28 | this.arp = new Arp(); 29 | 30 | this.start = function() 31 | { 32 | this.wrapper_el.innerHTML += "
"; 33 | this.wrapper_el.innerHTML += this.editor.build(); 34 | this.wrapper_el.innerHTML += this.instrument.build(); 35 | 36 | this.song.init(); 37 | this.theme.start(); 38 | 39 | this.sequencer.start(); 40 | this.editor.start(); 41 | this.instrument.start(); 42 | 43 | this.song.update(); 44 | this.sequencer.update(); 45 | this.editor.update(); 46 | this.instrument.update(); 47 | 48 | this.controller.add("default","*","About",() => { require('electron').shell.openExternal('https://github.com/hundredrabbits/Marabu'); },"CmdOrCtrl+,"); 49 | this.controller.add("default","*","Fullscreen",() => { app.toggle_fullscreen(); },"CmdOrCtrl+Enter"); 50 | this.controller.add("default","*","Hide",() => { app.toggle_visible(); },"CmdOrCtrl+H"); 51 | this.controller.add("default","*","Inspect",() => { app.inspect(); },"CmdOrCtrl+."); 52 | this.controller.add("default","*","Documentation",() => { marabu.controller.docs(); },"CmdOrCtrl+Esc"); 53 | this.controller.add("default","*","Reset",() => { marabu.theme.reset(); },"CmdOrCtrl+Backspace"); 54 | this.controller.add("default","*","Quit",() => { app.exit(); },"CmdOrCtrl+Q"); 55 | this.controller.add("default","File","New",() => { marabu.new(); },"CmdOrCtrl+N"); 56 | this.controller.add("default","File","Open",() => { marabu.open(); },"CmdOrCtrl+O"); 57 | this.controller.add("default","File","Save",() => { marabu.save(); },"CmdOrCtrl+S"); 58 | this.controller.add("default","File","Save As",() => { marabu.export(); },"CmdOrCtrl+Shift+S"); 59 | this.controller.add("default","File","Render",() => { marabu.render(); },"CmdOrCtrl+R"); 60 | this.controller.add("default","File","Export Ins",() => { marabu.export_instrument(); },"CmdOrCtrl+I"); 61 | this.controller.add("default","Edit","Inc BPM",() => { marabu.move_bpm(5) },">"); 62 | this.controller.add("default","Edit","Dec BPM",() => { marabu.move_bpm(-5) },"<"); 63 | this.controller.add("default","Edit","Delete",() => { marabu.set_note(0); marabu.remove_control_value(0); },"Backspace"); 64 | this.controller.add("default","Edit","Undo",() => { marabu.undo(); },"CmdOrCtrl+Z"); 65 | this.controller.add("default","Edit","Redo",() => { marabu.redo(); },"CmdOrCtrl+Shift+Z"); 66 | this.controller.add("default","Select","1st Row",() => { marabu.select_row(0); },"1"); 67 | this.controller.add("default","Select","4th Row",() => { marabu.select_row(4); },"2"); 68 | this.controller.add("default","Select","8th Row",() => { marabu.select_row(8); },"3"); 69 | this.controller.add("default","Select","12th Row",() => { marabu.select_row(12); },"4"); 70 | this.controller.add("default","Select","16th Row",() => { marabu.select_row(16); },"5"); 71 | this.controller.add("default","Select","20th Row",() => { marabu.select_row(20); },"6"); 72 | this.controller.add("default","Select","24th Row",() => { marabu.select_row(24); },"7"); 73 | this.controller.add("default","Select","28th Row",() => { marabu.select_row(28); },"8"); 74 | this.controller.add("default","Track","Next Inst",() => { marabu.move_inst(1); },"Right"); 75 | this.controller.add("default","Track","Prev Inst",() => { marabu.move_inst(-1) },"Left"); 76 | this.controller.add("default","Track","Next Row",() => { marabu.move_row(1); },"Down"); 77 | this.controller.add("default","Track","Prev Row",() => { marabu.move_row(-1) },"Up"); 78 | this.controller.add("default","Track","Next Track",() => { marabu.move_track(1); },"CmdOrCtrl+Down"); 79 | this.controller.add("default","Track","Prev Track",() => { marabu.move_track(-1); },"CmdOrCtrl+Up"); 80 | this.controller.add("default","Track","Next Pattern",() => { marabu.move_pattern(1); },"CmdOrCtrl+Right"); 81 | this.controller.add("default","Track","Prev Pattern",() => { marabu.move_pattern(-1); },"CmdOrCtrl+Left"); 82 | this.controller.add("default","Play","Track",() => { marabu.play(); },"Space"); 83 | this.controller.add("default","Play","Range",() => { marabu.loop.start(); },"Enter"); 84 | this.controller.add("default","Play","Stop",() => { marabu.stop(); },"Esc"); 85 | this.controller.add("default","Mode","Cheatcode",() => { marabu.cheatcode.start(); },"CmdOrCtrl+K"); 86 | this.controller.add("default","Mode","Loop",() => { marabu.loop.start(); },"CmdOrCtrl+L"); 87 | this.controller.add("default","Mode","Arp",() => { marabu.arp.start(); },"CmdOrCtrl+M"); 88 | this.controller.add("default","Mode","Composer",() => { marabu.editor.toggle_composer(); },"M"); 89 | this.controller.add("default","Keyboard","Inc Octave",() => { marabu.move_octave(1); },"X"); 90 | this.controller.add("default","Keyboard","Dec Octave",() => { marabu.move_octave(-1); },"Z"); 91 | this.controller.add("default","Keyboard","C",() => { marabu.play_note(0,true); },"A"); 92 | this.controller.add("default","Keyboard","C#",() => { marabu.play_note(1,true); },"W"); 93 | this.controller.add("default","Keyboard","D",() => { marabu.play_note(2,true); },"S"); 94 | this.controller.add("default","Keyboard","D#",() => { marabu.play_note(3,true); },"E"); 95 | this.controller.add("default","Keyboard","E",() => { marabu.play_note(4,true); },"D"); 96 | this.controller.add("default","Keyboard","F",() => { marabu.play_note(5,true); },"F"); 97 | this.controller.add("default","Keyboard","F#",() => { marabu.play_note(6,true); },"T"); 98 | this.controller.add("default","Keyboard","G",() => { marabu.play_note(7,true); },"G"); 99 | this.controller.add("default","Keyboard","G#",() => { marabu.play_note(8,true); },"Y"); 100 | this.controller.add("default","Keyboard","A",() => { marabu.play_note(9,true); },"H"); 101 | this.controller.add("default","Keyboard","A#",() => { marabu.play_note(10,true); },"U"); 102 | this.controller.add("default","Keyboard","B",() => { marabu.play_note(11,true); },"J"); 103 | this.controller.add("default","Keyboard","(Right)C",() => { marabu.play_note(0,false); },"Shift+A"); 104 | this.controller.add("default","Keyboard","(Right)C#",() => { marabu.play_note(1,false); },"Shift+W"); 105 | this.controller.add("default","Keyboard","(Right)D",() => { marabu.play_note(2,false); },"Shift+S"); 106 | this.controller.add("default","Keyboard","(Right)D#",() => { marabu.play_note(3,false); },"Shift+E"); 107 | this.controller.add("default","Keyboard","(Right)E",() => { marabu.play_note(4,false); },"Shift+D"); 108 | this.controller.add("default","Keyboard","(Right)F",() => { marabu.play_note(5,false); },"Shift+F"); 109 | this.controller.add("default","Keyboard","(Right)F#",() => { marabu.play_note(6,false); },"Shift+T"); 110 | this.controller.add("default","Keyboard","(Right)G",() => { marabu.play_note(7,false); },"Shift+G"); 111 | this.controller.add("default","Keyboard","(Right)G#",() => { marabu.play_note(8,false); },"Shift+Y"); 112 | this.controller.add("default","Keyboard","(Right)A",() => { marabu.play_note(9,false); },"Shift+H"); 113 | this.controller.add("default","Keyboard","(Right)A#",() => { marabu.play_note(10,false); },"Shift+U"); 114 | this.controller.add("default","Keyboard","(Right)B",() => { marabu.play_note(11,false); },"Shift+J"); 115 | this.controller.add("default","Instrument","Next Control",() => { marabu.move_control(1); },"Shift+Up"); 116 | this.controller.add("default","Instrument","Prev Control",() => { marabu.move_control(-1); },"Shift+Down"); 117 | this.controller.add("default","Instrument","Inc Control +10",() => { marabu.move_control_value(10); },"Shift+Right"); 118 | this.controller.add("default","Instrument","Dec Control -10",() => { marabu.move_control_value(-10); },"Shift+Left"); 119 | this.controller.add("default","Instrument","Inc Control 1",() => { marabu.move_control_value(1); },"}"); 120 | this.controller.add("default","Instrument","Dec Control -1",() => { marabu.move_control_value(-1); },"{"); 121 | this.controller.add("default","Instrument","Inc Control 10(alt)",() => { marabu.move_control_value(10); },"]"); 122 | this.controller.add("default","Instrument","Dec Control -10(alt)",() => { marabu.move_control_value(-10); },"["); 123 | this.controller.add("default","Instrument","Min",() => { marabu.move_control_min(); },"9"); 124 | this.controller.add("default","Instrument","Max",() => { marabu.move_control_max(); },"0"); 125 | this.controller.add("default","Instrument","Keyframe",() => { marabu.add_control_value(); },"/"); 126 | 127 | this.controller.add("cheatcode","*","Quit",() => { app.exit(); },"CmdOrCtrl+Q"); 128 | this.controller.add("cheatcode","Mode","Stop",() => { marabu.cheatcode.stop(); },"Esc"); 129 | this.controller.add("cheatcode","Mode","Copy",() => { marabu.cheatcode.copy(); },"C"); 130 | this.controller.add("cheatcode","Mode","Paste",() => { marabu.cheatcode.paste(); },"V"); 131 | this.controller.add("cheatcode","Mode","Erase",() => { marabu.cheatcode.del(); },"Backspace"); 132 | this.controller.add("cheatcode","Effect","Inc Note +12",() => { marabu.cheatcode.mod(12); },"]"); 133 | this.controller.add("cheatcode","Effect","Dec Note -12",() => { marabu.cheatcode.mod(-12); },"["); 134 | this.controller.add("cheatcode","Effect","Inc Note +1",() => { marabu.cheatcode.mod(1); },"}"); 135 | this.controller.add("cheatcode","Effect","Dec Note -1",() => { marabu.cheatcode.mod(-1); },"{"); 136 | this.controller.add("cheatcode","Selection","All",() => { marabu.cheatcode.set_rate(1); },"1"); 137 | this.controller.add("cheatcode","Selection","2nd",() => { marabu.cheatcode.set_rate(2); },"2"); 138 | this.controller.add("cheatcode","Selection","3rd",() => { marabu.cheatcode.set_rate(3); },"3"); 139 | this.controller.add("cheatcode","Selection","4th",() => { marabu.cheatcode.set_rate(4); },"4"); 140 | this.controller.add("cheatcode","Selection","5th",() => { marabu.cheatcode.set_rate(5); },"5"); 141 | this.controller.add("cheatcode","Selection","6th",() => { marabu.cheatcode.set_rate(6); },"6"); 142 | this.controller.add("cheatcode","Selection","7th",() => { marabu.cheatcode.set_rate(7); },"7"); 143 | this.controller.add("cheatcode","Selection","8th",() => { marabu.cheatcode.set_rate(8); },"8"); 144 | this.controller.add("cheatcode","Selection","Offset +1",() => { marabu.cheatcode.set_offset(1); },"Right"); 145 | this.controller.add("cheatcode","Selection","Offset -1",() => { marabu.cheatcode.set_offset(-1) },"Left"); 146 | this.controller.add("cheatcode","Selection","Length +1",() => { marabu.cheatcode.set_length(1); },"Down"); 147 | this.controller.add("cheatcode","Selection","Length -1",() => { marabu.cheatcode.set_length(-1) },"Up"); 148 | this.controller.add("cheatcode","Keyboard","C",() => { marabu.cheatcode.ins(0); },"A"); 149 | this.controller.add("cheatcode","Keyboard","C#",() => { marabu.cheatcode.ins(1); },"W"); 150 | this.controller.add("cheatcode","Keyboard","D",() => { marabu.cheatcode.ins(2); },"S"); 151 | this.controller.add("cheatcode","Keyboard","D#",() => { marabu.cheatcode.ins(3); },"E"); 152 | this.controller.add("cheatcode","Keyboard","E",() => { marabu.cheatcode.ins(4); },"D"); 153 | this.controller.add("cheatcode","Keyboard","F",() => { marabu.cheatcode.ins(5); },"F"); 154 | this.controller.add("cheatcode","Keyboard","F#",() => { marabu.cheatcode.ins(6); },"T"); 155 | this.controller.add("cheatcode","Keyboard","G",() => { marabu.cheatcode.ins(7); },"G"); 156 | this.controller.add("cheatcode","Keyboard","G#",() => { marabu.cheatcode.ins(8); },"Y"); 157 | this.controller.add("cheatcode","Keyboard","A",() => { marabu.cheatcode.ins(9); },"H"); 158 | this.controller.add("cheatcode","Keyboard","A#",() => { marabu.cheatcode.ins(10); },"U"); 159 | this.controller.add("cheatcode","Keyboard","B",() => { marabu.cheatcode.ins(11); },"J"); 160 | 161 | this.controller.add("loop","*","Quit",() => { app.exit(); },"CmdOrCtrl+Q"); 162 | this.controller.add("loop","Edit","Clear",() => { marabu.loop.cut(); },"X"); 163 | this.controller.add("loop","Edit","Copy",() => { marabu.loop.copy(); },"C"); 164 | this.controller.add("loop","Edit","Paste",() => { marabu.loop.paste(); },"V"); 165 | this.controller.add("loop","Edit","Delete",() => { marabu.loop.erase(); },"Backspace"); 166 | this.controller.add("loop","Select","Solo",() => { marabu.loop.solo(); },"/"); 167 | this.controller.add("loop","Select","1 Row",() => { marabu.loop.set_height(0); },"1"); 168 | this.controller.add("loop","Select","2 Rows",() => { marabu.loop.set_height(1); },"2"); 169 | this.controller.add("loop","Select","3 Rows",() => { marabu.loop.set_height(2); },"3"); 170 | this.controller.add("loop","Select","4 Rows",() => { marabu.loop.set_height(3); },"4"); 171 | this.controller.add("loop","Select","5 Rows",() => { marabu.loop.set_height(4); },"5"); 172 | this.controller.add("loop","Select","6 Rows",() => { marabu.loop.set_height(5); },"6"); 173 | this.controller.add("loop","Select","7 Rows",() => { marabu.loop.set_height(6); },"7"); 174 | this.controller.add("loop","Select","8 Rows",() => { marabu.loop.set_height(7); },"8"); 175 | this.controller.add("loop","Select","Inc",() => { marabu.loop.mod(1); },"Down"); 176 | this.controller.add("loop","Select","Dec",() => { marabu.loop.mod(-1); },"Up"); 177 | this.controller.add("loop","Mode","Play",() => { marabu.loop.play(); },"Enter"); 178 | this.controller.add("loop","Mode","Stop",() => { marabu.loop.stop(); },"Esc"); 179 | this.controller.add("loop","Mode","render",() => { marabu.loop.render(); },"CmdOrCtrl+R"); 180 | 181 | 182 | this.controller.add("arp","*","Quit",() => { app.exit(); },"CmdOrCtrl+Q"); 183 | this.controller.add("arp","Mode","Pause/Stop",() => { marabu.arp.stop(); },"Esc"); 184 | this.controller.add("arp","Keyboard","C",() => { marabu.arp.ins(0); },"A"); 185 | this.controller.add("arp","Keyboard","C#",() => { marabu.arp.ins(1); },"W"); 186 | this.controller.add("arp","Keyboard","D",() => { marabu.arp.ins(2); },"S"); 187 | this.controller.add("arp","Keyboard","D#",() => { marabu.arp.ins(3); },"E"); 188 | this.controller.add("arp","Keyboard","E",() => { marabu.arp.ins(4); },"D"); 189 | this.controller.add("arp","Keyboard","F",() => { marabu.arp.ins(5); },"F"); 190 | this.controller.add("arp","Keyboard","F#",() => { marabu.arp.ins(6); },"T"); 191 | this.controller.add("arp","Keyboard","G",() => { marabu.arp.ins(7); },"G"); 192 | this.controller.add("arp","Keyboard","G#",() => { marabu.arp.ins(8); },"Y"); 193 | this.controller.add("arp","Keyboard","A",() => { marabu.arp.ins(9); },"H"); 194 | this.controller.add("arp","Keyboard","A#",() => { marabu.arp.ins(10); },"U"); 195 | this.controller.add("arp","Keyboard","B",() => { marabu.arp.ins(11); },"J"); 196 | this.controller.add("arp","Keyboard","Skip",() => { marabu.arp.ins(-99); },"Space"); 197 | 198 | this.controller.commit(); 199 | 200 | setTimeout(marabu.show,250) 201 | } 202 | 203 | this.update = function() 204 | { 205 | this.selection.instrument = clamp(this.selection.instrument,0,this.channels-1); 206 | this.selection.track = clamp(this.selection.track,0,this.sequencer.length-1); 207 | this.selection.row = clamp(this.selection.row,0,31); 208 | this.selection.octave = clamp(this.selection.octave,3,8); 209 | this.selection.control = clamp(this.selection.control,0,23); 210 | 211 | this.song.update(); 212 | this.sequencer.update(); 213 | this.editor.update(); 214 | this.instrument.update(); 215 | } 216 | 217 | this.undo = function() 218 | { 219 | this.song.replace_song(this.history.prev()); 220 | this.update(); 221 | } 222 | 223 | this.redo = function() 224 | { 225 | this.song.replace_song(this.history.next()); 226 | this.update(); 227 | } 228 | 229 | // Controls 230 | 231 | this.move_inst = function(mod) 232 | { 233 | this.selection.instrument += mod; 234 | this.update(); 235 | } 236 | 237 | this.move_pattern = function(mod) 238 | { 239 | var p = this.song.pattern_at(this.selection.instrument,this.selection.track) + mod; 240 | p = clamp(p,0,15); 241 | this.song.inject_pattern_at(this.selection.instrument,this.selection.track,p); 242 | this.update(); 243 | } 244 | 245 | this.select_row = function(row) 246 | { 247 | this.selection.row = row; 248 | this.update(); 249 | } 250 | 251 | this.move_row = function(mod) 252 | { 253 | this.selection.row += mod; 254 | this.update(); 255 | } 256 | 257 | this.move_track = function(mod) 258 | { 259 | this.selection.track += mod; 260 | this.update(); 261 | } 262 | 263 | this.move_octave = function(mod) 264 | { 265 | this.selection.octave += mod; 266 | this.update(); 267 | } 268 | 269 | this.move_control = function(mod) 270 | { 271 | this.selection.control += mod; 272 | this.update(); 273 | } 274 | 275 | this.move_bpm = function(mod) 276 | { 277 | this.song.song().bpm = this.song.get_bpm() + mod; 278 | this.song.update_bpm(this.song.get_bpm() + mod); 279 | this.history.push(this.song.song()); 280 | this.update(); 281 | } 282 | 283 | this.move_control_value = function(mod,relative) 284 | { 285 | var control = this.instrument.control_target(this.selection.control); 286 | control.mod(mod,relative); 287 | this.history.push(this.song.song()); 288 | control.save(); 289 | } 290 | 291 | this.move_control_min = function() 292 | { 293 | var control = this.instrument.control_target(this.selection.control); 294 | control.value = control.min; 295 | this.history.push(this.song.song()); 296 | control.save(); 297 | control.update(); 298 | } 299 | 300 | this.move_control_max = function() 301 | { 302 | var control = this.instrument.control_target(this.selection.control); 303 | control.value = control.max; 304 | this.history.push(this.song.song()); 305 | control.save(); 306 | control.update(); 307 | } 308 | 309 | this.add_control_value = function() 310 | { 311 | var control = this.instrument.control_target(this.selection.control); 312 | var control_storage = this.instrument.get_storage(control.family+"_"+control.id); 313 | var control_value = control.value; 314 | 315 | this.song.inject_effect_at(this.selection.instrument,this.selection.track,this.selection.row,control_storage+1,control_value); 316 | this.history.push(this.song.song()); 317 | this.update(); 318 | } 319 | 320 | this.remove_control_value = function() 321 | { 322 | var control = this.instrument.control_target(this.selection.control); 323 | var control_storage = this.instrument.get_storage(control.family+"_"+control.id); 324 | var control_value = control.value; 325 | 326 | this.song.erase_effect_at(this.selection.instrument,this.selection.track,this.selection.row); 327 | this.history.push(this.song.song()); 328 | this.update(); 329 | } 330 | 331 | this.set_note = function(val) 332 | { 333 | this.song.inject_note_at(this.selection.instrument,this.selection.track,this.selection.row,val-87); 334 | 335 | if(val == 0){ 336 | this.song.inject_note_at(this.selection.instrument,this.selection.track,this.selection.row+32,val-87); 337 | } 338 | this.history.push(this.song.song()); 339 | this.update(); 340 | } 341 | 342 | this.move_note_value = function(mod) 343 | { 344 | var note = marabu.song.note_at(this.selection.instrument,this.selection.track,this.selection.row); 345 | 346 | this.song.inject_note_at(this.selection.instrument,this.selection.track,this.selection.row,note+mod-87); 347 | this.history.push(this.song.song()); 348 | this.update(); 349 | } 350 | 351 | this.play_note = function(note,right_hand = true) 352 | { 353 | var note_value = note + (this.selection.octave * 12); 354 | this.song.play_note(note_value); 355 | this.song.inject_note_at(this.selection.instrument,this.selection.track,this.selection.row+(right_hand ? 0 : 32),note_value); 356 | this.history.push(this.song.song()); 357 | this.update(); 358 | } 359 | 360 | // Methods 361 | 362 | this.is_playing = false; 363 | 364 | this.play = function() 365 | { 366 | if(this.selection.row > 0 || this.is_playing){ this.stop(); return; } 367 | console.log("Play!"); 368 | this.song.play_song(); 369 | this.is_playing = true; 370 | } 371 | 372 | this.stop = function() 373 | { 374 | console.log("Stop!"); 375 | this.song.stop_song(); 376 | this.instrument.controls.uv.monitor.clear(); 377 | this.is_playing = false; 378 | this.selection.row = 0; 379 | this.update(); 380 | } 381 | 382 | this.new = function() 383 | { 384 | this.history.clear(); 385 | this.path = null; 386 | this.song = new Song(); 387 | this.song.init(); 388 | this.update(); 389 | } 390 | 391 | this.path = null; 392 | 393 | this.open = function() 394 | { 395 | var filepath = dialog.showOpenDialog({filters: [{name: 'Marabu Files', extensions: ['mar', 'ins']}], properties: ['openFile']}); 396 | 397 | if(!filepath){ console.log("Nothing to load"); return; } 398 | 399 | fs.readFile(filepath[0], 'utf-8', (err, data) => { 400 | if(err){ alert("An error ocurred reading the file :" + err.message); return; } 401 | 402 | marabu.load(data,filepath[0]); 403 | }); 404 | this.history.clear(); 405 | } 406 | 407 | this.load = function(data,path = "") 408 | { 409 | console.log("loading",path); 410 | 411 | var file_type = path.split(".")[path.split(".").length-1]; 412 | 413 | if(file_type == "mar"){ 414 | var o = JSON.parse(data); 415 | marabu.load_file(o); 416 | marabu.path = path; 417 | } 418 | else if(file_type == "ins"){ 419 | var o = JSON.parse(data); 420 | marabu.load_instrument(o); 421 | } 422 | this.history.clear(); 423 | } 424 | 425 | this.save = function() 426 | { 427 | if(!marabu.path){ marabu.export(); return; } 428 | 429 | fs.writeFile(marabu.path, marabu.song.to_string(), (err) => { 430 | if(err) { alert("An error ocurred updating the file" + err.message); console.log(err); return; } 431 | console.log("saved",marabu.path); 432 | var el = document.getElementById("fxr31"); 433 | if(el){ el.className = "b_inv f_inv"; el.innerHTML = "--OK"; } 434 | }); 435 | } 436 | 437 | this.export = function() 438 | { 439 | this.song.update_ranges(); 440 | var str = this.song.to_string(); 441 | 442 | dialog.showSaveDialog({filters:[{name:'Marabu',extensions:['mar']}]},(fileName) => { 443 | if (fileName === undefined){ return; } 444 | fs.writeFile(`${fileName.substr(-4,4) != ".mar" ? fileName+".mar" : fileName}`, str, (err) => { 445 | if(err){ alert("An error ocurred creating the file "+ err.message); return; } 446 | marabu.path = fileName; 447 | var el = document.getElementById("fxr31"); 448 | if(el){ el.className = "b_inv f_inv"; el.innerHTML = "--OK"; } 449 | }); 450 | }); 451 | } 452 | 453 | this.load_file = function(track) 454 | { 455 | marabu.song.replace_song(track); 456 | this.history.clear(); 457 | marabu.update(); 458 | } 459 | 460 | this.export_instrument = function() 461 | { 462 | var instr = this.song.instrument(); 463 | var instr_obj = {}; 464 | instr_obj.name = instr.name; 465 | instr_obj.i = instr.i; 466 | var str = JSON.stringify(instr_obj); 467 | 468 | dialog.showSaveDialog({filters:[{name:"Instrument",extensions:['ins']}]},(fileName) => { 469 | if (fileName === undefined){ return; } 470 | fs.writeFile(`${fileName.substr(-4,4) != ".ins" ? fileName+".ins" : fileName}`, str, (err) => { 471 | if(err){ alert("An error ocurred creating the file "+ err.message); return; } 472 | }); 473 | }); 474 | } 475 | 476 | this.load_instrument = function(instr) 477 | { 478 | this.song.song().songData[this.selection.instrument].name = instr.name; 479 | this.song.song().songData[this.selection.instrument].i = instr.i; 480 | this.history.push(this.song.song()); 481 | this.update(); 482 | } 483 | 484 | this.render = function(val, is_passive = false) 485 | { 486 | this.song.export_wav(); 487 | } 488 | 489 | this.reset = function() 490 | { 491 | this.history.clear(); 492 | this.path = null; 493 | this.song = new Song(); 494 | this.theme.reset(); 495 | this.song.init(); 496 | this.update(); 497 | } 498 | 499 | this.show = function() 500 | { 501 | marabu.el.style.opacity = 1; 502 | } 503 | 504 | this.when_key = function(e) 505 | { 506 | var key = e.key; 507 | 508 | // These shortcuts are faster in repetition than the Electron managers. 509 | 510 | if(marabu.cheatcode.is_active == true){ return; } 511 | if(marabu.loop.is_active == true){ return; } 512 | 513 | // Arrows 514 | if(e.shiftKey){ // Instrument 515 | if(key == "ArrowDown") { marabu.move_control(1); e.preventDefault(); return; } 516 | if(key == "ArrowUp") { marabu.move_control(-1); e.preventDefault();return; } 517 | if(key == "ArrowRight"){ marabu.move_control_value(1,true); e.preventDefault(); return; } 518 | if(key == "ArrowLeft") { marabu.move_control_value(-1,true); e.preventDefault();return; } 519 | } 520 | else if(e.altKey || e.metaKey){ 521 | if(key == "ArrowDown") { marabu.move_track(1); e.preventDefault(); return; } 522 | if(key == "ArrowUp") { marabu.move_track(-1); e.preventDefault();return; } 523 | if(key == "ArrowRight"){ marabu.move_pattern(1); e.preventDefault(); return; } 524 | if(key == "ArrowLeft") { marabu.move_pattern(-1); e.preventDefault();return; } 525 | } 526 | else{ 527 | if(key == "ArrowRight"){ marabu.move_inst(1); e.preventDefault(); return; } 528 | if(key == "ArrowLeft") { marabu.move_inst(-1); e.preventDefault(); return; } 529 | if(key == "ArrowDown") { marabu.move_row(1); e.preventDefault(); return; } 530 | if(key == "ArrowUp") { marabu.move_row(-1); e.preventDefault(); return; } 531 | } 532 | if(key == "]") { marabu.move_control_value(1); e.preventDefault(); return; } 533 | if(key == "[") { marabu.move_control_value(-1); e.preventDefault(); return; } 534 | if(key == "}") { marabu.move_control_value(10); e.preventDefault(); return; } 535 | if(key == "{") { marabu.move_control_value(-10); e.preventDefault(); return; } 536 | } 537 | window.addEventListener("keydown", this.when_key, false); 538 | } 539 | 540 | window.addEventListener('dragover',function(e) 541 | { 542 | e.preventDefault(); 543 | e.stopPropagation(); 544 | e.dataTransfer.dropEffect = 'copy'; 545 | }); 546 | 547 | window.addEventListener('drop', function(e) 548 | { 549 | e.preventDefault(); 550 | e.stopPropagation(); 551 | 552 | var files = e.dataTransfer.files; 553 | 554 | for(file_id in files){ 555 | var file = files[file_id]; 556 | if(!file || !file.name || file.name.indexOf(".mar") == -1 && file.name.indexOf(".ins") == -1 && file.name.indexOf(".thm") == -1){ console.log("skipped",file); continue; } 557 | 558 | var path = file.path; 559 | var reader = new FileReader(); 560 | reader.onload = function(e){ 561 | var o = e.target.result; 562 | marabu.load(o,path); 563 | }; 564 | reader.readAsText(file); 565 | return; 566 | } 567 | }); 568 | 569 | window.onbeforeunload = function(e) 570 | { 571 | 572 | }; 573 | 574 | // Tools 575 | 576 | var parse_note = function(val) 577 | { 578 | val -= 87; 579 | if(val < 0){ val += 87; } 580 | var keyboard = ['C','D','E','F','G','A','B']; 581 | var notes = ['C-', 'C#', 'D-', 'D#', 'E-', 'F-', 'F#', 'G-', 'G#', 'A-', 'A#', 'B-']; 582 | var octave = Math.floor((val)/12); 583 | var key = notes[(val) % 12]; 584 | var key_sharp = key.substr(1,1) == "#" ? true : false; 585 | var key_note = key.substr(0,1); 586 | var offset = keyboard.indexOf(key_note); 587 | var distance = (keyboard.length*octave) + offset; 588 | return {id:val,octave:octave,sharp:key_sharp,note:key_note,offset:offset,distance:distance}; 589 | } 590 | 591 | var hex_to_int = function(hex) 592 | { 593 | var hex = hex.toLowerCase(); 594 | if(parseInt(hex) > 0){ return parseInt(hex); } 595 | if(hex == "a"){ return 10; } 596 | if(hex == "b"){ return 11; } 597 | if(hex == "c"){ return 12; } 598 | if(hex == "d"){ return 13; } 599 | if(hex == "e"){ return 14; } 600 | if(hex == "f"){ return 15; } 601 | return 0; 602 | } 603 | 604 | var prepend_to_length = function(str,length = 4,fill = "0") 605 | { 606 | var str = str+""; 607 | 608 | var offset = length - str.length; 609 | 610 | if(offset == 1){ return fill+str; } 611 | else if(offset == 2){ return fill+fill+str; } 612 | else if(offset == 3){ return fill+fill+fill+str; } 613 | else if(offset == 4){ return fill+fill+fill+fill+str; } 614 | 615 | return str 616 | } 617 | 618 | var to_hex_val = function(num) 619 | { 620 | if(num < 10){ return ""+num; } 621 | var l = ["a","b","c","d","e","f"]; 622 | return l[(num-10) % l.length]; 623 | } 624 | 625 | var to_hex = function(num, count = 1) 626 | { 627 | var s = num.toString(16).toUpperCase(); 628 | for (var i = 0; i < (count - s.length); ++i){ 629 | s = "0" + s; 630 | } 631 | return s; 632 | }; 633 | 634 | var clamp = function(val,min,max) 635 | { 636 | val = val < min ? min : val; 637 | val = val > max ? max : val; 638 | return val; 639 | } 640 | 641 | var calcSamplesPerRow = function(bpm) 642 | { 643 | return Math.round((60 * 44100 / 4) / bpm); 644 | }; -------------------------------------------------------------------------------- /desktop/sources/scripts/sequencer.js: -------------------------------------------------------------------------------- 1 | function Sequencer() 2 | { 3 | var MIN_LENGTH = 30; 4 | 5 | var target = this; 6 | 7 | this.el = null; 8 | this.scrollbar_el = null; 9 | this.position_el = null; 10 | this.follower = new Follower(); 11 | this.length = 0; 12 | 13 | this.start = function() 14 | { 15 | console.log("Started Sequencer"); 16 | 17 | this.el = document.getElementById("sequencer"); 18 | this.scrollbar_el = document.getElementById("scrollbar"); 19 | this.position_el = document.getElementById("position"); 20 | this.position_el.innerHTML = ""; this.position_el.className = "fl" 21 | 22 | this.build(MIN_LENGTH); 23 | 24 | this.el.addEventListener('wheel', function(e) 25 | { 26 | e.preventDefault(); 27 | marabu.sequencer.el.scrollTop += e.wheelDeltaY * -0.25; 28 | marabu.sequencer.scrollbar_el.style.height = 480 * (marabu.sequencer.el.scrollTop/(marabu.sequencer.el.scrollHeight * 0.75))+"px"; 29 | }, false); 30 | } 31 | 32 | this.build = function(target_length) 33 | { 34 | this.length = clamp(target_length,MIN_LENGTH,256); 35 | console.log("sequencer","build "+this.length) 36 | 37 | var table = document.getElementById("sequencer-table"); 38 | table.innerHTML = ""; 39 | var tr = document.createElement("tr"); 40 | for (var t = 0; t < this.length; t++) { 41 | var tr = document.createElement("tr"); 42 | tr.id = "spr"+t; 43 | for (var i = 0; i < 16; i++) { 44 | var td = document.createElement("td"); 45 | td.id = "sc" + i + "t" + t; 46 | td.textContent = "-"; 47 | td.addEventListener("mousedown", this.sequence_mouse_down, false); 48 | tr.appendChild(td); 49 | } 50 | table.appendChild(tr); 51 | } 52 | } 53 | 54 | this.sequence_mouse_down = function(e) 55 | { 56 | var c = e.target.id.substr(2) 57 | 58 | var i = parseInt(c.split("t")[0]); 59 | var r = parseInt(c.split("t")[1]); 60 | 61 | marabu.selection.instrument = i; 62 | marabu.selection.track = r; 63 | marabu.sequencer.follower.stop(); 64 | marabu.update(); 65 | } 66 | 67 | this.update = function() 68 | { 69 | var length = clamp(marabu.song.length,MIN_LENGTH,256) + 3; 70 | var active_pat = marabu.song.pattern_at(marabu.selection.instrument,marabu.selection.track); 71 | 72 | if(length != this.length){ 73 | this.build(length); 74 | } 75 | 76 | for (var t = 0; t < this.length; ++t) 77 | { 78 | var tr = document.getElementById("spr" + t); 79 | tr.className = t == marabu.selection.track ? "bl" : ""; 80 | 81 | for (var i = 0; i < 16; ++i) 82 | { 83 | var o = document.getElementById("sc" + i + "t" + t); 84 | var pat = marabu.song.pattern_at(i,t); 85 | // Default 86 | o.className = i == marabu.selection.instrument ? (pat && active_pat == pat ? "fh" : "fm") : (pat ? "fm" : "fl"); 87 | o.textContent = pat ? to_hex(pat) : (t % 8 == 0 && i == 0 ? ">" : "-"); 88 | // Selection 89 | if(marabu.loop.is_active && i >= marabu.loop.x && i < marabu.loop.x + marabu.loop.width+1 && t >= marabu.loop.y && t < marabu.loop.y + (marabu.loop.height+1)){ o.className = "b_inv f_inv"; } 90 | else if(t == marabu.selection.track && i == marabu.selection.instrument){ o.className = "fh"; } 91 | } 92 | } 93 | 94 | // Position 95 | var track_length = 100; 96 | var track_position = (marabu.selection.track * 32)+parseInt(marabu.selection.row); 97 | var track_time = parseInt(marabu.song.calculate_time(track_position/4)); 98 | var track_min = parseInt(track_time/60); 99 | var track_sec = track_time % 60; 100 | this.position_el.innerHTML = `${track_position}~${marabu.song.get_bpm()}+${marabu.selection.octave}${prepend(track_min,2)}:${prepend(track_sec,2)}`; 101 | } 102 | 103 | function prepend(str,length,char = "0") 104 | { 105 | var fill = ""; 106 | var i = 0; 107 | while(i < length - `${str}`.length){ 108 | fill += char; 109 | i += 1 110 | } 111 | return `${fill}${str}`; 112 | } 113 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/song.js: -------------------------------------------------------------------------------- 1 | var Song = function() 2 | { 3 | var MAX_SONG_ROWS = 32, 4 | MAX_PATTERNS = 32; 5 | 6 | // Resources 7 | var mSong = {}; 8 | var mAudio = null; 9 | var mAudioTimer = new CAudioTimer(); 10 | var mPlayer = new CPlayer(); 11 | var mJammer = new CJammer(); 12 | 13 | this.mAudio = function(){ return mAudio; } 14 | this.mAudio_timer = function(){ return mAudioTimer; } 15 | 16 | this.mJammer = mJammer; 17 | 18 | var mPreload = []; 19 | 20 | //-------------------------------------------------------------------------- 21 | // Song import/export functions 22 | //-------------------------------------------------------------------------- 23 | 24 | this.player = function() 25 | { 26 | return mPlayer; 27 | } 28 | 29 | this.get_bpm = function() 30 | { 31 | return Math.round((60 * 44100 / 4) / mSong.rowLen); 32 | }; 33 | 34 | this.update_bpm = function(bpm) 35 | { 36 | console.log(bpm) 37 | mSong.rowLen = calcSamplesPerRow(bpm); 38 | mJammer.updateRowLen(mSong.rowLen); 39 | } 40 | 41 | this.update_rpp = function(rpp) 42 | { 43 | setPatternLength(rpp); 44 | marabu.update(); 45 | } 46 | 47 | this.play_note = function(note) 48 | { 49 | mJammer.addNote(note+87); 50 | } 51 | 52 | this.song = function() 53 | { 54 | return mSong; 55 | } 56 | 57 | this.to_string = function() 58 | { 59 | return JSON.stringify(this.song()); 60 | } 61 | 62 | this.replace_song = function(new_song) 63 | { 64 | if(!new_song.name){ return; } 65 | stopAudio(); 66 | 67 | mSong = new_song; 68 | 69 | this.update_bpm(mSong.bpm ? mSong.bpm : 120); 70 | this.update_rpp(32); 71 | 72 | // Inject names 73 | for(id in this.song().songData){ 74 | var ins = this.song().songData[id]; 75 | ins.name = ins.name && ins.name.length > 3 ? ins.name.substr(0,4) : `INS${to_hex_val(id).toUpperCase()}` 76 | } 77 | 78 | updateSongRanges(); 79 | } 80 | 81 | this.mJammer_update = function() 82 | { 83 | return mJammer.updateInstr(this.instrument().i); 84 | } 85 | 86 | this.instrument = function(id = marabu.selection.instrument) 87 | { 88 | return this.song().songData[id]; 89 | } 90 | 91 | // 92 | 93 | this.pattern_at = function(i,t) 94 | { 95 | return this.song().songData[i].p[t]; 96 | } 97 | 98 | this.inject_pattern_at = function(i,t,v) 99 | { 100 | this.song().songData[i].p[t] = v; 101 | this.update_ranges(); 102 | marabu.update(); 103 | } 104 | 105 | this.note_at = function(i,t,n) 106 | { 107 | var c = this.pattern_at(i,t)-1; if(c == -1){ return; } 108 | return this.song().songData[i].c[c].n[n]; 109 | } 110 | 111 | this.inject_note_at = function(i,t,n,v) 112 | { 113 | var c = this.pattern_at(i,t)-1; if(c == -1){ return; } 114 | this.song().songData[i].c[c].n[n] = (v == -87 ? 0 : clamp(v,36,107)+87); 115 | } 116 | 117 | this.effect_at = function(i,t,f) 118 | { 119 | var c = this.pattern_at(i,t)-1; if(c == -1){ return; } 120 | return this.song().songData[i].c[c].f[f]; 121 | } 122 | 123 | this.effect_value_at = function(i,t,f) 124 | { 125 | var c = this.pattern_at(i,t)-1; if(c == -1){ return; } 126 | return this.song().songData[i].c[c].f[f+32]; 127 | } 128 | 129 | this.inject_effect_at = function(i,t,f,cmd,val) 130 | { 131 | if(!cmd || val === undefined){ return; } 132 | var c = this.pattern_at(i,t)-1; if(c == -1){ return; } 133 | this.song().songData[i].c[c].f[f] = cmd; 134 | this.song().songData[i].c[c].f[f+32] = val; 135 | } 136 | 137 | this.erase_effect_at = function(i,t,f) 138 | { 139 | var c = this.pattern_at(i,t)-1; if(c == -1){ return; } 140 | this.song().songData[i].c[c].f[f] = 0; 141 | this.song().songData[i].c[c].f[f+32] = 0; 142 | } 143 | 144 | this.control_at = function(i,s) 145 | { 146 | return this.song().songData[i].i[s]; 147 | } 148 | 149 | this.inject_control = function(i,s,v) 150 | { 151 | this.song().songData[i].i[s] = v; 152 | this.mJammer_update(); 153 | } 154 | 155 | var setPatternLength = function (length) 156 | { 157 | if (mSong.patternLen === length) 158 | return; 159 | 160 | stopAudio(); 161 | 162 | var i, j, k, col, notes, fx; 163 | for (i = 0; i < 8; i++) { 164 | for (j = 0; j < MAX_PATTERNS; j++) { 165 | col = mSong.songData[i].c[j]; 166 | notes = []; 167 | fx = []; 168 | for (k = 0; k < 4 * length; k++) 169 | notes[k] = 0; 170 | for (k = 0; k < 2 * length; k++) 171 | fx[k] = 0; 172 | for (k = 0; k < Math.min(mSong.patternLen, length); k++) { 173 | notes[k] = col.n[k]; 174 | notes[k + length] = col.n[k + mSong.patternLen]; 175 | notes[k + 2 * length] = col.n[k + 2 * mSong.patternLen]; 176 | notes[k + 3 * length] = col.n[k + 3 * mSong.patternLen]; 177 | fx[k] = col.f[k]; 178 | fx[k + length] = col.f[k + mSong.patternLen]; 179 | } 180 | col.n = notes; 181 | col.f = fx; 182 | } 183 | } 184 | 185 | // Update pattern length 186 | mSong.patternLen = length; 187 | }; 188 | 189 | this.update_ranges = function() 190 | { 191 | updateSongRanges(); 192 | } 193 | 194 | var updateSongRanges = function () 195 | { 196 | var i, j, emptyRow; 197 | 198 | // Determine the last song pattern 199 | mSong.endPattern = marabu.sequencer.length + 1; 200 | for (i = marabu.sequencer.length; i >= 0; --i) { 201 | emptyRow = true; 202 | for (j = 0; j < 16; ++j) { 203 | if (mSong.songData[j].p[i] > 0) { 204 | emptyRow = false; 205 | break; 206 | } 207 | } 208 | if (!emptyRow) break; 209 | mSong.endPattern--; 210 | } 211 | }; 212 | 213 | this.export_wav = function(opts = null) 214 | { 215 | updateSongRanges(); 216 | 217 | var doneFun = function(wave) 218 | { 219 | dialog.showSaveDialog({filters:[{name:'Audio File',extensions:['wav']}]},(fileName) => { 220 | if (fileName === undefined){ return; } 221 | fs.writeFile(`${fileName.substr(-4,4) != ".wav" ? fileName+".wav" : fileName}`, new Buffer(wave), (err) => { 222 | if(err){ alert("An error ocurred creating the file "+ err.message); return; } 223 | }); 224 | }); 225 | }; 226 | generateAudio(doneFun,opts); 227 | }; 228 | 229 | this.calculate_time = function(pos = (8 * (marabu.song.length+1))) 230 | { 231 | var bpm = parseFloat(marabu.song.song().bpm); 232 | var beats = pos; 233 | var minutes = beats/bpm; 234 | var seconds = minutes * 60; 235 | 236 | return seconds; 237 | } 238 | 239 | var generateAudio = function(doneFun, opts, override_song = null) 240 | { 241 | var display_progress_el = document.getElementById("fxr31"); 242 | var song = mSong; 243 | var render_time = marabu.song.calculate_time(); 244 | var minutes = Math.floor(render_time/60.0); 245 | var seconds = Math.floor(render_time % 60); 246 | 247 | var d1 = new Date(); 248 | mPlayer = new CPlayer(); 249 | mPlayer.generate(song, opts, function(progress){ 250 | if(progress >= 1){ 251 | var wave = mPlayer.createWave(); 252 | var d2 = new Date(); 253 | doneFun(wave); 254 | display_progress_el.className = "fl"; 255 | } 256 | else{ 257 | display_progress_el.className = "b_inv f_inv"; 258 | display_progress_el.textContent = prepend_to_length(parseInt(progress * 100),4,"0"); 259 | } 260 | }); 261 | }; 262 | 263 | var stopAudio = function () 264 | { 265 | marabu.sequencer.follower.stop(); 266 | if(mAudio) { 267 | mAudio.pause(); 268 | mAudioTimer.reset(); 269 | } 270 | }; 271 | 272 | this.stop_song = function() 273 | { 274 | stopAudio(); 275 | marabu.selection.row = 0; 276 | marabu.update(); 277 | } 278 | 279 | this.start_over = function() 280 | { 281 | this.currentTime = 0; 282 | this.play(); 283 | } 284 | 285 | this.play_song = function() 286 | { 287 | mAudio.removeEventListener('ended', marabu.song.start_over, false); 288 | 289 | this.update(); 290 | this.update_bpm(this.song().bpm); 291 | this.update_rpp(32); 292 | 293 | stopAudio(); 294 | updateSongRanges(); 295 | 296 | var doneFun = function(wave) 297 | { 298 | console.log("playing..") 299 | marabu.sequencer.follower.start(); 300 | mAudio.src = URL.createObjectURL(new Blob([wave], {type: "audio/wav"})); 301 | mAudioTimer.reset(); 302 | mAudio.play(); 303 | }; 304 | generateAudio(doneFun); 305 | } 306 | 307 | this.play_loop = function(opts,looped_song) 308 | { 309 | mAudio.addEventListener('ended', marabu.song.start_over, false); 310 | 311 | this.update_bpm(this.song().bpm); 312 | this.update_rpp(32); 313 | 314 | this.stop_song(); 315 | updateSongRanges(); 316 | 317 | var offset = opts.firstRow; 318 | 319 | var doneFun = function(wave) 320 | { 321 | console.log("playing..",offset) 322 | marabu.sequencer.follower.start(offset); 323 | mAudio.src = URL.createObjectURL(new Blob([wave], {type: "audio/wav"})); 324 | mAudioTimer.reset(); 325 | mAudio.play(); 326 | }; 327 | generateAudio(doneFun,opts,looped_song); 328 | } 329 | 330 | this.length = 0; 331 | this.is_looping = false; 332 | 333 | this.update = function() 334 | { 335 | this.validate(); 336 | this.update_length(); 337 | } 338 | 339 | this.validate = function() 340 | { 341 | for(var i = 0; i < 16; ++i) { 342 | var offset = (this.length+34) - mSong.songData[i].p.length; 343 | if(offset < 0){ continue; } 344 | // Fill 345 | for(var fill = 0; fill < offset; ++fill){ 346 | mSong.songData[i].p.push(0); 347 | } 348 | } 349 | } 350 | 351 | this.update_length = function() 352 | { 353 | var l = 0; 354 | for(var i = 0; i < 16; ++i) { 355 | for(var p = 0; p < mSong.songData[i].p.length; ++p){ 356 | if(mSong.songData[i].p[p] > 0 && p > l){ l = p; } 357 | } 358 | } 359 | this.length = l; 360 | } 361 | 362 | //-------------------------------------------------------------------------- 363 | // Initialization 364 | //-------------------------------------------------------------------------- 365 | 366 | this.init = function () 367 | { 368 | var i, j, o; 369 | 370 | // Create audio element, and always play the audio as soon as it's ready 371 | mAudio = new Audio(); 372 | mAudioTimer.setAudioElement(mAudio); 373 | mAudio.addEventListener("canplay", function (){ this.play(); }, true); 374 | 375 | mSong = new Track(); 376 | 377 | mJammer.start(); 378 | mJammer.updateRowLen(mSong.rowLen); 379 | }; 380 | }; 381 | 382 | var CAudioTimer = function () 383 | { 384 | var mAudioElement = null; 385 | var mStartT = 0; 386 | var mErrHist = [0, 0, 0, 0, 0, 0]; 387 | var mErrHistPos = 0; 388 | 389 | this.setAudioElement = function (audioElement) 390 | { 391 | mAudioElement = audioElement; 392 | } 393 | 394 | this.currentTime = function () 395 | { 396 | if (!mAudioElement) 397 | return 0; 398 | 399 | // Calculate current time according to Date() 400 | var t = (new Date()).getTime() * 0.001; 401 | var currentTime = t - mStartT; 402 | 403 | // Get current time according to the audio element 404 | var audioCurrentTime = mAudioElement.currentTime; 405 | 406 | // Check if we are off by too much - in which case we will use the time 407 | // from the audio element 408 | var err = audioCurrentTime - currentTime; 409 | if (audioCurrentTime < 0.01 || err > 0.2 || err < -0.2) { 410 | currentTime = audioCurrentTime; 411 | mStartT = t - currentTime; 412 | for (var i = 0; i < mErrHist.length; i++) 413 | mErrHist[i] = 0; 414 | } 415 | 416 | // Error compensation (this should fix the problem when we're constantly 417 | // slightly off) 418 | var comp = 0; 419 | for (var i = 0; i < mErrHist.length; i++) 420 | comp += mErrHist[i]; 421 | comp /= mErrHist.length; 422 | mErrHist[mErrHistPos] = err; 423 | mErrHistPos = (mErrHistPos + 1) % mErrHist.length; 424 | 425 | return currentTime + comp; 426 | }; 427 | 428 | this.reset = function () 429 | { 430 | mStartT = (new Date()).getTime() * 0.001; 431 | for (var i = 0; i < mErrHist.length; i++){ 432 | mErrHist[i] = 0; 433 | } 434 | }; 435 | }; -------------------------------------------------------------------------------- /desktop/sources/scripts/track.js: -------------------------------------------------------------------------------- 1 | function Track() 2 | { 3 | var MAX_SONG_ROWS = 32, MAX_PATTERNS = 32, MAX_INSTRUMENTS = 16; 4 | 5 | var song = {}, i, j, k, instr, col; 6 | 7 | // Settings 8 | song.artist = "Unknown"; 9 | song.name = "Untitled"; 10 | song.bpm = 120; 11 | 12 | song.theme = new Theme().default; 13 | 14 | // Automated 15 | song.rowLen = calcSamplesPerRow(song.bpm); 16 | song.endPattern = 2; 17 | song.patternLen = 32; 18 | 19 | // All 8 instruments 20 | song.songData = []; 21 | for (i = 0; i < MAX_INSTRUMENTS; i++){ 22 | instr = {}; 23 | instr.i = [15,67,111,0,0,100,111,6,0,0,0,0,153,0,20,0,0,8,64,2,205,90,0,63,127,0,0,0,190,0,0]; 24 | 25 | // Sequence 26 | instr.p = []; 27 | for (j = 0; j < MAX_SONG_ROWS; j++) 28 | instr.p[j] = 0; 29 | 30 | // Patterns 31 | instr.c = []; 32 | for (j = 0; j < MAX_PATTERNS; j++) 33 | { 34 | col = {}; 35 | col.n = []; 36 | for (k = 0; k < song.patternLen * 2; k++) 37 | col.n[k] = 0; 38 | col.f = []; 39 | for (k = 0; k < song.patternLen * 2; k++) 40 | col.f[k] = 0; 41 | instr.c[j] = col; 42 | } 43 | song.songData[i] = instr; 44 | } 45 | 46 | // Make a first empty pattern 47 | song.songData[0].p[0] = 1; 48 | 49 | song.songData[0].name = "SYN1" 50 | song.songData[1].name = "SYN2" 51 | song.songData[2].name = "PAD1" 52 | song.songData[3].name = "PAD2" 53 | 54 | song.songData[4].name = "IDM1" 55 | song.songData[5].name = "IDM2" 56 | song.songData[6].name = "TXT1" 57 | song.songData[7].name = "TXT2" 58 | 59 | song.songData[8].name = "SNAR" 60 | song.songData[9].name = "CLAP" 61 | song.songData[10].name = "BASS" 62 | song.songData[11].name = "KICK" 63 | 64 | song.songData[12].name = "INST" 65 | song.songData[13].name = "INST" 66 | song.songData[14].name = "INST" 67 | song.songData[15].name = "INST" 68 | 69 | return song; 70 | }; -------------------------------------------------------------------------------- /desktop/sources/scripts/ui/choice.js: -------------------------------------------------------------------------------- 1 | function UI_Choice(data) 2 | { 3 | this.family = null; 4 | this.id = data.id; 5 | this.name = data.name; 6 | this.choices = data.choices; 7 | 8 | this.control = 0; 9 | 10 | this.el = document.createElement("div"); 11 | this.name_el = document.createElement("t"); 12 | this.value_el = document.createElement("t"); 13 | 14 | this.value = 0; 15 | 16 | var target = this; 17 | 18 | this.install = function(parent) 19 | { 20 | this.el.className = "control choice"; 21 | // Name Span 22 | this.name_el.className = "name"; 23 | this.name_el.innerHTML = this.name; 24 | 25 | this.value_el.textContent = this.min+"/"+this.max; 26 | this.value_el.className = "value"; 27 | 28 | this.el.appendChild(this.name_el); 29 | this.el.appendChild(this.value_el); 30 | 31 | this.el.addEventListener("mousedown", this.mouse_down, false); 32 | 33 | parent.appendChild(this.el); 34 | this.storage = marabu.instrument.get_storage(this.family+"_"+this.id); 35 | } 36 | 37 | this.mod = function(v) 38 | { 39 | this.value += v > 0 ? 1 : -1; 40 | this.value = this.value % this.choices.length; 41 | this.value = this.value < 0 ? this.choices.length-1 : this.value; 42 | this.update(); 43 | } 44 | 45 | this.override = function(v) 46 | { 47 | if(v == null){ console.log("Missing control value",this.family+"."+this.id); return;} 48 | 49 | var v = v % this.choices.length; 50 | this.value = v; 51 | this.update(); 52 | } 53 | 54 | this.save = function() 55 | { 56 | var storage_id = marabu.instrument.get_storage(this.family+"_"+this.id); 57 | marabu.song.inject_control(marabu.selection.instrument,storage_id,this.value % this.choices.length); 58 | } 59 | 60 | this.update = function() 61 | { 62 | var target = this.choices[this.value % this.choices.length]; 63 | this.value_el.textContent = target; 64 | 65 | this.el.className = marabu.selection.control == this.control ? "control choice bl" : "control choice "; 66 | this.name_el.className = marabu.selection.control == this.control ? "name fh" : "name fm"; 67 | 68 | // Keyframes 69 | if(this.has_keyframes()){ 70 | this.name_el.className = "name b_inv f_inv"; 71 | } 72 | } 73 | 74 | this.has_keyframes = function() 75 | { 76 | var i = marabu.selection.instrument; 77 | var t = 0; 78 | var f = this.storage; 79 | while(t <= marabu.selection.track){ 80 | var r = 0; 81 | while(r < 32){ 82 | var cmd = marabu.song.effect_at(i,t,r) 83 | if(cmd == f+1){ 84 | return marabu.song.effect_value_at(i,t,r); 85 | } 86 | r += 1; 87 | } 88 | t += 1; 89 | } 90 | return null; 91 | } 92 | 93 | this.mouse_down = function(e) 94 | { 95 | marabu.selection.control = target.control; 96 | marabu.update(); 97 | } 98 | } -------------------------------------------------------------------------------- /desktop/sources/scripts/ui/slider.js: -------------------------------------------------------------------------------- 1 | function UI_Slider(data) 2 | { 3 | var app = marabu; 4 | var self = this; 5 | 6 | this.family = null; 7 | this.id = data.id; 8 | this.storage = 0; 9 | 10 | this.name = data.name; 11 | this.min = data.min; 12 | this.max = data.max; 13 | 14 | this.control = 0; 15 | this.center = data.center; 16 | this.percent = data.percent; 17 | 18 | this.value = this.min; 19 | 20 | this.el = document.createElement("div"); 21 | this.name_el = document.createElement("t"); 22 | this.value_el = document.createElement("t"); 23 | this.slide_el = document.createElement("div"); this.slide_el.className = "slide"; 24 | 25 | this.install = function(parent) 26 | { 27 | this.el.className = `control slider ${this.center ? 'center' : ''}`; 28 | 29 | // Name Span 30 | this.name_el.className = "name"; 31 | this.name_el.innerHTML = this.name; 32 | 33 | // Value Input 34 | this.value_el.className = "value"; 35 | this.value_el.textContent = "--"; 36 | 37 | this.el.appendChild(this.name_el); 38 | this.el.appendChild(this.slide_el); 39 | this.el.appendChild(this.value_el); 40 | 41 | this.el.addEventListener("mousedown", this.mouse_down, false); 42 | 43 | parent.appendChild(this.el); 44 | this.storage = marabu.instrument.get_storage(this.family+"_"+this.id); 45 | } 46 | 47 | this.mod = function(v,relative = false) 48 | { 49 | if(relative && this.max > 128){ v *= 10; } 50 | if(this.max <= 64 && (v > 1 || v < 1) && Math.abs(v) != 1){ v = v/10;} 51 | this.value += parseInt(v); 52 | this.value = clamp(this.value,this.min,this.max); 53 | this.update(); 54 | } 55 | 56 | this.override = function(v) 57 | { 58 | if(v == null){ console.log("Missing control value",this.family+"."+this.id); return;} 59 | 60 | this.value = parseInt(v); 61 | this.value = clamp(this.value,this.min,this.max); 62 | this.update(); 63 | } 64 | 65 | this.save = function() 66 | { 67 | marabu.song.inject_control(marabu.selection.instrument,this.storage,this.value); 68 | } 69 | 70 | this.update = function() 71 | { 72 | var val = parseInt(this.value) - parseInt(this.min); 73 | var over = parseFloat(this.max) - parseInt(this.min); 74 | var perc = val/parseFloat(over); 75 | var val_mod = this.center ? this.value - Math.floor(this.max/2) : this.value 76 | var keyframe = this.last_keyframe(); 77 | 78 | var c = "" 79 | c = 'slider control ' 80 | c += app.selection.control == this.control ? 'selected ' : ''; 81 | c += keyframe ? 'keyframed ' : ''; 82 | c += this.value == this.min ? 'min ' : '' 83 | c += this.value == this.max ? 'max ' : '' 84 | 85 | this.el.className = c; 86 | this.value_el.textContent = app.selection.control != this.control && keyframe ? '$'+keyframe : (keyframe ? '$' : '')+val_mod; 87 | this.update_display(perc); 88 | } 89 | 90 | this.last_keyframe = function() 91 | { 92 | var i = app.selection.instrument; 93 | var t = app.selection.track; 94 | var f = this.storage; 95 | while(t >= 0){ 96 | var r = app.selection.track == t ? app.selection.row : 32; 97 | while(r >= 0){ 98 | var cmd = app.song.effect_at(i,t,r) 99 | if(cmd == f+1){ 100 | return app.song.effect_value_at(i,t,r); 101 | } 102 | r -= 1; 103 | } 104 | t -= 1; 105 | } 106 | return null; 107 | } 108 | 109 | this.update_display = function(perc) 110 | { 111 | var html = "" 112 | var c = 0; 113 | while(c < perc*80){ 114 | html += "-" 115 | c += 10; 116 | } 117 | html = `${html}` 118 | while(c < 80){ 119 | html += "-" 120 | c += 10 121 | } 122 | this.slide_el.innerHTML = `${html}` 123 | } 124 | 125 | this.mouse_down = function(e) 126 | { 127 | app.selection.control = self.control; 128 | app.update(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /desktop/sources/scripts/ui/uv.js: -------------------------------------------------------------------------------- 1 | function UI_Uv() 2 | { 3 | this.el = document.createElement("div"); 4 | 5 | this.vol_el = document.createElement("div"); 6 | this.vol_name_el = document.createElement("t"); 7 | this.vol_canvas = document.createElement("canvas"); 8 | this.env_el = document.createElement("div"); 9 | this.env_name_el = document.createElement("t"); 10 | this.env_canvas = document.createElement("canvas"); 11 | this.wav_el = document.createElement("div"); 12 | this.wav_name_el = document.createElement("t"); 13 | this.wav_canvas = document.createElement("canvas"); 14 | 15 | this.size = {width:45,height:15}; 16 | 17 | var mFollowerLastVULeft = 0; 18 | var mFollowerLastVURight = 0; 19 | 20 | this.install = function(parent) 21 | { 22 | this.el.className = "control uv"; 23 | 24 | this.el.appendChild(this.env_el); 25 | this.el.appendChild(this.vol_el); 26 | this.el.appendChild(this.wav_el); 27 | 28 | this.env_el.appendChild(this.env_name_el); 29 | this.env_el.appendChild(this.env_canvas); 30 | 31 | this.vol_el.appendChild(this.vol_name_el); 32 | this.vol_el.appendChild(this.vol_canvas); 33 | 34 | this.wav_el.appendChild(this.wav_name_el); 35 | this.wav_el.appendChild(this.wav_canvas); 36 | 37 | this.vol_el.style.height = "15px"; 38 | this.env_el.style.height = "15px"; 39 | this.wav_el.style.height = "15px"; 40 | 41 | this.vol_el.style.marginTop = "-15px"; 42 | 43 | this.vol_name_el.className = "name fl"; 44 | this.vol_name_el.innerHTML = "VOL"; 45 | 46 | this.vol_canvas.style.width = this.size.width+"px"; 47 | this.vol_canvas.style.height = this.size.height+"px"; 48 | this.vol_canvas.width = this.size.width * 2; 49 | this.vol_canvas.height = this.size.height * 2; 50 | 51 | this.env_name_el.className = "name fl"; 52 | this.env_name_el.innerHTML = ""; 53 | 54 | this.env_canvas.style.width = this.size.width+"px"; 55 | this.env_canvas.style.height = this.size.height+"px"; 56 | this.env_canvas.width = this.size.width * 2; 57 | this.env_canvas.height = this.size.height * 2; 58 | 59 | this.wav_name_el.className = "name fl lh15"; 60 | this.wav_name_el.innerHTML = "WAV"; 61 | 62 | this.wav_canvas.style.width = this.size.width+"px"; 63 | this.wav_canvas.style.height = this.size.height+"px"; 64 | this.wav_canvas.width = this.size.width * 2; 65 | this.wav_canvas.height = this.size.height * 2; 66 | 67 | this.el.style.width = "120px"; 68 | 69 | var vol_ctx = this.vol_canvas.getContext("2d"); 70 | var env_ctx = this.env_canvas.getContext("2d"); 71 | var wav_ctx = this.env_canvas.getContext("2d"); 72 | 73 | vol_ctx.setLineDash([2, 2]); 74 | vol_ctx.lineWidth = 2; 75 | env_ctx.setLineDash([2, 2]); 76 | env_ctx.lineWidth = 2; 77 | wav_ctx.setLineDash([2, 2]); 78 | wav_ctx.lineWidth = 2; 79 | 80 | this.clear(); 81 | 82 | parent.appendChild(this.el); 83 | } 84 | 85 | var getSamplesSinceNote = function (t, chan) 86 | { 87 | var mSong = marabu.song.song(); 88 | var nFloat = t * 44100 / mSong.rowLen; 89 | var n = Math.floor(nFloat); 90 | var seqPos0 = Math.floor(n / mSong.patternLen); 91 | var patPos0 = n % mSong.patternLen; 92 | for (var k = 0; k < mSong.patternLen; ++k) { 93 | var seqPos = seqPos0; 94 | var patPos = patPos0 - k; 95 | while (patPos < 0) { 96 | --seqPos; 97 | patPos += mSong.patternLen; 98 | } 99 | var pat = mSong.songData[chan].p[seqPos] - 1; 100 | for (var patCol = 0; patCol < 4; patCol++) { 101 | if (pat >= 0 && mSong.songData[chan].c[pat].n[patPos+patCol*mSong.patternLen] > 0) 102 | return (k + (nFloat - n)) * mSong.rowLen; 103 | } 104 | } 105 | return -1; 106 | }; 107 | 108 | this.clear = function() 109 | { 110 | var vol_ctx = this.vol_canvas.getContext("2d"); 111 | var env_ctx = this.env_canvas.getContext("2d"); 112 | var wav_ctx = this.env_canvas.getContext("2d"); 113 | 114 | vol_ctx.clearRect(0, 0, this.size.width * 2, this.size.height * 2); 115 | env_ctx.clearRect(0, 0, this.size.width * 2, this.size.height * 2); 116 | wav_ctx.clearRect(0, 0, this.size.width * 2, this.size.height * 2); 117 | 118 | env_ctx.strokeStyle = marabu.theme.active.f_low; 119 | env_ctx.beginPath(); 120 | env_ctx.moveTo(0,15); 121 | env_ctx.lineTo(this.size.width * 2,15); 122 | env_ctx.stroke(); 123 | } 124 | 125 | this.override = function() 126 | { 127 | 128 | } 129 | 130 | this.update = function() 131 | { 132 | this.draw(-1); 133 | } 134 | 135 | this.draw = function(t) 136 | { 137 | if(t <= 0){ return; } 138 | 139 | this.clear(); 140 | 141 | var pl = 0, pr = 0; 142 | var vol_ctx = this.vol_canvas.getContext("2d"); 143 | var env_ctx = this.env_canvas.getContext("2d"); 144 | var wav_ctx = this.wav_canvas.getContext("2d"); 145 | 146 | // Get the waveform 147 | var wave = marabu.song.player().getData(t, 1000); 148 | 149 | wav_ctx.clearRect(0, 0, this.size.width * 2, this.size.height * 2); 150 | wav_ctx.strokeStyle = marabu.theme.active.f_high; 151 | wav_ctx.beginPath(); 152 | wav_ctx.setLineDash([2, 2]); 153 | wav_ctx.lineWidth = 2; 154 | wav_ctx.moveTo(0,this.size.height); 155 | 156 | // Calculate volume 157 | var i, l, r; 158 | var sl = 0, sr = 0, l_old = 0, r_old = 0; 159 | for (i = 1; i < wave.length; i += 2) 160 | { 161 | this.clear(); 162 | 163 | l = wave[i-1]; 164 | r = wave[i]; 165 | 166 | // Band-pass filter (low-pass + high-pass) 167 | sl = 0.8 * l + 0.1 * sl - 0.3 * l_old; 168 | sr = 0.8 * r + 0.1 * sr - 0.3 * r_old; 169 | l_old = l; 170 | r_old = r; 171 | 172 | // Sum of squares 173 | pl += sl * sl; 174 | pr += sr * sr; 175 | 176 | var last_x = 0; 177 | var x = parseInt(i/20); 178 | 179 | if(parseInt(x) != parseInt(last_x) && x < this.size.width * 2){ 180 | var mod = ((l+r)/2.0) * 20; 181 | var y = this.size.height + mod; 182 | y = clamp(y,0,this.size.height*2); 183 | wav_ctx.lineTo(x, y); 184 | last_x = x; 185 | } 186 | } 187 | wav_ctx.stroke(); 188 | 189 | // Low-pass filtered mean power (RMS) 190 | pl = Math.sqrt(pl / wave.length) * 0.2 + mFollowerLastVULeft * 0.8; 191 | pr = Math.sqrt(pr / wave.length) * 0.2 + mFollowerLastVURight * 0.8; 192 | mFollowerLastVULeft = pl; 193 | mFollowerLastVURight = pr; 194 | 195 | var index = ((pl+pr)/2) * 4.0; 196 | 197 | vol_ctx.strokeStyle = marabu.theme.active.f_high; 198 | vol_ctx.beginPath(); 199 | vol_ctx.moveTo(0,15); 200 | vol_ctx.lineTo((this.size.width * 2) * index,15); 201 | vol_ctx.stroke(); 202 | 203 | for (i = 0; i < 16; ++i) 204 | { 205 | var env_a = marabu.song.song().songData[i].i[10], 206 | env_s = marabu.song.song().songData[i].i[11], 207 | env_r = marabu.song.song().songData[i].i[12]; 208 | env_a = env_a * env_a * 4; 209 | env_r = env_s * env_s * 4 + env_r * env_r * 4; 210 | var env_tot = env_a + env_r; 211 | if (env_tot < 10000) 212 | { 213 | env_tot = 10000; 214 | env_r = env_tot - env_a; 215 | } 216 | 217 | var numSamp = getSamplesSinceNote(t, i); 218 | if (numSamp >= 0 && numSamp < env_tot) 219 | { 220 | var alpha = (numSamp < env_a) ? alpha = numSamp / env_a : 1 - (numSamp - env_a) / env_r; 221 | env_ctx.strokeStyle = marabu.theme.active.f_med; 222 | env_ctx.beginPath(); 223 | env_ctx.moveTo(0,15); 224 | env_ctx.lineTo((this.size.width * 2) * alpha,15); 225 | env_ctx.stroke(); 226 | } 227 | } 228 | 229 | } 230 | 231 | function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } 232 | } --------------------------------------------------------------------------------