├── .gitignore ├── .gitattributes ├── img ├── icon.png ├── play.svg ├── stop.svg ├── stopg.svg ├── range3marks.svg ├── range4marks.svg ├── range5marks.svg ├── range-short.svg ├── range-volume.svg ├── range8marks.svg ├── range10marks.svg ├── range12marks.svg ├── range8marks2.svg ├── adsr.svg ├── fullscreen.svg ├── noise.svg ├── effect.svg ├── modulation-osc.svg ├── lfo.svg ├── oscillator.svg ├── gear.svg ├── modulation-amp.svg ├── modulation-filt.svg ├── filter.svg └── icon.svg ├── refcards ├── refcard-synth.png ├── refcard-controls.png └── refcard-overview.png ├── data └── tracklist.json ├── manifest.json ├── lib ├── bufferToWave.js └── midi-writer.js ├── README.md ├── css ├── table-arrange.css ├── elements.css └── table-pattern.css └── js ├── scheduler-ui.js ├── defaults.js ├── reorder-menu.js ├── main.js ├── mid-synth.js ├── pattern.js ├── alerts.js ├── misc.js ├── scheduler.js ├── synth-ui.js ├── synth-param-apply.js ├── waveform-editor.js ├── arrange-ui.js ├── song-object.js └── synth-presets.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib/Tone.js.map 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.svg binary 2 | data/tracks/*.json binary 3 | -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Valent-in/pulseq/HEAD/img/icon.png -------------------------------------------------------------------------------- /refcards/refcard-synth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Valent-in/pulseq/HEAD/refcards/refcard-synth.png -------------------------------------------------------------------------------- /refcards/refcard-controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Valent-in/pulseq/HEAD/refcards/refcard-controls.png -------------------------------------------------------------------------------- /refcards/refcard-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Valent-in/pulseq/HEAD/refcards/refcard-overview.png -------------------------------------------------------------------------------- /img/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/stopg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/tracklist.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name": "Demo 1", "file": "demo1.json"}, 3 | {"name": "Chiptuney", "file": "chiptuney.json"}, 4 | {"name": "Tech_0", "file": "tech0.json"} 5 | ] -------------------------------------------------------------------------------- /img/range3marks.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/range4marks.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/range5marks.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/range-short.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PulseQueue", 3 | "display": "standalone", 4 | "background_color": "#111111", 5 | "theme_color": "#111111", 6 | "icons": [ 7 | { 8 | "src": "img/icon.png", 9 | "sizes": "128x128" 10 | }, 11 | { 12 | "src": "img/icon.svg", 13 | "sizes": "any" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /img/range-volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/range8marks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /img/range10marks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /img/range12marks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /img/range8marks2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/adsr.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/bufferToWave.js: -------------------------------------------------------------------------------- 1 | // https://www.russellgood.com/how-to-convert-audiobuffer-to-audio-file/ 2 | 3 | function bufferToWave(abuffer, len) { 4 | var numOfChan = abuffer.numberOfChannels, 5 | length = len * numOfChan * 2 + 44, 6 | buffer = new ArrayBuffer(length), 7 | view = new DataView(buffer), 8 | channels = [], i, sample, 9 | offset = 0, 10 | pos = 0; 11 | 12 | setUint32(0x46464952); 13 | setUint32(length - 8); 14 | setUint32(0x45564157); 15 | 16 | setUint32(0x20746d66); 17 | setUint32(16); 18 | setUint16(1); 19 | setUint16(numOfChan); 20 | setUint32(abuffer.sampleRate); 21 | setUint32(abuffer.sampleRate * 2 * numOfChan); 22 | setUint16(numOfChan * 2); 23 | setUint16(16); 24 | 25 | setUint32(0x61746164); 26 | setUint32(length - pos - 4); 27 | 28 | for (i = 0; i < abuffer.numberOfChannels; i++) 29 | channels.push(abuffer.getChannelData(i)); 30 | 31 | while (pos < length) { 32 | for (i = 0; i < numOfChan; i++) { 33 | sample = Math.max(-1, Math.min(1, channels[i][offset])); 34 | sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; 35 | view.setInt16(pos, sample, true); 36 | pos += 2; 37 | } 38 | offset++ 39 | } 40 | 41 | return new Blob([buffer], { type: "audio/wav" }); 42 | 43 | function setUint16(data) { 44 | view.setUint16(pos, data, true); 45 | pos += 2; 46 | } 47 | 48 | function setUint32(data) { 49 | view.setUint32(pos, data, true); 50 | pos += 4; 51 | } 52 | } -------------------------------------------------------------------------------- /img/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 31 | 35 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /img/noise.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 21 | 26 | 27 | 28 | 30 | 31 | 33 | image/svg+xml 34 | 36 | 37 | 38 | 39 | 40 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /img/effect.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 31 | 35 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /img/modulation-osc.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 21 | 26 | 27 | 28 | 30 | 31 | 33 | image/svg+xml 34 | 36 | 37 | 38 | 39 | 40 | 43 | 47 | 51 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PulseQueue 2 | 3 | Minimalistic web-application for creating electronic music with digital synthesizers. 4 | Initially designed as sketching tool but can be used for making full-fledged tracks :musical_note::notes: 5 | 6 | **[:link: RUN IN BROWSER :link:](https://valent-in.github.io/pulseq)** 7 | 8 | - Simple and easy to use 9 | - Mobile-friendly 10 | - Client only (no data processed on servers) 11 | 12 | - Subtractive synthesizers with various effects 13 | - Harmonic oscillators & basic FM 14 | - Multi-layered step sequencer 15 | - Exportable synth presets 16 | - WAV audio export 17 | - MIDI export 18 | 19 | ## Music examples (YouTube) 20 | - [Cosmix - part 1](https://www.youtube.com/watch?v=KkLsClq37w4) 21 | - [Cosmix - part 2](https://www.youtube.com/watch?v=8_aYqIMCa2k) 22 | - [Clean Steps](https://www.youtube.com/watch?v=2IaCb21nIZU) 23 | 24 | ## Quick start 25 | To get started you can experiment with included songs. Click "Demo" button on startup menu and select demo track. Reload page to reach this menu again. 26 | 27 | **Program tabs:** 28 | ARRANGE :cd: 29 | Combine patterns into complete music track. 30 | 31 | PATTERN :musical_keyboard: 32 | Place notes here. Synth engine is monophonic (single-voice), additional voices can be added with pattern layers. 33 | 34 | SYNTH :control_knobs: 35 | Configuration panel for selected instrument. Presets are available from 3-dot menu. 36 | 37 | LIST :level_slider::level_slider: 38 | Contains list of synthesizers and also a mixer. 39 | 40 | ## Reference Cards 41 | ![overview card](refcards/refcard-overview.png) 42 | ![controls card](refcards/refcard-controls.png) 43 | ![routing card](refcards/refcard-synth.png) 44 | 45 | ## Performance notes 46 | - Some effects are CPU-heavy (especially reverb and phaser). This should be accounted when using on mobile devices. 47 | - WAV export duration may be limited on mobile browsers to about 10 minutes. 48 | 49 | --- 50 | Using Web Audio API and [Tone.js](https://github.com/Tonejs/Tone.js) 51 | File export sources: 52 | [bufferToWave](https://github.com/rwgood18/javascript-audio-file-dynamics-compressor), 53 | [midi-writer](https://github.com/carter-thaxton/midi-file). 54 | 55 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3. 56 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. -------------------------------------------------------------------------------- /img/lfo.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 21 | 26 | 27 | 28 | 30 | 31 | 33 | image/svg+xml 34 | 36 | 37 | 38 | 39 | 40 | 43 | 49 | 53 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /img/oscillator.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 21 | 26 | 27 | 28 | 30 | 31 | 33 | image/svg+xml 34 | 36 | 37 | 38 | 39 | 40 | 43 | 49 | 53 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /img/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 32 | 38 | 39 | -------------------------------------------------------------------------------- /img/modulation-amp.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 21 | 26 | 27 | 28 | 30 | 31 | 33 | image/svg+xml 34 | 36 | 37 | 38 | 39 | 40 | 43 | 47 | 51 | 55 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /img/modulation-filt.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 21 | 26 | 27 | 28 | 30 | 31 | 33 | image/svg+xml 34 | 36 | 37 | 38 | 39 | 40 | 43 | 47 | 51 | 58 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /img/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 21 | 26 | 27 | 28 | 30 | 31 | 33 | image/svg+xml 34 | 36 | 37 | 38 | 39 | 40 | 43 | 47 | 51 | 58 | 62 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /css/table-arrange.css: -------------------------------------------------------------------------------- 1 | #arrange-main table { 2 | border-spacing: 0; 3 | border-right: 1px solid #444; 4 | -webkit-user-select: none; 5 | user-select: none; 6 | } 7 | 8 | #arrange-main td { 9 | border-bottom: 1px solid #444; 10 | border-left: 1px solid #444; 11 | width: 20px; 12 | min-width: 20px; 13 | max-width: 20px; 14 | height: 20px; 15 | position: relative; 16 | box-sizing: border-box; 17 | } 18 | 19 | #arrange-main .arrange-header { 20 | color: #444; 21 | overflow: visible; 22 | } 23 | 24 | #arrange-main .arrange-header:last-child { 25 | overflow: hidden; 26 | } 27 | 28 | #arrange-main .arrange-header.play-start-point { 29 | background-image: radial-gradient(circle, #eee 0%, #eee 10%, #444 20%, transparent 35%); 30 | } 31 | 32 | #arrange-main .arrange-header.loop-start-point { 33 | box-shadow: inset 2px 0 0 0 #ccc; 34 | } 35 | 36 | #arrange-main .arrange-header.loop-end-point { 37 | box-shadow: inset -2px 0 0 0 #ccc; 38 | } 39 | 40 | #arrange-main .arrange-header.loop-start-point.loop-end-point { 41 | box-shadow: inset 2px 0 0 0 #ccc, inset -2px 0 0 0 #ccc; 42 | } 43 | 44 | #arrange-main td:first-child { 45 | min-width: 59px; 46 | max-width: 59px; 47 | position: -webkit-sticky; 48 | position: sticky; 49 | left: 0; 50 | background-color: #111; 51 | border-right: 1px solid #444; 52 | border-left: 1px solid #444; 53 | z-index: 1; 54 | font-size: 0.75rem; 55 | overflow: hidden; 56 | white-space: nowrap; 57 | } 58 | 59 | #arrange-main tr:first-child td { 60 | border-left: none; 61 | position: -webkit-sticky; 62 | position: sticky; 63 | top: 0; 64 | background-color: #111; 65 | z-index: 1; 66 | font-size: 1rem; 67 | counter-increment: numbers; 68 | } 69 | 70 | #arrange-main tr:first-child td:first-child { 71 | border-left: 1px solid #444; 72 | z-index: 3; 73 | background-image: url("data:image/svg+xml;utf8,"); 74 | background-position: center left 0.5em; 75 | background-repeat: no-repeat; 76 | background-size: auto 35%; 77 | counter-reset: numbers -1; 78 | } 79 | 80 | #arrange-main tr td:nth-child(4n+2) { 81 | border-left: 1px solid #6a6a6a; 82 | } 83 | 84 | #arrange-main tr:first-child td:nth-child(4n+2) { 85 | z-index: 2; 86 | } 87 | 88 | #arrange-main tr:first-child td:nth-child(4n+2):before { 89 | content: counter(numbers); 90 | pointer-events: none; 91 | } 92 | 93 | #arrange-main tr:nth-child(4n+5) td { 94 | border-bottom: 1px solid #6a6a6a; 95 | } 96 | 97 | #arrange-main td.non-free-cell { 98 | background-color: #322; 99 | } 100 | 101 | #arrange-main td.js-fill-head { 102 | background-color: #eee; 103 | box-shadow: inset 0 0 0 1px #111; 104 | } 105 | 106 | #arrange-main td.js-fill-tail { 107 | background-color: #eee; 108 | box-shadow: inset 0 0 0 1px #111, inset 21px 0px 0 0 rgba(0, 0, 0, 0.39); 109 | } 110 | 111 | #arrange-main td.current-pattern-mark { 112 | box-shadow: inset -19px 0 #444; 113 | } -------------------------------------------------------------------------------- /js/scheduler-ui.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function schedulerUi(scheduler, setLoopMarkersCallback) { 4 | let songPlayBtn = document.getElementById("button-arrange-play"); 5 | let patternPlayBtn = document.getElementById("button-pattern-play"); 6 | let barsInput = document.getElementById("input-loop-bars"); 7 | 8 | document.addEventListener("keydown", (event) => { 9 | if (event.target.type == "number" || event.target.type == "text") 10 | return; 11 | 12 | if (event.code == "Backquote") 13 | songPlayListener(); 14 | 15 | if (event.code == "Digit1") 16 | patternPlayListener(); 17 | 18 | if (event.code == "Space") { 19 | if (event.target.type == "checkbox") 20 | return; 21 | 22 | event.preventDefault(); 23 | 24 | switch (window.g_activeTab) { 25 | case "arrange": 26 | songPlayListener(); 27 | break; 28 | 29 | case "pattern": 30 | patternPlayListener(); 31 | break; 32 | 33 | default: 34 | scheduler.stop(); 35 | updateButtons(false, false); 36 | } 37 | } 38 | }); 39 | 40 | document.addEventListener("keyup", (event) => { 41 | if (event.target.tagName != "INPUT" && event.code == "Space") { 42 | event.preventDefault(); 43 | } 44 | }); 45 | 46 | songPlayBtn.onclick = songPlayListener; 47 | patternPlayBtn.onclick = patternPlayListener; 48 | 49 | document.getElementById("button-loop-play").onclick = loopPlayListener; 50 | barsInput.addEventListener("keyup", (event) => { 51 | if (event.key == "Enter") { 52 | loopPlayListener(); 53 | } 54 | }); 55 | 56 | function onForceStop() { 57 | updateButtons(false, false); 58 | removePlayMarkers(); 59 | }; 60 | 61 | function songPlayListener() { 62 | let state = scheduler.playStopSong(onForceStop); 63 | updateButtons(false, state); 64 | removePlayMarkers(); 65 | }; 66 | 67 | function loopPlayListener() { 68 | let barsInLoop = Math.round(barsInput.value); 69 | 70 | if (!(barsInLoop >= 1 && barsInLoop <= 99)) { 71 | showAlert("Loop length should be in range 1..99"); 72 | barsInput.value = 1; 73 | return; 74 | } 75 | 76 | let startIndex = scheduler.playLoop(onForceStop, barsInLoop); 77 | removePlayMarkers(); 78 | setPlayMarkers(startIndex, barsInLoop); 79 | 80 | updateButtons(false, true); 81 | hideModal("column-modal-menu"); 82 | }; 83 | 84 | function patternPlayListener() { 85 | let state = scheduler.playStopPattern(onForceStop); 86 | updateButtons(state, false); 87 | removePlayMarkers(); 88 | }; 89 | 90 | function updateButtons(patternState, songState) { 91 | let playUrl = "url('img/play.svg')"; 92 | let stopUrl = "url('img/stop.svg')"; 93 | let stopgUrl = "url('img/stopg.svg')"; 94 | 95 | if (!patternState && !songState) { 96 | songPlayBtn.style.backgroundImage = playUrl; 97 | patternPlayBtn.style.backgroundImage = playUrl; 98 | } 99 | 100 | if (patternState) { 101 | patternPlayBtn.style.backgroundImage = stopUrl; 102 | songPlayBtn.style.backgroundImage = stopgUrl; 103 | } 104 | 105 | if (songState) { 106 | songPlayBtn.style.backgroundImage = stopUrl; 107 | patternPlayBtn.style.backgroundImage = stopgUrl; 108 | } 109 | }; 110 | 111 | function removePlayMarkers() { 112 | songPlayBtn.classList.remove("button--play-loop"); 113 | setLoopMarkersCallback(-1); 114 | }; 115 | 116 | function setPlayMarkers(startIndex, length) { 117 | songPlayBtn.classList.add("button--play-loop"); 118 | setLoopMarkersCallback(startIndex, length); 119 | } 120 | } -------------------------------------------------------------------------------- /js/defaults.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const DEFAULT_PARAMS = {}; 4 | 5 | DEFAULT_PARAMS.programVersion = "1.4"; 6 | DEFAULT_PARAMS.fileFormatVersion = "20"; 7 | 8 | DEFAULT_PARAMS.maxPatternSteps = 64; 9 | 10 | DEFAULT_PARAMS.minSongBars = 32; 11 | DEFAULT_PARAMS.emptyBarsBuffer = 8; 12 | 13 | DEFAULT_PARAMS.pressDelay = 380; 14 | 15 | DEFAULT_PARAMS.synthState = { 16 | "synth-amplifier-gain": 1, 17 | "synth-amplifier-mod-input": "[none]", 18 | "synth-amplifier-mod-value": 0, 19 | "synth-envelope-attack": 0.2, 20 | "synth-envelope-decay": 0.2, 21 | "synth-envelope-release": 2, 22 | "synth-envelope-sustain": 1, 23 | "synth-envelope-type": "exponential", 24 | "synth-filter-frequency": 1, 25 | "synth-filter-mod-input": "[none]", 26 | "synth-filter-mod-value": 0, 27 | "synth-filter-quality": 0, 28 | "synth-filter-type": "[none]", 29 | "synth-fx-amount": 0.5, 30 | "synth-fx-rate": 0.5, 31 | "synth-fx-sync": false, 32 | "synth-fx-type": "[none]", 33 | "synth-fx-wet": 0.5, 34 | "synth-glide": 0, 35 | "synth-lfo1-frequency": 0, 36 | "synth-lfo1-partials": "", 37 | "synth-lfo1-sync": false, 38 | "synth-lfo1-type": "[none]", 39 | "synth-lfo2-frequency": 0, 40 | "synth-lfo2-partials": "", 41 | "synth-lfo2-retrig": false, 42 | "synth-lfo2-type": "[none]", 43 | "synth-mod-envelope-attack": 6, 44 | "synth-mod-envelope-decay": 6, 45 | "synth-mod-envelope-release": 6, 46 | "synth-mod-envelope-sustain": 0, 47 | "synth-mod-envelope-type": "[none]", 48 | "synth-noise-level": 0, 49 | "synth-noise-type": "[none]", 50 | "synth-osc1-detune": 0, 51 | "synth-osc1-level": 1, 52 | "synth-osc1-mod-input": "[none]", 53 | "synth-osc1-mod-value": 0, 54 | "synth-osc1-octave": 0, 55 | "synth-osc1-partials": "", 56 | "synth-osc1-type": "triangle", 57 | "synth-osc2-detune": 0, 58 | "synth-osc2-level": 0, 59 | "synth-osc2-mod-input": "[none]", 60 | "synth-osc2-mod-value": 0, 61 | "synth-osc2-octave": 0, 62 | "synth-osc2-partials": "", 63 | "synth-osc2-type": "[none]", 64 | "synth-osc3-detune": 0, 65 | "synth-osc3-level": 0, 66 | "synth-osc3-octave": 0, 67 | "synth-osc3-partials": "", 68 | "synth-osc3-type": "[none]", 69 | "synth-pan": 0, 70 | "synth-porta": false 71 | }; 72 | 73 | DEFAULT_PARAMS.noteSet = [ 74 | "C2", "Db2", "D2", "Eb2", "E2", "F2", "Gb2", "G2", "Ab2", "A2", "Bb2", "B2", 75 | "C3", "Db3", "D3", "Eb3", "E3", "F3", "Gb3", "G3", "Ab3", "A3", "Bb3", "B3", 76 | "C4", "Db4", "D4", "Eb4", "E4", "F4", "Gb4", "G4", "Ab4", "A4", "Bb4", "B4", 77 | "C5", "Db5", "D5", "Eb5", "E5", "F5", "Gb5", "G5", "Ab5", "A5", "Bb5", "B5", 78 | "C6", "Db6", "D6", "Eb6", "E6", "F6", "Gb6", "G6", "Ab6", "A6", "Bb6", "B6" 79 | ]; 80 | 81 | DEFAULT_PARAMS.noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 82 | 83 | DEFAULT_PARAMS.scaleSet = [[null, "[none]"], 84 | ["101011010101", "Major"], 85 | ["101011011001", "H-Major"], // Harmonic 86 | ["101101011010", "Minor"], 87 | ["101101011001", "H-Minor"], 88 | ["100101010010", "P-Minor"], // Pentatonic 89 | ["100110011001", "Augmented"], 90 | ["101011011101", "Bebop"], // major 91 | ["100101110010", "Blues"], 92 | ["110010101011", "Enigmatic"], 93 | ["110011011001", "Flamenco"], 94 | ["101100111010", "Gypsy"], 95 | ["100111010100", "Harmonics"], 96 | ["100010110001", "Hirajoshi"], 97 | ["100110110110", "Hungarian"], // major 98 | ["110001010010", "Insen"], 99 | ["110110110000", "Istrian"], 100 | ["101101010101", "Jazz"], // Melodic minor (ascending) 101 | ["110101010101", "Neapolitan"], // major ? 102 | ["101101101101", "Octatonic"], 103 | ["110011101001", "Persian"], 104 | ["101010100110", "Prometheus"], 105 | ["110010110010", "Tritone"], 106 | ["101100110110", "Ukrainian"], 107 | ["101010101010", "Wholetone"] 108 | ]; 109 | 110 | DEFAULT_PARAMS.colorSet = [ 111 | "#b4d", "#4bd", "#4db", "#bd4", "#db4", "#d4b", 112 | "#97d", "#7dd", "#7d9", "#dd7", "#d94", "#d77", 113 | "#b9d", "#9bd", "#bd9", "#dd9", "#db9", "#d9b", 114 | "#999", "#bbb", "#ddd", "#55d", "#5b5", "#d55" 115 | ]; 116 | 117 | for (let key in DEFAULT_PARAMS) { 118 | let o = DEFAULT_PARAMS[key]; 119 | if (typeof o == "object") 120 | Object.freeze(o); 121 | } 122 | 123 | Object.freeze(DEFAULT_PARAMS); -------------------------------------------------------------------------------- /js/reorder-menu.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function ReorderMenu(songObj, onSongChangeCallback) { 4 | let patternListContainer = document.getElementById("pattern-list-container"); 5 | let sortMenuIsShown = false; 6 | let patternEntries = []; 7 | let dragPatternFrom; 8 | 9 | this.showMenu = () => { 10 | if (songObj.patterns.length > 1) { 11 | rebuildPatternList(); 12 | hideModal("column-modal-menu"); 13 | showModal("pattern-reorder-modal-menu"); 14 | sortMenuIsShown = true; 15 | } else { 16 | showToast("Can not order single pattern"); 17 | } 18 | } 19 | 20 | patternListContainer.ondragstart = () => { 21 | patternEntries.forEach((e) => e.classList.add("drop-hint")); 22 | } 23 | 24 | patternListContainer.ondragend = dragend; 25 | patternListContainer.ontouchend = dragend; 26 | patternListContainer.ontouchcancel = dragend; 27 | 28 | function dragend() { 29 | patternEntries.forEach((e) => e.classList.remove("drop-hint")); 30 | patternEntries.forEach((e) => e.classList.remove("drop-target")); 31 | patternEntries.forEach((e) => e.classList.remove("dragged")); 32 | } 33 | 34 | document.getElementById("button-sort-patterns-asc").onclick = () => { 35 | showConfirm("Sort patterns by name in ascending order?", (isOk) => { 36 | if (!isOk) 37 | return; 38 | songObj.sortPatternsByName(false); 39 | rebuildPatternList(); 40 | }); 41 | } 42 | 43 | document.getElementById("button-sort-patterns-desc").onclick = () => { 44 | showConfirm("Sort patterns by name in descending order?", (isOk) => { 45 | if (!isOk) 46 | return; 47 | songObj.sortPatternsByName(true); 48 | rebuildPatternList(); 49 | }); 50 | } 51 | 52 | document.getElementById("button-reorder-menu-close").onclick = () => { 53 | hideModal("pattern-reorder-modal-menu"); 54 | document.getElementById("pattern-list-container").innerHTML = ""; 55 | sortMenuIsShown = false; 56 | 57 | setTimeout(() => { 58 | onSongChangeCallback(false); 59 | }, 0); 60 | } 61 | 62 | let reorderMenu = document.getElementById("pattern-reorder-modal-menu"); 63 | window.addEventListener("keyup", (event) => { 64 | if (event.key != "Escape") 65 | return; 66 | 67 | if (sortMenuIsShown) { 68 | setTimeout(() => { 69 | onSongChangeCallback(false); 70 | 71 | if (reorderMenu.classList.contains("nodisplay")) 72 | sortMenuIsShown = false; 73 | }, 0); 74 | } 75 | }); 76 | 77 | function rebuildPatternList() { 78 | patternListContainer.innerHTML = ""; 79 | patternEntries.length = 0; 80 | 81 | for (let i = 0; i < songObj.patterns.length; i++) { 82 | let entry = document.createElement("DIV"); 83 | entry.classList.add("pattern-list-entry"); 84 | entry.draggable = true; 85 | entry.dataset.index = i; 86 | 87 | entry.ondragstart = () => { 88 | entry.classList.add("dragged"); 89 | dragPatternFrom = entry.dataset.index; 90 | } 91 | 92 | entry.ondragenter = () => { 93 | patternEntries.forEach(e => e.classList.remove("drop-target")); 94 | entry.classList.add("drop-target"); 95 | } 96 | 97 | // not working if mouse moves fast 98 | entry.ondragleave = (event) => { 99 | entry.classList.remove("drop-target"); 100 | } 101 | 102 | entry.ondragover = (event) => { 103 | event.preventDefault(); 104 | } 105 | 106 | entry.ondrop = (event) => { 107 | event.preventDefault(); 108 | songObj.movePattern(Number(dragPatternFrom), Number(entry.dataset.index)); 109 | rebuildPatternList(); 110 | } 111 | 112 | let name = document.createElement("SPAN"); 113 | name.appendChild(document.createTextNode(songObj.patterns[i].name)); 114 | entry.appendChild(name); 115 | name.style.color = DEFAULT_PARAMS.colorSet[songObj.patterns[i].colorIndex]; 116 | 117 | let downBtn = document.createElement("BUTTON"); 118 | downBtn.classList.add("button--arrow-down") 119 | downBtn.onclick = () => { 120 | songObj.movePattern(i, i + 1); 121 | rebuildPatternList(); 122 | } 123 | 124 | let upBtn = document.createElement("BUTTON"); 125 | upBtn.classList.add("button--arrow-up"); 126 | upBtn.onclick = () => { 127 | songObj.movePattern(i, i - 1); 128 | rebuildPatternList(); 129 | } 130 | 131 | entry.appendChild(downBtn); 132 | entry.appendChild(upBtn); 133 | patternListContainer.appendChild(entry); 134 | patternEntries.push(entry); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | console.log("%c\u25A0 %c\u25B6 %c\u25A0 %c PulseQueue v" + DEFAULT_PARAMS.programVersion + " ", 4 | "color:#1ff", "color:#f81", "color:#bbb", "background-color:#000;color:#fff"); 5 | 6 | { 7 | Tone.context.lookAhead = 0.15; 8 | console.log("Sample rate:", Tone.context.sampleRate); 9 | 10 | // Disable closing browser window with back button (Android/PWA) 11 | if (history.length == 1) { 12 | history.replaceState({ alter: true }, "", location.href); 13 | history.pushState({ alter: true }, "", location.href); 14 | } 15 | 16 | if (history.state && history.state.alter) { 17 | window.onpopstate = function () { 18 | history.go(1); 19 | scheduler.stop(); 20 | } 21 | } 22 | 23 | window.g_markCurrentSynth = function () { 24 | let previous = document.querySelectorAll("#synth-list-main > .synth-list-entry--current"); 25 | if (previous.length > 0) 26 | previous[0].classList.remove("synth-list-entry--current"); 27 | 28 | let element = document.getElementById("synth-list-entry_" + songObject.currentSynthIndex); 29 | if (element) 30 | element.classList.add("synth-list-entry--current"); 31 | } 32 | 33 | window.g_markCurrentPattern = function () { 34 | let previous = document.querySelectorAll("#arrange-main .current-pattern-mark"); 35 | if (previous.length > 0) 36 | previous[0].classList.remove("current-pattern-mark"); 37 | 38 | let element = document.getElementById("arr_side_row-" + songObject.currentPatternIndex); 39 | if (element) 40 | element.classList.add("current-pattern-mark"); 41 | } 42 | 43 | window.g_scrollToLastPattern = function () { 44 | setTimeout(() => { 45 | let rows = document.querySelectorAll("#arrange-main table tr:last-child"); 46 | if (rows[0]) 47 | rows[0].scrollIntoView(); 48 | }, 0); 49 | } 50 | 51 | let styleForRows = document.getElementById("colored-rows-style"); 52 | let styleTxt = ``; 53 | for (let i = 0; i < DEFAULT_PARAMS.colorSet.length; i++) { 54 | styleTxt += ` 55 | #arrange-main tr.color-index-${i} td.js-fill-head { 56 | background-color: ${DEFAULT_PARAMS.colorSet[i]}; 57 | } 58 | #arrange-main tr.color-index-${i} td.js-fill-tail { 59 | background-color: ${DEFAULT_PARAMS.colorSet[i]}; 60 | } 61 | #arrange-main tr.color-index-${i} td:first-child { 62 | color: ${DEFAULT_PARAMS.colorSet[i]}; 63 | } 64 | `; 65 | } 66 | styleForRows.innerText = styleTxt; 67 | 68 | let patternDiv = document.getElementById("pattern-main"); 69 | // non-zero width indicates non-overlay scrollbar 70 | if (patternDiv.offsetWidth <= 1) { 71 | patternDiv.classList.add("add-scrollbar-spacing"); 72 | document.getElementById("arrange-main").classList.add("add-scrollbar-spacing"); 73 | } 74 | 75 | const songObject = new SongObject(); 76 | 77 | const synthUi = new SynthUi(songObject); 78 | 79 | const patternUi = new PatternUi(songObject, synthUi.assignSynth, onSongChange); 80 | patternUi.build(); 81 | 82 | const synthHelper = new SynthHelper(songObject, synthUi, updPatternSynthList); 83 | synthHelper.buildPresetList(); 84 | 85 | const arrangeUi = new ArrangeUi(songObject, onPatternSelect, DEFAULT_PARAMS); 86 | arrangeUi.build(); 87 | 88 | const scheduler = new Scheduler(songObject, arrangeUi.setMarker, patternUi.setMarker); 89 | 90 | schedulerUi(scheduler, arrangeUi.setLoopMarkers); 91 | menuInit(songObject, onSongChange, synthHelper.loadSynth, scheduler.renderSong, scheduler.exportMidiSequence); 92 | waveformEditor(songObject); 93 | 94 | document.getElementById("startup-loading-title").style.display = "none"; 95 | document.getElementById("startup-menu").style.display = "block"; 96 | document.getElementById("input-import-track").focus(); 97 | 98 | function onSongChange(isNewSong, stopCommand, preserveArrangeView) { 99 | updPatternSynthList(!preserveArrangeView); 100 | if (isNewSong) { 101 | synthUi.assignSynth(songObject.synthParams[0], songObject.synths[0], songObject.synthNames[0]); 102 | songObject.currentSynthIndex = 0; 103 | } 104 | synthHelper.rebuildSynthList(); 105 | 106 | switch (stopCommand) { 107 | case "stop": 108 | scheduler.stop(); 109 | break; 110 | case "release": 111 | scheduler.release(); 112 | break; 113 | } 114 | } 115 | 116 | function updPatternSynthList(updArrangeView) { 117 | if (updArrangeView) { 118 | arrangeUi.fillSongView(); 119 | g_markCurrentPattern(); 120 | } 121 | 122 | patternUi.importSequence(songObject.currentPattern); 123 | patternUi.rebuildPatternSynthList(songObject.currentPattern); 124 | } 125 | 126 | function onPatternSelect(newCurrentPattern, isNewPattern) { 127 | scheduler.releasePattern(); 128 | patternUi.importSequence(newCurrentPattern); 129 | patternUi.rebuildPatternSynthList(newCurrentPattern); 130 | 131 | if (isNewPattern) 132 | patternUi.setNewPatternSynth(); 133 | } 134 | } -------------------------------------------------------------------------------- /js/mid-synth.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Fake synth constructor for MIDI export 4 | function MidSynth(songObj, index, exportParams) { 5 | let { isExpand, isOverlap, velocityType } = exportParams; 6 | 7 | let track = []; 8 | this.track = track; 9 | 10 | let lastNote; 11 | let lastTick = 0; 12 | let lastVelo = 0; 13 | let pitchRepeats = 0; 14 | 15 | let barMarkerTick = 0; 16 | 17 | this.isEmpty = true; 18 | 19 | track.push({ 20 | type: "trackName", 21 | deltaTime: 0, 22 | text: songObj.synthNames[index] 23 | }); 24 | 25 | let denominator; 26 | switch (songObj.barSteps % 4) { 27 | case 0: 28 | denominator = 4; 29 | break; 30 | 31 | case 2: 32 | denominator = 8; 33 | break; 34 | 35 | default: 36 | denominator = 16; 37 | } 38 | 39 | let numerator = Math.round(songObj.barSteps * denominator / 16); 40 | 41 | track.push({ 42 | type: "timeSignature", 43 | deltaTime: 0, 44 | numerator: numerator, 45 | denominator: denominator 46 | }); 47 | 48 | let bps = songObj.bpm / 60; 49 | let mkspb = Math.round(1000000 / bps); 50 | track.push({ 51 | type: "setTempo", 52 | deltaTime: 0, 53 | microsecondsPerBeat: mkspb 54 | }); 55 | 56 | this.triggerAttack = function (note, volumeMod, time, duration) { 57 | let tick = getTick(time); 58 | lastVelo = toVelocity(volumeMod); 59 | lastNote = note; 60 | 61 | track.push(noteOn(note, tick - lastTick, lastVelo)); 62 | 63 | lastTick = tick; 64 | this.isEmpty = false; 65 | } 66 | 67 | this.triggerRelease = function (time) { 68 | let tick = getTick(time); 69 | 70 | if (isExpand) 71 | tick = Math.ceil(tick / 32) * 32; 72 | 73 | track.push(noteOff(lastNote, tick - lastTick)); 74 | 75 | for (let i = 0; i < pitchRepeats; i++) 76 | track.push(noteOff(lastNote, 0)) 77 | pitchRepeats = 0; 78 | 79 | lastTick = tick; 80 | } 81 | 82 | this.glideTo = function (note, volumeMod, time, duration) { 83 | let velo = toVelocity(volumeMod); 84 | 85 | if (note == lastNote && velo == lastVelo) 86 | return; 87 | 88 | let tick = getTick(time); 89 | 90 | if (note == lastNote) { 91 | if (isOverlap) { 92 | track.push(noteOn(note, tick - lastTick, velo)); 93 | lastTick = tick; 94 | pitchRepeats++; 95 | } 96 | } else { 97 | if (isOverlap) { 98 | track.push(noteOn(note, tick - lastTick, velo)); 99 | track.push(noteOff(lastNote, 3)); 100 | 101 | for (let i = 0; i < pitchRepeats; i++) 102 | track.push(noteOff(lastNote, 0)); 103 | pitchRepeats = 0; 104 | 105 | lastTick = tick + 3; 106 | } else { 107 | track.push(noteOff(lastNote, tick - lastTick)); 108 | track.push(noteOn(note, 0, velo)); 109 | lastTick = tick; 110 | } 111 | } 112 | 113 | lastNote = note; 114 | lastVelo = velo; 115 | } 116 | 117 | // Dummy 118 | this.filterSweep = function () { } 119 | 120 | this.setBarMarker = function (time) { 121 | if (barMarkerTick < lastTick) 122 | barMarkerTick = getTick(time); 123 | } 124 | 125 | this.finish = function () { 126 | if (barMarkerTick < lastTick) 127 | barMarkerTick = lastTick; 128 | 129 | track.push({ type: "endOfTrack", deltaTime: barMarkerTick - lastTick }); 130 | } 131 | 132 | function noteOn(note, delta, velocity) { 133 | return { 134 | type: "noteOn", 135 | deltaTime: delta, 136 | noteNumber: Tone.Frequency(note).toMidi(), 137 | velocity: velocity 138 | } 139 | } 140 | 141 | function noteOff(note, delta, velocity) { 142 | return { 143 | type: "noteOff", 144 | deltaTime: delta, 145 | noteNumber: Tone.Frequency(note).toMidi() 146 | } 147 | } 148 | 149 | function getTick(time) { 150 | let beatLength = 60 / songObj.bpm; 151 | return Math.round((time / beatLength) * 128); 152 | } 153 | 154 | function toVelocity(volumeMod) { 155 | switch (velocityType) { 156 | case 0: // exponential 157 | let volume = (100 + volumeMod) / 100; 158 | let ret = Math.ceil((Math.exp(volume * 5.76) - 1) / 10 * 3.1611 * 1.27); 159 | return Math.min(ret, 127); 160 | 161 | case 1: // linear 162 | return Math.round((100 + volumeMod) * 1.27); 163 | 164 | default: // fixed value 165 | return 127; 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /js/pattern.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class Pattern { 4 | constructor(name, steps) { 5 | this.name = name || ""; 6 | this.length = steps || 16; 7 | this.colorIndex = 0; 8 | this.patternData = []; 9 | 10 | this.addLayer(); 11 | } 12 | 13 | addLayer = () => { 14 | this.activeIndex = this.patternData.length; 15 | this.patternData.push({ notes: [], lengths: [], volumes: [], filtF: [], filtQ: [], synthIndex: null }); 16 | } 17 | 18 | deleteActiveLayer() { 19 | this.patternData.splice(this.activeIndex, 1); 20 | this.activeIndex = 0; 21 | } 22 | 23 | spliceSynth(index) { 24 | for (let i = 0; i < this.patternData.length; i++) { 25 | if (this.patternData[i].synthIndex == index) 26 | this.patternData[i].synthIndex = null; 27 | 28 | if (this.patternData[i].synthIndex > index) 29 | this.patternData[i].synthIndex--; 30 | } 31 | } 32 | 33 | isActiveLayerEmpty() { 34 | let layer = this.patternData[this.activeIndex]; 35 | for (let i = 0; i < this.length; i++) { 36 | if (layer.notes[i]) 37 | return false; 38 | } 39 | 40 | return true; 41 | } 42 | 43 | getFadeRange() { 44 | let layer = this.patternData[this.activeIndex]; 45 | let startVolume = 0, endVolume = 0, isEmpty = true; 46 | 47 | for (let i = 0; i < this.length; i++) { 48 | if (layer.notes[i]) { 49 | isEmpty = false; 50 | endVolume = 100 + layer.volumes[i]; 51 | 52 | if (startVolume == 0) 53 | startVolume = 100 + layer.volumes[i]; 54 | } 55 | } 56 | 57 | return { startVolume: startVolume, endVolume: endVolume, isEmpty: isEmpty }; 58 | } 59 | 60 | fadeActiveLayer(startVolume, endVolume) { 61 | let layer = this.patternData[this.activeIndex]; 62 | 63 | let startIndex = -1, endIndex = 0; 64 | for (let i = 0; i < this.length; i++) { 65 | if (layer.notes[i]) { 66 | endIndex = i; 67 | 68 | if (startIndex < 0) 69 | startIndex = i; 70 | } 71 | } 72 | 73 | let lineLength = Math.max(1, endIndex - startIndex); 74 | let step = (endVolume - startVolume) / lineLength; 75 | 76 | for (let i = 0; i <= lineLength; i++) { 77 | if (layer.notes[i + startIndex]) 78 | layer.volumes[i + startIndex] = Math.min(0, -100 + Math.round(startVolume + i * step)); 79 | } 80 | } 81 | 82 | fadeAddActiveLayer(volumeMod) { 83 | let layer = this.patternData[this.activeIndex]; 84 | 85 | for (let i = 0; i < this.length; i++) { 86 | layer.volumes[i] += Math.round(volumeMod); 87 | layer.volumes[i] = Math.max(layer.volumes[i], -99); 88 | layer.volumes[i] = Math.min(layer.volumes[i], 0); 89 | } 90 | } 91 | 92 | copyActiveLayer() { 93 | let layer = this.patternData[this.activeIndex]; 94 | 95 | this.addLayer(); 96 | let newLayer = this.patternData[this.activeIndex]; 97 | 98 | for (let i = 0; i < this.length; i++) { 99 | newLayer.notes[i] = layer.notes[i]; 100 | newLayer.lengths[i] = layer.lengths[i]; 101 | newLayer.volumes[i] = layer.volumes[i]; 102 | newLayer.filtF[i] = layer.filtF[i]; 103 | newLayer.filtQ[i] = layer.filtQ[i]; 104 | } 105 | } 106 | 107 | shiftActiveLayer(steps, isShiftPattern) { 108 | if (steps == 0) 109 | return; 110 | 111 | let length = this.length; 112 | steps = steps % length; 113 | 114 | if (isShiftPattern) { 115 | for (let i = 0; i < this.patternData.length; i++) { 116 | let layer = this.patternData[i]; 117 | shiftLayer(layer, steps, this.length); 118 | } 119 | } else { 120 | let layer = this.patternData[this.activeIndex]; 121 | shiftLayer(layer, steps); 122 | } 123 | 124 | function shiftLayer(layer, steps) { 125 | for (let i = 0; i < Math.abs(steps); i++) { 126 | let direction = steps > 0 ? 1 : -1; 127 | shiftOne(layer.notes, direction); 128 | shiftOne(layer.lengths, direction); 129 | shiftOne(layer.volumes, direction); 130 | shiftOne(layer.filtF, direction); 131 | shiftOne(layer.filtQ, direction); 132 | } 133 | } 134 | 135 | function shiftOne(arr, direction) { 136 | if (direction < 0) 137 | shiftOneLeft(arr); 138 | if (direction > 0) 139 | shiftOneRight(arr); 140 | } 141 | 142 | function shiftOneLeft(arr) { 143 | let tmp = arr[0]; 144 | for (let i = 0; i < length - 1; i++) 145 | arr[i] = arr[i + 1]; 146 | 147 | arr[length - 1] = tmp; 148 | } 149 | 150 | function shiftOneRight(arr) { 151 | let tmp = arr[length - 1]; 152 | for (let i = length - 1; i > 0; i--) 153 | arr[i] = arr[i - 1]; 154 | 155 | arr[0] = tmp; 156 | } 157 | } 158 | 159 | transposeActiveLayer(steps) { 160 | let noteArr = DEFAULT_PARAMS.noteSet.slice().reverse(); 161 | let layer = this.patternData[this.activeIndex]; 162 | let buffer = []; 163 | 164 | for (let i = 0; i < this.length; i++) { 165 | buffer[i] = findRowByNote(layer.notes[i]); 166 | 167 | if (buffer[i] !== null) 168 | buffer[i] -= steps; 169 | 170 | if (buffer[i] < 0 || buffer[i] >= noteArr.length) 171 | return false; 172 | } 173 | 174 | for (let i = 0; i < this.length; i++) 175 | if (buffer[i] !== null) 176 | layer.notes[i] = noteArr[buffer[i]]; 177 | 178 | 179 | return true; 180 | 181 | function findRowByNote(note) { 182 | for (let i = 0; i < noteArr.length; i++) 183 | if (noteArr[i] == note) 184 | return i; 185 | 186 | return null; 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 18 | 22 | 26 | 27 | 29 | 33 | 37 | 38 | 40 | 44 | 48 | 49 | 58 | 68 | 76 | 77 | 79 | 80 | 82 | image/svg+xml 83 | 85 | 86 | 87 | 88 | 89 | 97 | 101 | 105 | 109 | 113 | 114 | 118 | 125 | 129 | 130 | -------------------------------------------------------------------------------- /js/alerts.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | { 4 | // Close dialogs on Escape key 5 | window.addEventListener("keyup", (event) => { 6 | if (event.key != "Escape") 7 | return; 8 | 9 | if (discardAlert()) 10 | return; 11 | 12 | let dialogs = document.querySelectorAll(".modal-container:not(.nodisplay)"); 13 | if (dialogs.length <= 0 || dialogs[dialogs.length - 1].classList.contains("js-noskip")) 14 | return; 15 | 16 | hideModal(dialogs[dialogs.length - 1].id); 17 | }); 18 | } 19 | 20 | { 21 | // Dialog helper 22 | window.showModal = function (elementId) { 23 | let element = document.getElementById(elementId); 24 | if (!element) 25 | return; 26 | 27 | element.classList.remove("nodisplay"); 28 | requestFocus(element); 29 | } 30 | 31 | window.hideModal = function (elementId) { 32 | let element = document.getElementById(elementId); 33 | if (!element) 34 | return; 35 | 36 | element.classList.add("nodisplay"); 37 | 38 | let prev = document.querySelectorAll(".modal-container:not(.nodisplay)"); 39 | 40 | if (prev.length > 0) 41 | requestFocus(prev[prev.length - 1]); 42 | else 43 | document.getElementById("button-settings-open").focus(); 44 | } 45 | 46 | function requestFocus(element) { 47 | let focus = element.querySelectorAll(".js-request-focus:not(:disabled)"); 48 | 49 | if (focus.length > 0) { 50 | focus[0].focus(); 51 | } else { 52 | let focusAlt = element.querySelectorAll("button"); 53 | if (focusAlt.length > 0) 54 | focusAlt[focusAlt.length - 1].focus(); 55 | } 56 | } 57 | } 58 | 59 | { 60 | // showAlert/showConfirm/showPrompt to replace alert/confirm/prompt with custom dialogs 61 | let alertMessage = document.getElementById("modal-alert-message"); 62 | let okButton = document.getElementById("button-alert-ok"); 63 | let cancelButton = document.getElementById("button-alert-cancel"); 64 | let inputField = document.getElementById("input-modal-alert"); 65 | let inputFieldArea = document.getElementById("input-area-modal-alert"); 66 | 67 | let dialogTypes = []; 68 | let messages = []; 69 | let defaults = []; 70 | let callbacks = []; 71 | 72 | let dialogType = "alert"; 73 | let callbackFunc = null; 74 | 75 | let isShow = false; 76 | 77 | cancelButton.onclick = () => { 78 | discardAlert(); 79 | }; 80 | 81 | okButton.onclick = () => { 82 | confirmAlert(); 83 | }; 84 | 85 | inputFieldArea.addEventListener("keyup", (event) => { 86 | if (event.key == "Enter") { 87 | confirmAlert(); 88 | } 89 | }); 90 | 91 | window.discardAlert = function () { 92 | if (!isShow) 93 | return false; 94 | 95 | if (dialogType == "confirm" && typeof callbackFunc == "function") 96 | callbackFunc(false); 97 | 98 | if (dialogType.indexOf("prompt") == 0 && typeof callbackFunc == "function") 99 | callbackFunc(null); 100 | 101 | hideDialog(); 102 | return true; 103 | } 104 | 105 | window.showAlert = function (messageText) { 106 | showDialog("alert", messageText, null, null); 107 | } 108 | 109 | window.showConfirm = function (messageText, callback) { 110 | showDialog("confirm", messageText, callback, null); 111 | } 112 | 113 | window.showPrompt = function (messageText, callback, inputText, type) { 114 | if (type == "number") 115 | showDialog("promptNumber", messageText, callback, inputText); 116 | else 117 | showDialog("prompt", messageText, callback, inputText); 118 | } 119 | 120 | function confirmAlert() { 121 | if (dialogType == "confirm" && typeof callbackFunc == "function") 122 | callbackFunc(true); 123 | 124 | if (dialogType.indexOf("prompt") == 0 && typeof callbackFunc == "function") 125 | callbackFunc(inputField.value); 126 | 127 | hideDialog(); 128 | } 129 | 130 | function showDialog(type, alertText, callback, inputText) { 131 | if (isShow) { 132 | messages.push(alertText); 133 | callbacks.push(callback); 134 | defaults.push(inputText); 135 | dialogTypes.push(type); 136 | } else { 137 | showFieldsByType(type); 138 | setDialogText(alertText || "", inputText || ""); 139 | callbackFunc = callback; 140 | dialogType = type; 141 | showModal("modal-alert"); 142 | isShow = true; 143 | } 144 | } 145 | 146 | function showFieldsByType(type) { 147 | switch (type) { 148 | case "alert": 149 | cancelButton.classList.add("nodisplay"); 150 | inputFieldArea.classList.add("nodisplay"); 151 | inputField.disabled = true; 152 | break; 153 | 154 | case "confirm": 155 | cancelButton.classList.remove("nodisplay"); 156 | inputFieldArea.classList.add("nodisplay"); 157 | inputField.disabled = true; 158 | break; 159 | 160 | case "prompt": 161 | cancelButton.classList.remove("nodisplay"); 162 | inputFieldArea.classList.remove("nodisplay"); 163 | inputField.disabled = false; 164 | inputField.type = "text"; 165 | break; 166 | 167 | case "promptNumber": 168 | cancelButton.classList.remove("nodisplay"); 169 | inputFieldArea.classList.remove("nodisplay"); 170 | inputField.disabled = false; 171 | inputField.type = "number"; 172 | break; 173 | } 174 | } 175 | 176 | function setDialogText(messageText, inputText) { 177 | alertMessage.innerHTML = ""; 178 | 179 | let lines = String(messageText).split("\n"); 180 | for (let i = 0; i < lines.length; i++) { 181 | alertMessage.appendChild(document.createTextNode(lines[i])); 182 | if (i < lines.length - 1) 183 | alertMessage.appendChild(document.createElement("BR")); 184 | } 185 | 186 | inputField.value = inputText; 187 | } 188 | 189 | function hideDialog() { 190 | hideModal("modal-alert"); 191 | isShow = false; 192 | 193 | if (messages.length > 0) { 194 | showDialog(dialogTypes.shift(), messages.shift(), callbacks.shift(), defaults.shift()); 195 | } 196 | } 197 | } 198 | 199 | { 200 | let toastAlert = document.getElementById("toast-alert"); 201 | let toastBox = document.getElementById("toast-box"); 202 | let timeout = null 203 | 204 | window.showToast = function (text) { 205 | toastBox.innerHTML = ""; 206 | toastBox.appendChild(document.createTextNode(text)); 207 | toastAlert.classList.remove("nodisplay"); 208 | 209 | clearTimeout(timeout); 210 | timeout = setTimeout(() => { 211 | toastAlert.classList.add("nodisplay"); 212 | timeout = null; 213 | }, 2000); 214 | } 215 | } -------------------------------------------------------------------------------- /css/elements.css: -------------------------------------------------------------------------------- 1 | button, 2 | .button { 3 | background-color: #333; 4 | color: #bbb; 5 | text-align: center; 6 | padding: 2px 6px; 7 | border: 1px solid #444; 8 | border-radius: 3px; 9 | font-size: 1rem; 10 | min-width: 1rem; 11 | box-sizing: content-box; 12 | -webkit-user-select: none; 13 | user-select: none; 14 | display: inline-block; 15 | } 16 | 17 | button::-moz-focus-inner { 18 | padding: 0; 19 | border: 0; 20 | } 21 | 22 | button.button--small, 23 | .button.button--small { 24 | font-size: 0.9rem; 25 | padding: 1px 4px; 26 | margin: 2px; 27 | } 28 | 29 | button:hover, 30 | .button:hover { 31 | background-color: #3c3c3c; 32 | } 33 | 34 | button:active, 35 | .button:active { 36 | background-color: #282828; 37 | } 38 | 39 | button:focus, 40 | .button:focus { 41 | border-color: #606060; 42 | outline: none; 43 | } 44 | 45 | input[type=text], 46 | input[type=number] { 47 | -webkit-appearance: none; 48 | -moz-appearance: textfield; 49 | appearance: textfield; 50 | font-size: 1rem; 51 | border-radius: 3px; 52 | background-color: #101010; 53 | border: 1px solid #444; 54 | padding: 2px 4px; 55 | } 56 | 57 | input[type=text]:hover, 58 | input[type=number]:hover { 59 | background-color: #070707; 60 | } 61 | 62 | input[type=text]:focus, 63 | input[type=number]:focus { 64 | border-color: #606060; 65 | outline: none; 66 | } 67 | 68 | input[type=checkbox] { 69 | -moz-appearance: none; 70 | -webkit-appearance: none; 71 | appearance: none; 72 | border: 2px solid #444; 73 | background-color: #101010; 74 | border-radius: 3px; 75 | width: 1.1rem; 76 | height: 1.1rem; 77 | vertical-align: middle; 78 | margin-top: -0.2rem; 79 | } 80 | 81 | input[type=checkbox]:checked { 82 | background-image: url("data:image/svg+xml;utf8,"); 83 | background-repeat: no-repeat; 84 | background-position: center; 85 | background-size: 100% 100%; 86 | } 87 | 88 | input[type=radio] { 89 | -moz-appearance: none; 90 | -webkit-appearance: none; 91 | appearance: none; 92 | border: 2px solid #444; 93 | background-color: #101010; 94 | border-radius: 50%; 95 | width: 1.1rem; 96 | height: 1.1rem; 97 | vertical-align: middle; 98 | margin-top: -0.2rem; 99 | } 100 | 101 | input[type=radio]:checked { 102 | background-image: url("data:image/svg+xml;utf8,"); 103 | background-position: center; 104 | background-repeat: no-repeat; 105 | background-size: 100% 100%; 106 | } 107 | 108 | input[type=checkbox]:hover, 109 | input[type=radio]:hover { 110 | background-color: #222; 111 | } 112 | 113 | input[type=checkbox]:focus, 114 | input[type=radio]:focus { 115 | border: 2px solid #606060; 116 | outline: none; 117 | } 118 | 119 | input[type=range] { 120 | -webkit-appearance: none; 121 | appearance: none; 122 | width: 100%; 123 | height: 36px; 124 | background-color: transparent; 125 | } 126 | 127 | input[type=range]:focus { 128 | outline: none; 129 | } 130 | 131 | input[type=range]::-moz-range-track { 132 | width: 100%; 133 | height: 8px; 134 | border-radius: 4px; 135 | cursor: pointer; 136 | background: black; 137 | box-shadow: inset 0 0 0 2px #333; 138 | } 139 | 140 | input[type=range]:focus::-moz-range-track { 141 | box-shadow: inset 0 0 0 2px #3c3c3c; 142 | } 143 | 144 | input[type=range]::-moz-range-thumb { 145 | border-left: 8px solid #444; 146 | border-right: 8px solid #444; 147 | border-top: none; 148 | border-bottom: none; 149 | box-shadow: 0 0 0 1px #555; 150 | height: 24px; 151 | width: 2px; 152 | border-radius: 0; 153 | background: #ffffff; 154 | cursor: pointer; 155 | } 156 | 157 | input[type=range]::-moz-range-thumb:hover { 158 | box-shadow: 0 0 0 1px #646464; 159 | border-color: #555; 160 | } 161 | 162 | input[type=range]::-moz-range-thumb:active { 163 | border-color: #333; 164 | } 165 | 166 | input[type=range]::-webkit-slider-runnable-track { 167 | width: 100%; 168 | height: 8px; 169 | border-radius: 4px; 170 | cursor: pointer; 171 | background: black; 172 | box-sizing: content-box; 173 | box-shadow: inset 0 0 0 2px #333; 174 | } 175 | 176 | input[type=range]:focus::-webkit-slider-runnable-track { 177 | box-shadow: inset 0 0 0 2px #3c3c3c; 178 | } 179 | 180 | input[type=range]::-webkit-slider-thumb { 181 | -webkit-appearance: none; 182 | border-left: 8px solid #444; 183 | border-right: 8px solid #444; 184 | border-top: none; 185 | border-bottom: none; 186 | box-shadow: 0 0 0 1px #555; 187 | height: 24px; 188 | width: 2px; 189 | border-radius: 0; 190 | box-sizing: content-box; 191 | background: #ffffff; 192 | cursor: pointer; 193 | margin-top: -8px; 194 | } 195 | 196 | input[type=range]::-webkit-slider-thumb:hover { 197 | box-shadow: 0 0 0 1px #646464; 198 | border-color: #555; 199 | } 200 | 201 | input[type=range]::-webkit-slider-thumb:active { 202 | border-color: #333; 203 | } 204 | 205 | select { 206 | -moz-appearance: none; 207 | -webkit-appearance: none; 208 | appearance: none; 209 | background-color: #333; 210 | color: #bbb; 211 | padding: 2px 1.1rem 2px 6px; 212 | border: 1px solid #444; 213 | border-radius: 3px; 214 | font-size: 1rem; 215 | min-width: 1rem; 216 | box-sizing: content-box; 217 | box-shadow: none; 218 | position: relative; 219 | background-image: url("data:image/svg+xml;utf8,"); 220 | background-position: center right 0.5em; 221 | background-repeat: no-repeat; 222 | background-size: auto 25%; 223 | } 224 | 225 | select:hover { 226 | background-color: #3c3c3c; 227 | } 228 | 229 | select:active { 230 | background-color: #282828; 231 | } 232 | 233 | select:focus { 234 | border-color: #606060; 235 | outline: none; 236 | } 237 | 238 | option { 239 | background-color: #333; 240 | color: #bbb; 241 | } 242 | 243 | button:disabled, 244 | select:disabled, 245 | input:disabled, 246 | label.disabled { 247 | opacity: 0.4; 248 | pointer-events: none; 249 | } 250 | 251 | @media (hover: hover) { 252 | ::-webkit-scrollbar { 253 | width: 12px; 254 | height: 12px; 255 | } 256 | 257 | ::-webkit-scrollbar-track { 258 | background: #111; 259 | } 260 | 261 | ::-webkit-scrollbar-thumb { 262 | background: #444; 263 | } 264 | 265 | ::-webkit-scrollbar-thumb:hover { 266 | background: #555; 267 | } 268 | 269 | ::-webkit-scrollbar-corner { 270 | background: #111; 271 | } 272 | } 273 | 274 | @-moz-document url-prefix() { 275 | body { 276 | scrollbar-color: #444 #111; 277 | scrollbar-width: auto; 278 | } 279 | } -------------------------------------------------------------------------------- /js/misc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | { 4 | // Application settings 5 | let appSettings; 6 | const settingsStorage = "pulseq-settings"; 7 | 8 | window.getAppSettings = function (key) { 9 | if (appSettings) 10 | return appSettings[key]; 11 | 12 | let str = localStorage.getItem(settingsStorage); 13 | if (!str) { 14 | console.log("no settings in storage"); 15 | return; 16 | } 17 | 18 | try { 19 | appSettings = JSON.parse(str); 20 | } catch { 21 | console.log("Can not parse settings from local storage"); 22 | localStorage.removeItem(settingsStorage); 23 | appSettings = {}; 24 | } 25 | 26 | return appSettings[key]; 27 | } 28 | 29 | window.setAppSettings = function (key, value) { 30 | if (!appSettings) 31 | appSettings = {}; 32 | 33 | appSettings[key] = value; 34 | localStorage.setItem(settingsStorage, JSON.stringify(appSettings)); 35 | } 36 | } 37 | 38 | { 39 | // Set UI zoom on startup 40 | let zoom = getAppSettings("zoom"); 41 | if (zoom) 42 | document.body.style.zoom = zoom + "%"; 43 | } 44 | 45 | { 46 | // Switch tabs 47 | let tabs = document.querySelectorAll(".js-tab"); 48 | let containers = document.querySelectorAll(".js-view-container"); 49 | window.g_activeTab = "arrange"; 50 | 51 | window.g_switchTab = function (tabName) { 52 | let tabId = tabName + "-tab"; 53 | let viewId = tabName + "-view"; 54 | 55 | let cTab = document.getElementById(tabId); 56 | let cView = document.getElementById(viewId); 57 | 58 | if (!cTab || !cView) 59 | return; 60 | 61 | window.g_activeTab = tabName; 62 | 63 | containers.forEach(e => { 64 | e.classList.add("view--hidden") 65 | }); 66 | 67 | tabs.forEach(e => { 68 | e.classList.remove("tab--active") 69 | }); 70 | 71 | cTab.classList.add("tab--active"); 72 | cView.classList.remove("view--hidden"); 73 | } 74 | 75 | tabs.forEach((elem) => { 76 | elem.addEventListener("click", (event) => { 77 | let tabName = event.target.id.replace("-tab", ""); 78 | g_switchTab(tabName); 79 | }); 80 | }); 81 | } 82 | 83 | { 84 | // Fullscreen mode 85 | let fullBtn = document.getElementById("button-fullscreen"); 86 | let fullCheck = document.getElementById("input-fullscreen"); 87 | let paddingCheck = document.getElementById("input-fullscreen-padding"); 88 | let container = document.getElementById("container"); 89 | 90 | fullBtn.onclick = () => { 91 | if (document.fullscreenElement || 92 | document.webkitFullscreenElement || 93 | document.msFullscreenElement) 94 | exitFullscrn(); 95 | else 96 | enterFullScrn(); 97 | }; 98 | 99 | fullCheck.onchange = () => { 100 | if (fullCheck.checked) 101 | enterFullScrn(); 102 | else 103 | exitFullscrn(); 104 | }; 105 | 106 | paddingCheck.onchange = () => { 107 | if (paddingCheck.checked) { 108 | enterFullScrn(); 109 | 110 | if (fullCheck.checked) 111 | g_triggerResize(); 112 | else 113 | fullCheck.checked = true; 114 | } else { 115 | container.classList.remove("fullscreen-padding"); 116 | g_triggerResize(); 117 | } 118 | }; 119 | 120 | document.addEventListener("fullscreenchange", () => { 121 | if (!document.fullscreenElement && 122 | !document.webkitFullscreenElement && 123 | !document.msFullscreenElement) { 124 | container.classList.remove("fullscreen-padding"); 125 | fullCheck.checked = false; 126 | } else { 127 | fullCheck.checked = true; 128 | } 129 | 130 | g_triggerResize(); 131 | }); 132 | 133 | function exitFullscrn() { 134 | if (document.exitFullscreen) 135 | document.exitFullscreen(); 136 | else if (document.webkitExitFullscreen) 137 | document.webkitExitFullscreen(); 138 | else if (document.msExitFullscreen) 139 | document.msExitFullscreen(); 140 | }; 141 | 142 | function enterFullScrn() { 143 | if (paddingCheck.checked) 144 | container.classList.add("fullscreen-padding"); 145 | 146 | let elem = document.documentElement; 147 | if (elem.requestFullscreen) 148 | elem.requestFullscreen(); 149 | else if (elem.webkitRequestFullscreen) 150 | elem.webkitRequestFullscreen(); 151 | else if (elem.msRequestFullscreen) 152 | elem.msRequestFullscreen(); 153 | } 154 | } 155 | 156 | { 157 | // Fit footer height 158 | let patternFooter = document.getElementById("pattern-footer"); 159 | let patternContainer = document.getElementById("pattern-container"); 160 | createHeightObserver(patternFooter, patternObserverCallback); 161 | 162 | function patternObserverCallback(h) { 163 | patternContainer.style.marginBottom = h + "px"; 164 | } 165 | 166 | let arrangeFooter = document.getElementById("arrange-footer"); 167 | let arrangeContainer = document.getElementById("arrange-container"); 168 | createHeightObserver(arrangeFooter, arrangeObserverCallback); 169 | 170 | function arrangeObserverCallback(h) { 171 | arrangeContainer.style.marginBottom = h + "px"; 172 | } 173 | 174 | let synthFooter = document.getElementById("synth-footer"); 175 | let synthContainer = document.getElementById("synth-main"); 176 | createHeightObserver(synthFooter, synthObserverCallback); 177 | 178 | function synthObserverCallback(h) { 179 | synthContainer.style.marginBottom = h + "px"; 180 | } 181 | 182 | let synthListFooter = document.getElementById("synth-list-footer"); 183 | let synthListContainer = document.getElementById("synth-list-main"); 184 | createHeightObserver(synthListFooter, listObserverCallback); 185 | 186 | function listObserverCallback(h) { 187 | synthListContainer.style.marginBottom = h + "px"; 188 | } 189 | 190 | function createHeightObserver(target, callback) { 191 | if (!window.ResizeObserver) { 192 | console.log("ResizeObserver not supported"); 193 | return; 194 | } 195 | 196 | let observer = new ResizeObserver((list) => { 197 | for (let item of list) { 198 | callback(item.target.offsetHeight); 199 | } 200 | }); 201 | 202 | observer.observe(target); 203 | } 204 | 205 | window.g_triggerResize = function () { 206 | patternObserverCallback(patternFooter.offsetHeight); 207 | arrangeObserverCallback(arrangeFooter.offsetHeight); 208 | synthObserverCallback(synthFooter.offsetHeight); 209 | listObserverCallback(synthListFooter.offsetHeight); 210 | } 211 | } 212 | 213 | { 214 | // Tab-trap 215 | let areas = document.querySelectorAll(".focus-lock-area, .modal-container"); 216 | 217 | areas.forEach((area) => { 218 | let elements = area.querySelectorAll("input, button, a, textarea"); 219 | 220 | if (elements.length <= 0) 221 | return; 222 | 223 | let first = elements[0]; 224 | let last = elements[elements.length - 1]; 225 | 226 | let trap = document.createElement("DIV"); 227 | trap.setAttribute("tabindex", 0); 228 | trap.classList.add("tab-trap"); 229 | area.before(trap); 230 | 231 | trap.addEventListener("keydown", (event) => { 232 | if (event.key == "Tab") { 233 | event.preventDefault(); 234 | } 235 | }); 236 | 237 | first.addEventListener("keydown", (event) => { 238 | if (event.key == "Tab" && event.shiftKey) { 239 | last.focus(); 240 | event.preventDefault(); 241 | } 242 | }); 243 | 244 | last.addEventListener("keydown", (event) => { 245 | if (event.key == "Tab" && !event.shiftKey) { 246 | first.focus(); 247 | event.preventDefault(); 248 | } 249 | }); 250 | }); 251 | } -------------------------------------------------------------------------------- /css/table-pattern.css: -------------------------------------------------------------------------------- 1 | #pattern-main table { 2 | border-spacing: 0; 3 | -webkit-user-select: none; 4 | user-select: none; 5 | } 6 | 7 | #pattern-main td { 8 | border-bottom: 1px solid #444; 9 | border-right: 1px solid #444; 10 | width: 20px; 11 | min-width: 20px; 12 | height: 20px; 13 | position: relative; 14 | box-sizing: border-box; 15 | } 16 | 17 | #pattern-main table tr:first-child td { 18 | position: -webkit-sticky; 19 | position: sticky; 20 | top: 0; 21 | background-color: #111; 22 | color: #444; 23 | border-right: none; 24 | z-index: 1; 25 | font-size: 1rem; 26 | } 27 | 28 | #pattern-main table tr:first-child td:first-child { 29 | z-index: 2; 30 | left: 0; 31 | top: 0; 32 | border-bottom: 1px solid #444; 33 | border-right: 1px solid #444; 34 | border-left: none; 35 | } 36 | 37 | #pattern-main table #seq-volume-cell { 38 | width: 0; 39 | min-width: 0; 40 | border: none; 41 | right: 0; 42 | } 43 | 44 | #pattern-main tr:nth-child(12n+13) td { 45 | border-bottom: 1px solid #6a6a6a; 46 | } 47 | 48 | #pattern-main table tr:not(:first-child):not(:last-child) td:nth-child(4n+2):not(.shade) { 49 | background-color: #1e1e1e; 50 | } 51 | 52 | #pattern-main td.shade { 53 | background-color: #1a343a; 54 | box-shadow: inset 0 0 0 1px #111; 55 | } 56 | 57 | #pattern-main table tr:first-child td:nth-child(4n+5) { 58 | border-right: 1px solid #444; 59 | color: #555; 60 | } 61 | 62 | #pattern-main table td:first-child { 63 | background-color: #ccc; 64 | width: 39px; 65 | min-width: 39px; 66 | position: -webkit-sticky; 67 | position: sticky; 68 | left: 0; 69 | z-index: 1; 70 | border-bottom: 1px solid #111; 71 | border-top: none; 72 | border-right: 1px solid #444; 73 | border-left: 1px solid #444; 74 | font-weight: bold; 75 | font-size: 1rem; 76 | } 77 | 78 | #pattern-main table td:nth-child(2) { 79 | border-left: 1px solid #444; 80 | min-width: 21px; 81 | } 82 | 83 | #pattern-main table td.pattern-black-key { 84 | background-color: #333; 85 | border-right: 8px solid #111; 86 | border-top: 1px solid #111; 87 | border-bottom: 2px solid #111; 88 | color: transparent; 89 | } 90 | 91 | #pattern-main table td.key--inscale { 92 | color: #48a; 93 | } 94 | 95 | #pattern-main table td.key--root { 96 | color: #46e; 97 | } 98 | 99 | #pattern-main table td.key--pressed { 100 | border-color: #d44; 101 | } 102 | 103 | #note-length-control { 104 | position: absolute; 105 | box-sizing: border-box; 106 | top: -1px; 107 | left: 0; 108 | width: 51px; 109 | height: 27px; 110 | background-color: #111; 111 | border: 1px solid #696969; 112 | border-bottom-right-radius: 3px; 113 | font-weight: normal; 114 | } 115 | 116 | #note-length-control:before { 117 | content: "−"; 118 | position: absolute; 119 | top: 2px; 120 | bottom: 2px; 121 | left: 4px; 122 | right: 4px; 123 | pointer-events: none; 124 | color: #888; 125 | } 126 | 127 | #note-length-control:after { 128 | content: "+"; 129 | position: absolute; 130 | top: 2px; 131 | bottom: 2px; 132 | right: 4px; 133 | pointer-events: none; 134 | color: #888; 135 | } 136 | 137 | .control-fill-25:before { 138 | background-image: linear-gradient(90deg, #55c 0px, #55c 25%, transparent 25%); 139 | } 140 | 141 | .control-fill-50:before { 142 | background-image: linear-gradient(90deg, #55c 0px, #55c 50%, transparent 50%); 143 | } 144 | 145 | .control-fill-75:before { 146 | background-image: linear-gradient(90deg, #55c 0px, #55c 75%, transparent 75%); 147 | } 148 | 149 | .control-fill-99:before { 150 | background-image: linear-gradient(90deg, #55c 0px, #55c 94%, transparent 75%); 151 | border-right: 2px solid #696969; 152 | } 153 | 154 | .control-fill-100:before { 155 | background-image: linear-gradient(90deg, #55c 0px, #55c 100%); 156 | } 157 | 158 | #note-volume-control { 159 | position: absolute; 160 | top: -1px; 161 | right: 0; 162 | width: 51px; 163 | height: 27px; 164 | background-color: #111; 165 | border: 1px solid #696969; 166 | box-sizing: border-box; 167 | background-image: url("data:image/svg+xml;utf8,"); 168 | background-position: 1px 2px; 169 | background-size: 47px 21px; 170 | background-repeat: no-repeat; 171 | z-index: 2; 172 | border-bottom-left-radius: 3px; 173 | } 174 | 175 | #note-volume-control:before { 176 | content: "−"; 177 | position: absolute; 178 | top: 2px; 179 | bottom: 2px; 180 | left: 4px; 181 | pointer-events: none; 182 | color: #888; 183 | } 184 | 185 | #note-volume-control:after { 186 | content: "+"; 187 | position: absolute; 188 | top: 2px; 189 | bottom: 2px; 190 | right: 4px; 191 | pointer-events: none; 192 | color: #888; 193 | } 194 | 195 | #pattern-main td.shade:before { 196 | background-image: linear-gradient(90deg, transparent 0px, #089 9px, transparent 18px); 197 | } 198 | 199 | .fill-25:before { 200 | content: ""; 201 | position: absolute; 202 | top: 0; 203 | left: 0; 204 | bottom: 0; 205 | right: 70%; 206 | background-color: #55f; 207 | pointer-events: none; 208 | border: 1px solid #111; 209 | } 210 | 211 | .fill-50:before { 212 | content: ""; 213 | position: absolute; 214 | top: 0; 215 | left: 0; 216 | bottom: 0; 217 | right: 45%; 218 | background-color: #55f; 219 | pointer-events: none; 220 | border: 1px solid #111; 221 | } 222 | 223 | .fill-75:before { 224 | content: ""; 225 | position: absolute; 226 | top: 0; 227 | left: 0; 228 | bottom: 0; 229 | right: 20%; 230 | background-color: #55f; 231 | pointer-events: none; 232 | border: 1px solid #111; 233 | } 234 | 235 | .fill-99:before { 236 | content: ""; 237 | position: absolute; 238 | top: 0; 239 | left: 0; 240 | bottom: 0; 241 | right: 0; 242 | background-color: #55f; 243 | pointer-events: none; 244 | border: 1px solid #111; 245 | } 246 | 247 | .fill-100:before { 248 | content: ""; 249 | position: absolute; 250 | top: 0; 251 | left: 0; 252 | bottom: 0; 253 | right: -1px; 254 | background-color: #55f; 255 | pointer-events: none; 256 | border: 1px solid #111; 257 | border-right: none; 258 | } 259 | 260 | .fill-100 + td:before { 261 | border-left: none; 262 | } 263 | 264 | .vol--6:before { 265 | box-shadow: inset 0 2px 0 0 rgba(0, 0, 0, 0.3); 266 | } 267 | 268 | .vol--12:before { 269 | box-shadow: inset 0 3px 0 0 rgba(0, 0, 0, 0.3); 270 | } 271 | 272 | .vol--18:before { 273 | box-shadow: inset 0 4px 0 0 rgba(0, 0, 0, 0.3); 274 | } 275 | 276 | .vol--24:before { 277 | box-shadow: inset 0 5px 0 0 rgba(0, 0, 0, 0.3); 278 | } 279 | 280 | .vol--30:before { 281 | box-shadow: inset 0 6px 0 0 rgba(0, 0, 0, 0.3); 282 | } 283 | 284 | .vol--36:before { 285 | box-shadow: inset 0 7px 0 0 rgba(0, 0, 0, 0.3); 286 | } 287 | 288 | .vol--42:before { 289 | box-shadow: inset 0 8px 0 0 rgba(0, 0, 0, 0.3); 290 | } 291 | 292 | .vol--48:before { 293 | box-shadow: inset 0 9px 0 0 rgba(0, 0, 0, 0.3); 294 | } 295 | 296 | .vol--54:before { 297 | box-shadow: inset 0 10px 0 0 rgba(0, 0, 0, 0.3); 298 | } 299 | 300 | .vol--60:before { 301 | box-shadow: inset 0 11px 0 0 rgba(0, 0, 0, 0.3); 302 | } 303 | 304 | .vol--66:before { 305 | box-shadow: inset 0 12px 0 0 rgba(0, 0, 0, 0.3); 306 | } 307 | 308 | .vol--72:before { 309 | box-shadow: inset 0 13px 0 0 rgba(0, 0, 0, 0.3); 310 | } 311 | 312 | .vol--78:before { 313 | box-shadow: inset 0 14px 0 0 rgba(0, 0, 0, 0.3); 314 | } 315 | 316 | .vol--84:before { 317 | box-shadow: inset 0 15px 0 0 rgba(0, 0, 0, 0.3); 318 | } 319 | 320 | .vol--90:before { 321 | box-shadow: inset 0 16px 0 0 rgba(0, 0, 0, 0.3); 322 | } 323 | 324 | .vol--96:before { 325 | box-shadow: inset 0 17px 0 0 rgba(0, 0, 0, 0.3); 326 | } 327 | 328 | #pattern-main table tr:last-child td { 329 | bottom: 0; 330 | position: -webkit-sticky; 331 | position: sticky; 332 | z-index: 1; 333 | border-top: 1px solid #444; 334 | height: 30px; 335 | } 336 | 337 | #pattern-main table tr:last-child td:first-child { 338 | z-index: 2; 339 | overflow: visible; 340 | border: 1px solid #555; 341 | background-color: #333; 342 | } 343 | 344 | #pattern-auto-row td { 345 | background-color: #111; 346 | } 347 | 348 | #pattern-auto-row td.active { 349 | background-color: #241a34; 350 | } 351 | 352 | #pattern-auto-row .filter-bar { 353 | position: absolute; 354 | left: 1px; 355 | bottom: 1px; 356 | width: 6px; 357 | background-color: #333; 358 | pointer-events: none; 359 | } 360 | 361 | #pattern-auto-row .filter-bar:last-child { 362 | left: 8px; 363 | } 364 | 365 | #pattern-auto-row td.active .filter-bar { 366 | background-color: #b4d; 367 | } 368 | 369 | #pattern-auto-row td.active .filter-bar:last-child { 370 | background-color: #97d; 371 | } 372 | 373 | #pattern-auto-row.inactive td { 374 | background-color: #090909; 375 | } 376 | 377 | #pattern-auto-row.inactive td.active { 378 | background-color: #322; 379 | } 380 | 381 | #pattern-auto-row.inactive td > div { 382 | display: none; 383 | } 384 | 385 | #automation-levels-set .filter-bar { 386 | position: absolute; 387 | left: 1px; 388 | bottom: 1px; 389 | width: 12px; 390 | background-color: #b4d; 391 | pointer-events: none; 392 | } 393 | 394 | #automation-levels-set .filter-bar:last-child { 395 | position: absolute; 396 | left: 14px; 397 | bottom: 1px; 398 | width: 12px; 399 | background-color: #97d; 400 | pointer-events: none; 401 | } 402 | 403 | #automation-levels-control { 404 | position: absolute; 405 | left: 2px; 406 | bottom: 32px; 407 | height: 208px; 408 | width: 64px; 409 | border: 1px solid #696969; 410 | border-radius: 3px; 411 | background-color: #191919; 412 | } 413 | 414 | #automation-levels-control > span { 415 | display: inline-block; 416 | width: 100%; 417 | text-align: center; 418 | margin-top: 4px; 419 | font-weight: normal; 420 | color: #ddd; 421 | } 422 | 423 | #automation-levels-control > span:before { 424 | content: "F"; 425 | position: absolute; 426 | top: 1.4rem; 427 | left: 4px; 428 | width: 24px; 429 | text-align: center; 430 | color: #555; 431 | } 432 | 433 | #automation-levels-control > span:after { 434 | content: "P"; 435 | position: absolute; 436 | top: 1.4rem; 437 | right: 4px; 438 | width: 24px; 439 | text-align: center; 440 | color: #555; 441 | } 442 | 443 | #automation-levels-control input[type=range] { 444 | height: 28px; 445 | position: absolute; 446 | bottom: -25px; 447 | left: 3px; 448 | width: 160px; 449 | transform-origin: top left; 450 | transform: rotate(-90deg); 451 | background-image: url("../img/range-short.svg"); 452 | } 453 | 454 | #automation-levels-control input[type=range]:last-child { 455 | left: 33px; 456 | } -------------------------------------------------------------------------------- /js/scheduler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function Scheduler(songObj, barCallback, stepCallback) { 4 | Tone.Transport.bpm.value = songObj.bpm; 5 | 6 | let isPlaying = false; 7 | let isPatternPlaying = false; 8 | let onInnerStopCallback = null; 9 | let schedulerId = null; 10 | 11 | this.stop = () => { 12 | stop(); 13 | 14 | if (onInnerStopCallback) { 15 | onInnerStopCallback(); 16 | onInnerStopCallback = null; 17 | } 18 | } 19 | 20 | this.playSong = () => { 21 | stop(); 22 | 23 | for (let i = 0; i < songObj.synths.length; i++) { 24 | songObj.synths[i].resetFilter(); 25 | songObj.synths[i].resetPorta(); 26 | } 27 | 28 | isPlaying = true; 29 | isPatternPlaying = false; 30 | scheduleSong(); 31 | Tone.Transport.start(); 32 | console.log("Play SONG"); 33 | } 34 | 35 | this.playLoop = (callback, barsInLoop) => { 36 | stop(); 37 | 38 | onInnerStopCallback = callback; 39 | 40 | isPlaying = true; 41 | isPatternPlaying = false; 42 | scheduleLoop(barsInLoop * songObj.barSteps); 43 | Tone.Transport.start(); 44 | console.log("Play LOOP"); 45 | 46 | return songObj.arrangeStartPoint; 47 | } 48 | 49 | this.playPattern = () => { 50 | stop(); 51 | 52 | isPlaying = true; 53 | isPatternPlaying = true; 54 | schedulePattern(); 55 | Tone.Transport.start(); 56 | console.log("Play PATTERN"); 57 | } 58 | 59 | this.release = () => { 60 | for (let i = 0; i < songObj.synths.length; i++) 61 | songObj.synths[i].triggerRelease(); 62 | } 63 | 64 | this.releasePattern = () => { 65 | if (isPatternPlaying) 66 | this.release(); 67 | } 68 | 69 | this.playStopSong = (callback) => { 70 | if (isPlaying) { 71 | stop(); 72 | return false; 73 | } else { 74 | onInnerStopCallback = callback; 75 | this.playSong(); 76 | return true; 77 | } 78 | } 79 | 80 | this.playStopPattern = (callback) => { 81 | if (isPlaying || !songObj.currentPattern) { 82 | stop(); 83 | return false; 84 | } else { 85 | onInnerStopCallback = callback; 86 | this.playPattern(); 87 | return true; 88 | } 89 | } 90 | 91 | this.renderSong = (renderLength) => { 92 | this.stop(); 93 | 94 | let schedulerData = { 95 | stepIndex: 0, 96 | barIndex: 0, 97 | swingedStep: false, 98 | queue: [] 99 | }; 100 | 101 | console.log("Render SONG: " + length + "sec."); 102 | let lSynths = []; 103 | 104 | // Promise 105 | return Tone.Offline(({ transport }) => { 106 | transport.bpm.value = songObj.bpm; 107 | let compressor = new Tone.Compressor(songObj.compressorThreshold, songObj.compressorRatio); 108 | compressor.toDestination(); 109 | 110 | for (let i = 0; i < songObj.synthParams.length; i++) { 111 | console.log(" >> >> Offline synth " + i); 112 | lSynths[i] = new Synth(compressor, songObj.bpm); 113 | lSynths[i].mute(songObj.synths[i].isMuted); 114 | 115 | for (let key in songObj.synthParams[i]) 116 | synthParamApply(key, songObj.synthParams[i][key], lSynths[i]); 117 | } 118 | 119 | let synced = false; 120 | transport.scheduleRepeat(function (time) { 121 | if (!synced) { 122 | syncLfos(lSynths, time); 123 | synced = true; 124 | } 125 | 126 | performSchedulerStep(schedulerData, lSynths, time, null, songObj.song.length); 127 | }, "16n"); 128 | 129 | transport.start(0.2); 130 | }, renderLength); 131 | }; 132 | 133 | this.exportMidiSequence = (isOverlap, isExpand, velocityType) => { 134 | console.log("Export MIDI"); 135 | 136 | let lSynths = []; 137 | for (let i = 0; i < songObj.synthParams.length; i++) 138 | lSynths[i] = new MidSynth(songObj, i, { isOverlap, isExpand, velocityType }); 139 | 140 | let schedulerData = { 141 | stepIndex: 0, 142 | barIndex: 0, 143 | swingedStep: false, 144 | queue: [] 145 | }; 146 | 147 | let len = songObj.song.length * songObj.barSteps; 148 | let stepDuration = (60 / songObj.bpm) / 4; 149 | 150 | for (let i = 0; i < len; i++) { 151 | performSchedulerStep(schedulerData, lSynths, i * stepDuration, null, songObj.song.length); 152 | 153 | if (i % songObj.barSteps == 0) 154 | lSynths.forEach((e) => { e.setBarMarker(i * stepDuration) }); 155 | } 156 | 157 | let tracks = []; 158 | for (let i = 0; i < lSynths.length; i++) { 159 | let e = lSynths[i]; 160 | if (e.isEmpty) { 161 | console.log("empty track >>", songObj.synthNames[i]); 162 | } else { 163 | e.finish(); 164 | tracks.push(e.track); 165 | } 166 | } 167 | 168 | return { tracks }; 169 | }; 170 | 171 | function stop() { 172 | if (!isPlaying) 173 | return; 174 | 175 | if (schedulerId !== null) { 176 | Tone.Transport.clear(schedulerId); 177 | schedulerId = null; 178 | } 179 | Tone.Transport.cancel(Tone.now()); 180 | Tone.Transport.stop(Tone.now()); 181 | 182 | isPlaying = false; 183 | isPatternPlaying = false; 184 | console.log("STOP Playback"); 185 | 186 | scheduleCall(stepCallback, -1, Tone.now()); 187 | scheduleCall(barCallback, -1, Tone.now()); 188 | 189 | for (let i = 0; i < songObj.synths.length; i++) 190 | songObj.synths[i].triggerRelease(Tone.now()); 191 | } 192 | 193 | function syncLfos(synths, time) { 194 | for (let synth of synths) { 195 | if (synth.lfo1) { 196 | synth.lfo1.stop(time); 197 | synth.lfo1.start(time); 198 | } 199 | 200 | if (synth.lfo2) { 201 | synth.lfo2.stop(time); 202 | synth.lfo2.start(time); 203 | } 204 | } 205 | } 206 | 207 | function scheduleCall(callback, param, time) { 208 | Tone.Draw.schedule(() => { callback(param) }, time) 209 | } 210 | 211 | function schedulePattern() { 212 | let sequenceIndex = 0; 213 | let synced = false; 214 | let isSwingedStep = false; 215 | 216 | schedulerId = Tone.Transport.scheduleRepeat(function (time) { 217 | if (!synced) { 218 | syncLfos(songObj.synths, time); 219 | synced = true; 220 | } 221 | 222 | playPatternStep(sequenceIndex, songObj.currentPattern, songObj.synths, time, isSwingedStep); 223 | isSwingedStep = !isSwingedStep; 224 | 225 | scheduleCall(stepCallback, sequenceIndex, time); 226 | 227 | sequenceIndex++; 228 | if (sequenceIndex >= songObj.currentPattern.length) 229 | sequenceIndex = 0; 230 | 231 | if (sequenceIndex % songObj.barSteps == 0) 232 | isSwingedStep = false; 233 | }, "16n"); 234 | } 235 | 236 | function scheduleSong() { 237 | let schedulerData = { 238 | stepIndex: 0, 239 | barIndex: songObj.arrangeStartPoint, 240 | swingedStep: false, 241 | queue: [] 242 | }; 243 | let synced = false; 244 | 245 | schedulerId = Tone.Transport.scheduleRepeat(function (time) { 246 | if (!synced) { 247 | syncLfos(songObj.synths, time); 248 | synced = true; 249 | } 250 | 251 | performSchedulerStep(schedulerData, songObj.synths, time, barCallback, songObj.playableLength + 1); 252 | }, "16n"); 253 | } 254 | 255 | function scheduleLoop(steps) { 256 | let startPoint = songObj.arrangeStartPoint; 257 | let schedulerData = { 258 | stepIndex: 0, 259 | barIndex: startPoint, 260 | swingedStep: false, 261 | queue: [] 262 | }; 263 | let synced = false; 264 | 265 | schedulerId = Tone.Transport.scheduleRepeat(function (time) { 266 | if (!synced) { 267 | syncLfos(songObj.synths, time); 268 | synced = true; 269 | } 270 | 271 | if (schedulerData.stepIndex >= steps) { 272 | schedulerData.stepIndex = 0; 273 | schedulerData.barIndex = startPoint; 274 | schedulerData.queue = []; 275 | for (let i = 0; i < songObj.synths.length; i++) 276 | songObj.synths[i].triggerRelease(time); 277 | } 278 | 279 | performSchedulerStep(schedulerData, songObj.synths, time, barCallback, songObj.song.length); 280 | }, "16n"); 281 | } 282 | 283 | function performSchedulerStep(data, synths, time, realtimeBarCallback, stopPoint) { 284 | if (data.stepIndex % songObj.barSteps == 0) { 285 | if (data.barIndex >= stopPoint) { 286 | 287 | if (realtimeBarCallback) { 288 | console.log("Song END"); 289 | stop(); 290 | if (onInnerStopCallback) { 291 | onInnerStopCallback(); 292 | onInnerStopCallback = null; 293 | } 294 | } 295 | 296 | return; 297 | } 298 | 299 | if (realtimeBarCallback) 300 | scheduleCall(realtimeBarCallback, data.barIndex, time); 301 | 302 | for (let i = 0; i < songObj.song[data.barIndex].length; i++) { 303 | if (songObj.song[data.barIndex][i]) 304 | data.queue.push({ pattern: i, index: 0 }); 305 | } 306 | 307 | data.barIndex++; 308 | data.swingedStep = false; 309 | } 310 | 311 | for (let i = 0; i < data.queue.length; i++) { 312 | let stepIndex = data.queue[i].index; 313 | let pind = data.queue[i].pattern; 314 | let pattern = songObj.patterns[pind]; 315 | 316 | playPatternStep(stepIndex, pattern, synths, time, data.swingedStep); 317 | data.queue[i].index++; 318 | } 319 | 320 | for (let i = data.queue.length - 1; i >= 0; i--) { 321 | if (songObj.patterns[data.queue[i].pattern].length <= data.queue[i].index) 322 | data.queue.splice(i, 1); 323 | } 324 | 325 | data.stepIndex++; 326 | data.swingedStep = !data.swingedStep; 327 | } 328 | 329 | function playPatternStep(stepIndex, pattern, synths, time, isSwingedStep) { 330 | for (let j = 0; j < pattern.patternData.length; j++) { 331 | let notes = pattern.patternData[j].notes; 332 | let lengths = pattern.patternData[j].lengths; 333 | let note = notes[stepIndex]; 334 | let volume = pattern.patternData[j].volumes[stepIndex]; 335 | let synthIndex = pattern.patternData[j].synthIndex; 336 | 337 | let filterF = pattern.patternData[j].filtF[stepIndex]; 338 | let filterQ = pattern.patternData[j].filtQ[stepIndex]; 339 | 340 | if (synthIndex !== null) { 341 | let synth = synths[synthIndex]; 342 | 343 | let attackTime = time; 344 | let lenCoef = lengths[stepIndex] / 100; 345 | let stepLen = (60 / songObj.bpm) / 4 - 0.001; 346 | let stopTime = time + lenCoef * stepLen; 347 | 348 | synth.filterSweep(filterF, filterQ, time, stepLen); 349 | 350 | if (!note) 351 | continue; 352 | 353 | if (songObj.swing && isSwingedStep) { 354 | attackTime = time + stepLen * songObj.swing / 200; 355 | stopTime = Math.min(attackTime + lenCoef * stepLen, time + stepLen); 356 | } 357 | 358 | if (stepIndex > 0 && lengths[stepIndex - 1] >= 100) 359 | synth.glideTo(note, volume, time, stepLen) 360 | else 361 | synth.triggerAttack(note, volume, attackTime, stepLen); 362 | 363 | if (lengths[stepIndex] < 100 || stepIndex == pattern.length - 1 || !notes[stepIndex + 1]) 364 | synth.triggerRelease(stopTime); 365 | } 366 | } 367 | } 368 | } -------------------------------------------------------------------------------- /js/synth-ui.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function SynthUi(songObj) { 4 | const rowKeys = ["KeyQ", "KeyW", "KeyE", "KeyR", "KeyT", "KeyY", "KeyU", "KeyI", "KeyO", "KeyP", "BracketLeft", "BracketRight", "Backslash", 5 | "KeyA", "KeyS", "KeyD", "KeyF", "KeyG", "KeyH", "KeyJ", "KeyK", "KeyL", "Semicolon", "Quote"]; 6 | const rowNotes = ["F3", "G3", "A3", "B3", "Db4", "Eb4", "F4", "G4", "A4", "B4", "Db5", "Eb5", "E5", 7 | "Gb3", "Ab3", "Bb3", "C4", "D4", "E4", "Gb4", "Ab4", "Bb4", "C5", "D5"]; 8 | const rowSymbols = ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\", 9 | "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'"]; 10 | 11 | let rowDomKeys = new Array(rowNotes.length); 12 | let lastDomKey; 13 | let lastKey = null; 14 | let showLetters = false; 15 | 16 | let pianoScroll = 260; 17 | let pianoOuter = document.getElementById("piano-container-outer"); 18 | let pianoContainer = document.getElementById("piano-container"); 19 | pianoContainer.oncontextmenu = () => false; 20 | 21 | let btnShow = document.getElementById("button-piano-show"); 22 | btnShow.onclick = () => { 23 | if (pianoContainer.classList.contains("piano--hidden")) { 24 | pianoContainer.classList.remove("piano--hidden"); 25 | pianoContainer.classList.remove("show-letters"); 26 | pianoOuter.classList.remove("piano--scrolllock"); 27 | pianoOuter.scroll(pianoScroll, 0); 28 | showLetters = false; 29 | } else { 30 | let notScrollable = pianoContainer.offsetWidth < document.body.clientWidth; 31 | let noTouchInput = !("ontouchstart" in window); 32 | if (pianoOuter.classList.contains("piano--scrolllock") || notScrollable || noTouchInput) { 33 | pianoScroll = pianoOuter.scrollLeft; 34 | pianoContainer.classList.add("piano--hidden"); 35 | btnShow.classList.remove("button--highlight-yellow"); 36 | } else { 37 | pianoOuter.classList.add("piano--scrolllock"); 38 | btnShow.classList.add("button--highlight-yellow"); 39 | showToast("Scroll Lock"); 40 | } 41 | } 42 | } 43 | 44 | let lastNote = ""; 45 | let isPlaying = false; 46 | let pianoTouched = false; 47 | 48 | const upperRow = document.createElement("DIV"); 49 | const lowerRow = document.createElement("DIV"); 50 | 51 | const playSynth = (note, newDomKey) => { 52 | let stepLen = (60 / songObj.bpm) / 4; 53 | 54 | this.currentSynth.filterSweep(null, null, Tone.now(), 0.01) 55 | 56 | if (isPlaying) 57 | this.currentSynth.glideTo(note, 0, Tone.now(), stepLen); 58 | else 59 | this.currentSynth.triggerAttack(note, 0, Tone.now(), stepLen); 60 | 61 | isPlaying = true; 62 | lastNote = note; 63 | 64 | lastDomKey.classList.remove("key--pressed"); 65 | lastDomKey = newDomKey; 66 | lastDomKey.classList.add("key--pressed"); 67 | } 68 | 69 | const stopSynth = () => { 70 | this.currentSynth.triggerRelease(); 71 | isPlaying = false; 72 | lastNote = ""; 73 | lastDomKey.classList.remove("key--pressed"); 74 | } 75 | 76 | window.addEventListener("blur", () => { 77 | if (isPlaying) 78 | stopSynth(); 79 | }); 80 | 81 | for (let i = 0; i < DEFAULT_PARAMS.noteSet.length; i++) { 82 | let key = document.createElement("DIV"); 83 | key.classList.add("piano-key"); 84 | let note = DEFAULT_PARAMS.noteSet[i]; 85 | 86 | let indexInKeys = rowNotes.indexOf(note); 87 | if (indexInKeys != -1) { 88 | rowDomKeys[indexInKeys] = key; 89 | let s = document.createElement("SPAN"); 90 | s.classList.add("letter-mark"); 91 | s.appendChild(document.createTextNode(rowSymbols[indexInKeys])); 92 | key.appendChild(s); 93 | } 94 | 95 | if (note.indexOf("b") == -1) { 96 | key.classList.add("piano-key-white"); 97 | let s = document.createElement("SPAN"); 98 | s.classList.add("note-mark"); 99 | s.appendChild(document.createTextNode(note)); 100 | 101 | if (note == "C4") 102 | s.classList.add("c4-key-mark"); 103 | 104 | key.appendChild(s); 105 | } else { 106 | key.classList.add("piano-key-black"); 107 | } 108 | 109 | if (i % 2 == 0) 110 | lowerRow.appendChild(key); 111 | else 112 | upperRow.appendChild(key); 113 | 114 | key.addEventListener("pointerdown", () => { 115 | if (isPlaying && lastNote == note) 116 | return; 117 | 118 | playSynth(note, key); 119 | }); 120 | 121 | key.addEventListener("pointerup", () => { 122 | if (pianoTouched) 123 | return; 124 | 125 | if (lastKey && note != lastNote) 126 | return; 127 | 128 | stopSynth(); 129 | }); 130 | 131 | key.addEventListener("pointercancel", () => { 132 | if (pianoTouched) 133 | return; 134 | 135 | stopSynth(); 136 | }); 137 | 138 | key.addEventListener("touchstart", () => { 139 | pianoTouched = true; 140 | 141 | if (isPlaying && lastNote == note) 142 | return; 143 | 144 | playSynth(note, key); 145 | }); 146 | 147 | key.addEventListener("touchend", () => { 148 | if (note != lastNote) 149 | return; 150 | 151 | pianoTouched = false; 152 | stopSynth(); 153 | }); 154 | 155 | key.addEventListener("touchcancel", () => { 156 | pianoTouched = false; 157 | stopSynth(); 158 | }); 159 | } 160 | 161 | pianoContainer.appendChild(upperRow); 162 | pianoContainer.appendChild(lowerRow); 163 | pianoOuter.scroll(pianoScroll, 0); 164 | lastDomKey = rowDomKeys[0]; 165 | 166 | document.addEventListener("keydown", (e) => { 167 | if (e.target.tagName == "INPUT" && e.target.type != "range" && e.target.type != "checkbox") 168 | return; 169 | 170 | if (e.target.tagName == "SELECT") 171 | e.preventDefault(); 172 | 173 | if (e.code == "Slash" || e.code == "Quote") 174 | e.preventDefault(); 175 | 176 | if (e.code == lastKey) 177 | return; 178 | 179 | lastKey = e.code; 180 | 181 | let keyIndex = rowKeys.indexOf(e.code); 182 | if (keyIndex == -1) 183 | return; 184 | 185 | if (!showLetters) { 186 | pianoContainer.classList.add("show-letters"); 187 | showLetters = true; 188 | } 189 | 190 | let note = rowNotes[keyIndex]; 191 | 192 | if (isPlaying && note == lastNote) 193 | return; 194 | 195 | playSynth(note, rowDomKeys[keyIndex]); 196 | }); 197 | 198 | document.addEventListener("keyup", (e) => { 199 | let keyIndex = rowKeys.indexOf(e.code); 200 | if (keyIndex == -1) 201 | return; 202 | 203 | let note = rowNotes[keyIndex]; 204 | 205 | if (e.code == lastKey) 206 | lastKey = null; 207 | 208 | if (note != lastNote) 209 | return; 210 | 211 | stopSynth(); 212 | }); 213 | 214 | if ("ontouchstart" in window) { 215 | let rangeInputs = document.querySelectorAll("#synth-main input[type=range]"); 216 | rangeInputs.forEach((e) => { 217 | e.addEventListener("pointercancel", (el) => { 218 | el.target.value = el.target.dataset.value; 219 | universalSynthListener(el); 220 | }); 221 | 222 | e.addEventListener("change", (el) => { 223 | el.target.dataset.value = el.target.value; 224 | }); 225 | }); 226 | } 227 | 228 | const universalSynthListener = (e) => { 229 | let idValue; 230 | if (e.target.type == "checkbox") 231 | idValue = synthParamApply(e.target.id, e.target.checked, this.currentSynth); 232 | else 233 | idValue = synthParamApply(e.target.id, e.target.value, this.currentSynth); 234 | 235 | if (idValue) { 236 | this.curSynthParamObj[idValue.id] = idValue.value; 237 | 238 | if (e.target.id != idValue.id) { 239 | let newTgt = document.getElementById(idValue.id); 240 | newTgt.value = idValue.value; 241 | newTgt.dataset.value = idValue.value; 242 | } 243 | } 244 | 245 | setBlockState(e.target); 246 | } 247 | 248 | let controls = document.querySelectorAll("#synth-main input, #synth-main select, #synth-main button"); 249 | controls.forEach((e) => { 250 | if (!e.id.startsWith("synth")) 251 | return; 252 | 253 | switch (e.tagName) { 254 | case "INPUT": 255 | e.addEventListener("input", universalSynthListener); 256 | break; 257 | 258 | case "SELECT": 259 | e.addEventListener("change", universalSynthListener); 260 | break; 261 | 262 | case "BUTTON": 263 | e.addEventListener("click", universalSynthListener); 264 | break; 265 | } 266 | }); 267 | 268 | this.assignSynth = (params, targetSynth, name) => { 269 | this.curSynthParamObj = params; 270 | this.currentSynth = targetSynth; 271 | 272 | for (let key in params) { 273 | let input = document.getElementById(key); 274 | if (input) { 275 | if (input.type == "checkbox") { 276 | input.checked = params[key]; 277 | } else { 278 | input.value = params[key]; 279 | input.dataset.value = params[key]; 280 | } 281 | 282 | setBlockState(input); 283 | } 284 | } 285 | 286 | document.getElementById("synth-name-area").textContent = name; 287 | this.updateMuteControls(); 288 | } 289 | 290 | this.updateMuteControls = () => { 291 | let muteButton = document.getElementById("button-synth-mute"); 292 | 293 | if (isSoloSynth(this.currentSynth)) 294 | muteButton.classList.add("button--highlight-blue"); 295 | else 296 | muteButton.classList.remove("button--highlight-blue"); 297 | 298 | if (this.currentSynth.isMuted) 299 | muteButton.classList.add("button--highlight-orange"); 300 | else 301 | muteButton.classList.remove("button--highlight-orange"); 302 | } 303 | 304 | function isSoloSynth(targetSynth) { 305 | if (targetSynth.isMuted) 306 | return false; 307 | 308 | if (songObj.synths.length < 2) 309 | return false; 310 | 311 | let mutedCount = 0; 312 | songObj.synths.forEach(e => { if (e.isMuted) mutedCount++ }); 313 | if (mutedCount == songObj.synths.length - 1) 314 | return true; 315 | else 316 | return false; 317 | } 318 | 319 | function setBlockState(selector) { 320 | if (selector.id == "synth-fx-type") { 321 | let range = document.getElementById("synth-fx-rate"); 322 | let span = document.getElementById("synth-fx-rate-span"); 323 | let checkbox = document.getElementById("synth-fx-sync"); 324 | let label = document.getElementById("synth-fx-sync-label"); 325 | let warning = document.getElementById("cpu-warning-span"); 326 | 327 | if ("stereo distort reverb".includes(selector.value)) { 328 | range.disabled = true; 329 | span.classList.add("disabled") 330 | } else { 331 | range.disabled = false; 332 | span.classList.remove("disabled") 333 | } 334 | 335 | if ("delay pingpong".includes(selector.value)) { 336 | checkbox.style.visibility = "visible"; 337 | label.style.visibility = "visible"; 338 | } else { 339 | checkbox.style.visibility = "hidden"; 340 | label.style.visibility = "hidden"; 341 | } 342 | 343 | if ("reverb phaser".includes(selector.value)) { 344 | warning.style.visibility = "visible"; 345 | } else { 346 | warning.style.visibility = "hidden"; 347 | } 348 | } 349 | 350 | if (selector.dataset.block) { 351 | let blocks = document.getElementsByClassName(selector.dataset.block); 352 | for (let el of blocks) { 353 | if (selector.value == "[none]") 354 | el.style.visibility = "hidden"; 355 | else 356 | el.style.visibility = "visible"; 357 | } 358 | } 359 | 360 | if (selector.dataset.edit) { 361 | let edit = document.getElementById(selector.dataset.edit); 362 | if (selector.value == "custom") 363 | edit.style.display = "inline"; 364 | else 365 | edit.style.display = "none"; 366 | } 367 | } 368 | 369 | } -------------------------------------------------------------------------------- /lib/midi-writer.js: -------------------------------------------------------------------------------- 1 | // data should be the same type of format returned by parseMidi 2 | // for maximum compatibililty, returns an array of byte values, suitable for conversion to Buffer, Uint8Array, etc. 3 | 4 | // opts: 5 | // - running reuse previous eventTypeByte when possible, to compress file 6 | // - useByte9ForNoteOff use 0x09 for noteOff when velocity is zero 7 | (function() { 8 | function writeMidi(data, opts) { 9 | if (typeof data !== 'object') 10 | throw 'Invalid MIDI data' 11 | 12 | opts = opts || {} 13 | 14 | var header = data.header || {} 15 | var tracks = data.tracks || [] 16 | var i, len = tracks.length 17 | 18 | var w = new Writer() 19 | writeHeader(w, header, len) 20 | 21 | for (i=0; i < len; i++) { 22 | writeTrack(w, tracks[i], opts) 23 | } 24 | 25 | return w.buffer 26 | } 27 | 28 | function writeHeader(w, header, numTracks) { 29 | var format = header.format == null ? 1 : header.format 30 | 31 | var timeDivision = 128 32 | if (header.timeDivision) { 33 | timeDivision = header.timeDivision 34 | } else if (header.ticksPerFrame && header.framesPerSecond) { 35 | timeDivision = (-(header.framesPerSecond & 0xFF) << 8) | (header.ticksPerFrame & 0xFF) 36 | } else if (header.ticksPerBeat) { 37 | timeDivision = header.ticksPerBeat & 0x7FFF 38 | } 39 | 40 | var h = new Writer() 41 | h.writeUInt16(format) 42 | h.writeUInt16(numTracks) 43 | h.writeUInt16(timeDivision) 44 | 45 | w.writeChunk('MThd', h.buffer) 46 | } 47 | 48 | function writeTrack(w, track, opts) { 49 | var t = new Writer() 50 | var i, len = track.length 51 | var eventTypeByte = null 52 | for (i=0; i < len; i++) { 53 | // Reuse last eventTypeByte when opts.running is set, or event.running is explicitly set on it. 54 | // parseMidi will set event.running for each event, so that we can get an exact copy by default. 55 | // Explicitly set opts.running to false, to override event.running and never reuse last eventTypeByte. 56 | if (opts.running === false || !opts.running && !track[i].running) eventTypeByte = null 57 | 58 | eventTypeByte = writeEvent(t, track[i], eventTypeByte, opts.useByte9ForNoteOff) 59 | } 60 | w.writeChunk('MTrk', t.buffer) 61 | } 62 | 63 | function writeEvent(w, event, lastEventTypeByte, useByte9ForNoteOff) { 64 | var type = event.type 65 | var deltaTime = event.deltaTime 66 | var text = event.text || '' 67 | var data = event.data || [] 68 | var eventTypeByte = null 69 | w.writeVarInt(deltaTime) 70 | 71 | switch (type) { 72 | // meta events 73 | case 'sequenceNumber': 74 | w.writeUInt8(0xFF) 75 | w.writeUInt8(0x00) 76 | w.writeVarInt(2) 77 | w.writeUInt16(event.number) 78 | break; 79 | 80 | case 'text': 81 | w.writeUInt8(0xFF) 82 | w.writeUInt8(0x01) 83 | w.writeVarInt(text.length) 84 | w.writeString(text) 85 | break; 86 | 87 | case 'copyrightNotice': 88 | w.writeUInt8(0xFF) 89 | w.writeUInt8(0x02) 90 | w.writeVarInt(text.length) 91 | w.writeString(text) 92 | break; 93 | 94 | case 'trackName': 95 | w.writeUInt8(0xFF) 96 | w.writeUInt8(0x03) 97 | w.writeVarInt(text.length) 98 | w.writeString(text) 99 | break; 100 | 101 | case 'instrumentName': 102 | w.writeUInt8(0xFF) 103 | w.writeUInt8(0x04) 104 | w.writeVarInt(text.length) 105 | w.writeString(text) 106 | break; 107 | 108 | case 'lyrics': 109 | w.writeUInt8(0xFF) 110 | w.writeUInt8(0x05) 111 | w.writeVarInt(text.length) 112 | w.writeString(text) 113 | break; 114 | 115 | case 'marker': 116 | w.writeUInt8(0xFF) 117 | w.writeUInt8(0x06) 118 | w.writeVarInt(text.length) 119 | w.writeString(text) 120 | break; 121 | 122 | case 'cuePoint': 123 | w.writeUInt8(0xFF) 124 | w.writeUInt8(0x07) 125 | w.writeVarInt(text.length) 126 | w.writeString(text) 127 | break; 128 | 129 | case 'channelPrefix': 130 | w.writeUInt8(0xFF) 131 | w.writeUInt8(0x20) 132 | w.writeVarInt(1) 133 | w.writeUInt8(event.channel) 134 | break; 135 | 136 | case 'portPrefix': 137 | w.writeUInt8(0xFF) 138 | w.writeUInt8(0x21) 139 | w.writeVarInt(1) 140 | w.writeUInt8(event.port) 141 | break; 142 | 143 | case 'endOfTrack': 144 | w.writeUInt8(0xFF) 145 | w.writeUInt8(0x2F) 146 | w.writeVarInt(0) 147 | break; 148 | 149 | case 'setTempo': 150 | w.writeUInt8(0xFF) 151 | w.writeUInt8(0x51) 152 | w.writeVarInt(3) 153 | w.writeUInt24(event.microsecondsPerBeat) 154 | break; 155 | 156 | case 'smpteOffset': 157 | w.writeUInt8(0xFF) 158 | w.writeUInt8(0x54) 159 | w.writeVarInt(5) 160 | var FRAME_RATES = { 24: 0x00, 25: 0x20, 29: 0x40, 30: 0x60 } 161 | var hourByte = (event.hour & 0x1F) | FRAME_RATES[event.frameRate] 162 | w.writeUInt8(hourByte) 163 | w.writeUInt8(event.min) 164 | w.writeUInt8(event.sec) 165 | w.writeUInt8(event.frame) 166 | w.writeUInt8(event.subFrame) 167 | break; 168 | 169 | case 'timeSignature': 170 | w.writeUInt8(0xFF) 171 | w.writeUInt8(0x58) 172 | w.writeVarInt(4) 173 | w.writeUInt8(event.numerator) 174 | var denominator = Math.floor((Math.log(event.denominator) / Math.LN2)) & 0xFF 175 | w.writeUInt8(denominator) 176 | w.writeUInt8(event.metronome) 177 | w.writeUInt8(event.thirtyseconds || 8) 178 | break; 179 | 180 | case 'keySignature': 181 | w.writeUInt8(0xFF) 182 | w.writeUInt8(0x59) 183 | w.writeVarInt(2) 184 | w.writeInt8(event.key) 185 | w.writeUInt8(event.scale) 186 | break; 187 | 188 | case 'sequencerSpecific': 189 | w.writeUInt8(0xFF) 190 | w.writeUInt8(0x7F) 191 | w.writeVarInt(data.length) 192 | w.writeBytes(data) 193 | break; 194 | 195 | case 'unknownMeta': 196 | if (event.metatypeByte != null) { 197 | w.writeUInt8(0xFF) 198 | w.writeUInt8(event.metatypeByte) 199 | w.writeVarInt(data.length) 200 | w.writeBytes(data) 201 | } 202 | break; 203 | 204 | // system-exclusive 205 | case 'sysEx': 206 | w.writeUInt8(0xF0) 207 | w.writeVarInt(data.length) 208 | w.writeBytes(data) 209 | break; 210 | 211 | case 'endSysEx': 212 | w.writeUInt8(0xF7) 213 | w.writeVarInt(data.length) 214 | w.writeBytes(data) 215 | break; 216 | 217 | // channel events 218 | case 'noteOff': 219 | // Use 0x90 when opts.useByte9ForNoteOff is set and velocity is zero, or when event.byte9 is explicitly set on it. 220 | // parseMidi will set event.byte9 for each event, so that we can get an exact copy by default. 221 | // Explicitly set opts.useByte9ForNoteOff to false, to override event.byte9 and always use 0x80 for noteOff events. 222 | var noteByte = ((useByte9ForNoteOff !== false && event.byte9) || (useByte9ForNoteOff && event.velocity == 0)) ? 0x90 : 0x80 223 | 224 | eventTypeByte = noteByte | event.channel 225 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 226 | w.writeUInt8(event.noteNumber) 227 | w.writeUInt8(event.velocity) 228 | break; 229 | 230 | case 'noteOn': 231 | eventTypeByte = 0x90 | event.channel 232 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 233 | w.writeUInt8(event.noteNumber) 234 | w.writeUInt8(event.velocity) 235 | break; 236 | 237 | case 'noteAftertouch': 238 | eventTypeByte = 0xA0 | event.channel 239 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 240 | w.writeUInt8(event.noteNumber) 241 | w.writeUInt8(event.amount) 242 | break; 243 | 244 | case 'controller': 245 | eventTypeByte = 0xB0 | event.channel 246 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 247 | w.writeUInt8(event.controllerType) 248 | w.writeUInt8(event.value) 249 | break; 250 | 251 | case 'programChange': 252 | eventTypeByte = 0xC0 | event.channel 253 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 254 | w.writeUInt8(event.programNumber) 255 | break; 256 | 257 | case 'channelAftertouch': 258 | eventTypeByte = 0xD0 | event.channel 259 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 260 | w.writeUInt8(event.amount) 261 | break; 262 | 263 | case 'pitchBend': 264 | eventTypeByte = 0xE0 | event.channel 265 | if (eventTypeByte !== lastEventTypeByte) w.writeUInt8(eventTypeByte) 266 | var value14 = 0x2000 + event.value 267 | var lsb14 = (value14 & 0x7F) 268 | var msb14 = (value14 >> 7) & 0x7F 269 | w.writeUInt8(lsb14) 270 | w.writeUInt8(msb14) 271 | break; 272 | 273 | default: 274 | throw 'Unrecognized event type: ' + type 275 | } 276 | return eventTypeByte 277 | } 278 | 279 | 280 | function Writer() { 281 | this.buffer = [] 282 | } 283 | 284 | Writer.prototype.writeUInt8 = function(v) { 285 | this.buffer.push(v & 0xFF) 286 | } 287 | Writer.prototype.writeInt8 = Writer.prototype.writeUInt8 288 | 289 | Writer.prototype.writeUInt16 = function(v) { 290 | var b0 = (v >> 8) & 0xFF, 291 | b1 = v & 0xFF 292 | 293 | this.writeUInt8(b0) 294 | this.writeUInt8(b1) 295 | } 296 | Writer.prototype.writeInt16 = Writer.prototype.writeUInt16 297 | 298 | Writer.prototype.writeUInt24 = function(v) { 299 | var b0 = (v >> 16) & 0xFF, 300 | b1 = (v >> 8) & 0xFF, 301 | b2 = v & 0xFF 302 | 303 | this.writeUInt8(b0) 304 | this.writeUInt8(b1) 305 | this.writeUInt8(b2) 306 | } 307 | Writer.prototype.writeInt24 = Writer.prototype.writeUInt24 308 | 309 | Writer.prototype.writeUInt32 = function(v) { 310 | var b0 = (v >> 24) & 0xFF, 311 | b1 = (v >> 16) & 0xFF, 312 | b2 = (v >> 8) & 0xFF, 313 | b3 = v & 0xFF 314 | 315 | this.writeUInt8(b0) 316 | this.writeUInt8(b1) 317 | this.writeUInt8(b2) 318 | this.writeUInt8(b3) 319 | } 320 | Writer.prototype.writeInt32 = Writer.prototype.writeUInt32 321 | 322 | 323 | Writer.prototype.writeBytes = function(arr) { 324 | this.buffer = this.buffer.concat(Array.prototype.slice.call(arr, 0)) 325 | } 326 | 327 | Writer.prototype.writeString = function(str) { 328 | var i, len = str.length, arr = [] 329 | for (i=0; i < len; i++) { 330 | arr.push(str.codePointAt(i)) 331 | } 332 | this.writeBytes(arr) 333 | } 334 | 335 | Writer.prototype.writeVarInt = function(v) { 336 | if (v < 0) throw "Cannot write negative variable-length integer" 337 | 338 | if (v <= 0x7F) { 339 | this.writeUInt8(v) 340 | } else { 341 | var i = v 342 | var bytes = [] 343 | bytes.push(i & 0x7F) 344 | i >>= 7 345 | while (i) { 346 | var b = i & 0x7F | 0x80 347 | bytes.push(b) 348 | i >>= 7 349 | } 350 | this.writeBytes(bytes.reverse()) 351 | } 352 | } 353 | 354 | Writer.prototype.writeChunk = function(id, data) { 355 | this.writeString(id) 356 | this.writeUInt32(data.length) 357 | this.writeBytes(data) 358 | } 359 | 360 | //module.exports = writeMidi 361 | window.writeMidi = writeMidi; 362 | })(); -------------------------------------------------------------------------------- /js/synth-param-apply.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function synthParamApply(paramId, controlValue, synth) { 4 | let value = Number(controlValue); 5 | 6 | const envelopeExp = (x) => (2 ** x - 1) / 255; 7 | 8 | const lfoExp = (x) => (2 ** x) / 32; 9 | 10 | const freqModExp = (x) => { 11 | let absX = Math.abs(x); 12 | let mod = x > 0 ? 1 : -1; 13 | return (2 ** absX - 1) * 20 * mod; 14 | } 15 | 16 | const setPartials = (oscName, isThisParam) => { 17 | if (isThisParam) { 18 | value = controlValue; 19 | synth.partials[oscName] = value.split(",").map(e => Number(e)); 20 | } 21 | if (synth[oscName] && synth[oscName].type == "custom") 22 | synth[oscName].partials = synth.partials[oscName].slice(); 23 | } 24 | 25 | switch (paramId) { 26 | // Oscillator 1 27 | case "synth-osc1-octave": 28 | synth.values.osc1octaveValue = controlValue * 1200; 29 | if (synth.osc1) 30 | synth.osc1.detune.value = synth.values.osc1octaveValue + synth.values.osc1detuneValue; 31 | break; 32 | 33 | case "synth-osc1-detune": 34 | synth.values.osc1detuneValue = value; 35 | if (synth.osc1) 36 | synth.osc1.detune.value = synth.values.osc1octaveValue + synth.values.osc1detuneValue; 37 | break; 38 | 39 | case "synth-osc1-reset-detune": 40 | synth.values.osc1detuneValue = 0; 41 | value = 0; 42 | paramId = "synth-osc1-detune" 43 | if (synth.osc1) 44 | synth.osc1.detune.value = synth.values.osc1octaveValue + synth.values.osc1detuneValue; 45 | break; 46 | 47 | case "synth-osc1-level": 48 | synth.values.osc1gainValue = value; 49 | if (synth.gain1) 50 | synth.gain1.gain.value = synth.values.osc1gainValue; 51 | break; 52 | 53 | case "synth-osc1-type": 54 | value = controlValue; 55 | if (value == "[none]") { 56 | synth.addOsc1(false); 57 | } else { 58 | synth.addOsc1(true); 59 | synth.osc1.type = controlValue; 60 | setPartials("osc1", false); 61 | } 62 | break; 63 | 64 | case "synth-osc1-partials": 65 | setPartials("osc1", true); 66 | break; 67 | 68 | // Oscillator 2 69 | case "synth-osc2-octave": 70 | synth.values.osc2octaveValue = controlValue * 1200; 71 | if (synth.osc2) 72 | synth.osc2.detune.value = synth.values.osc2octaveValue + synth.values.osc2detuneValue; 73 | break; 74 | 75 | case "synth-osc2-detune": 76 | synth.values.osc2detuneValue = value; 77 | if (synth.osc2) 78 | synth.osc2.detune.value = synth.values.osc2octaveValue + synth.values.osc2detuneValue; 79 | break; 80 | 81 | case "synth-osc2-reset-detune": 82 | synth.values.osc2detuneValue = 0; 83 | value = 0; 84 | paramId = "synth-osc2-detune" 85 | if (synth.osc2) 86 | synth.osc2.detune.value = synth.values.osc2octaveValue + synth.values.osc2detuneValue; 87 | break; 88 | 89 | case "synth-osc2-level": 90 | synth.values.osc2gainValue = value; 91 | if (synth.gain2) 92 | synth.gain2.gain.value = synth.values.osc2gainValue; 93 | break; 94 | 95 | case "synth-osc2-type": 96 | value = controlValue; 97 | if (value == "[none]") { 98 | synth.addOsc2(false); 99 | } else { 100 | synth.addOsc2(true); 101 | synth.osc2.type = controlValue; 102 | setPartials("osc2", false); 103 | } 104 | break; 105 | 106 | case "synth-osc2-partials": 107 | setPartials("osc2", true); 108 | break; 109 | 110 | // Oscillator 3 111 | case "synth-osc3-octave": 112 | synth.values.osc3octaveValue = controlValue * 1200; 113 | if (synth.osc3) 114 | synth.osc3.detune.value = synth.values.osc3octaveValue + synth.values.osc3detuneValue; 115 | break; 116 | 117 | case "synth-osc3-detune": 118 | synth.values.osc3detuneValue = value; 119 | if (synth.osc3) 120 | synth.osc3.detune.value = synth.values.osc3octaveValue + synth.values.osc3detuneValue;; 121 | break; 122 | 123 | case "synth-osc3-reset-detune": 124 | synth.values.osc3detuneValue = 0; 125 | value = 0; 126 | paramId = "synth-osc3-detune" 127 | if (synth.osc3) 128 | synth.osc3.detune.value = synth.values.osc3octaveValue + synth.values.osc3detuneValue; 129 | break; 130 | 131 | case "synth-osc3-level": 132 | synth.values.osc3gainValue = value; 133 | if (synth.osc3) 134 | synth.gain3.gain.value = synth.values.osc3gainValue; 135 | break; 136 | 137 | case "synth-osc3-type": 138 | value = controlValue; 139 | if (value == "[none]") { 140 | synth.addOsc3(false); 141 | } else { 142 | synth.addOsc3(true); 143 | synth.osc3.type = controlValue; 144 | setPartials("osc3", false); 145 | } 146 | break; 147 | 148 | case "synth-osc3-partials": 149 | setPartials("osc3", true); 150 | break; 151 | 152 | // Noise 153 | case "synth-noise-type": 154 | value = controlValue; 155 | if (value == "[none]") { 156 | synth.addNoise(false); 157 | } else { 158 | synth.addNoise(true); 159 | synth.noise.type = value; 160 | } 161 | break; 162 | 163 | case "synth-noise-level": 164 | synth.values.noiseValue = value; 165 | if (synth.noisegain) 166 | synth.noisegain.gain.value = value; 167 | break; 168 | 169 | // Amplitude envelope 170 | case "synth-envelope-attack": 171 | synth.values.envAttackValue = envelopeExp(value); 172 | synth.envelope.attack = synth.values.envAttackValue; 173 | break; 174 | 175 | case "synth-envelope-decay": 176 | synth.values.envDecayValue = envelopeExp(value) + 0.001; 177 | synth.envelope.decay = synth.values.envDecayValue; 178 | break; 179 | 180 | case "synth-envelope-sustain": 181 | synth.values.envSustainValue = value; 182 | synth.envelope.sustain = synth.values.envSustainValue; 183 | break; 184 | 185 | case "synth-envelope-release": 186 | synth.values.envReleaseValue = envelopeExp(value) + 0.001; 187 | synth.envelope.release = synth.values.envReleaseValue; 188 | break; 189 | 190 | case "synth-envelope-type": 191 | value = controlValue; 192 | synth.envelope.decayCurve = value; 193 | synth.envelope.releaseCurve = value; 194 | break; 195 | 196 | // Filter 197 | case "synth-filter-type": 198 | value = controlValue; 199 | synth.addFilter(value); 200 | break; 201 | 202 | case "synth-filter-frequency": 203 | synth.setFilterFrequency(value); 204 | break; 205 | 206 | case "synth-filter-quality": 207 | synth.setFilterQ(value); 208 | break; 209 | 210 | // Amplifier 211 | case "synth-amplifier-gain": 212 | synth.setVolume(value); 213 | break; 214 | 215 | //Glide 216 | case "synth-glide": 217 | synth.glide = value; 218 | break; 219 | 220 | case "synth-porta": 221 | value = controlValue; 222 | synth.porta = value; 223 | break; 224 | 225 | // Panner 226 | case "synth-pan": 227 | synth.addPan(!!value); 228 | synth.values.panValue = value; 229 | if (synth.pan) 230 | synth.pan.pan.value = value; 231 | break; 232 | 233 | case "synth-pan-reset": 234 | value = 0; 235 | paramId = "synth-pan" 236 | 237 | synth.addPan(false); 238 | synth.values.panValue = 0; 239 | if (synth.pan) 240 | synth.pan.pan.value = 0; 241 | break; 242 | 243 | //LFO1 244 | case "synth-lfo1-frequency": 245 | synth.setLfo1Frequency(lfoExp(value)); 246 | break; 247 | 248 | case "synth-lfo1-sync": 249 | value = controlValue; 250 | synth.lfo1sync = value; 251 | synth.setLfo1Frequency(); 252 | break; 253 | 254 | case "synth-lfo1-type": 255 | value = controlValue; 256 | if (value == "[none]") { 257 | synth.addLfo1(false); 258 | } else { 259 | synth.addLfo1(true); 260 | synth.lfo1.type = controlValue; 261 | setPartials("lfo1", false); 262 | } 263 | break; 264 | 265 | case "synth-lfo1-partials": 266 | setPartials("lfo1", true); 267 | break; 268 | 269 | //LFO2 270 | case "synth-lfo2-frequency": 271 | synth.values.lfo2Value = lfoExp(value); 272 | if (synth.lfo2) 273 | synth.lfo2.frequency.value = synth.values.lfo2Value; 274 | break; 275 | 276 | case "synth-lfo2-retrig": 277 | value = controlValue; 278 | synth.lfo2retrig = value; 279 | break; 280 | 281 | case "synth-lfo2-type": 282 | value = controlValue; 283 | if (value == "[none]") { 284 | synth.addLfo2(false); 285 | } else { 286 | synth.addLfo2(true); 287 | synth.lfo2.type = controlValue; 288 | setPartials("lfo2", false); 289 | } 290 | break; 291 | 292 | case "synth-lfo2-partials": 293 | setPartials("lfo2", true); 294 | break; 295 | 296 | // Modulation envelope 297 | case "synth-mod-envelope-type": 298 | value = controlValue; 299 | synth.addModEnvelope(value); 300 | synth.syncModEnvelope(); 301 | break; 302 | 303 | case "synth-mod-envelope-attack": 304 | synth.values.envModAttackValue = envelopeExp(value); 305 | synth.syncModEnvelope(); 306 | break; 307 | 308 | case "synth-mod-envelope-decay": 309 | synth.values.envModDecayValue = envelopeExp(value) + 0.001; 310 | synth.syncModEnvelope(); 311 | break; 312 | 313 | case "synth-mod-envelope-sustain": 314 | synth.values.envModSustainValue = value; 315 | synth.syncModEnvelope(); 316 | break; 317 | 318 | case "synth-mod-envelope-release": 319 | synth.values.envModReleaseValue = envelopeExp(value) + 0.001; 320 | synth.syncModEnvelope(); 321 | break; 322 | 323 | // Modulators 324 | case "synth-osc1-mod-input": 325 | value = controlValue; 326 | synth.setModulator(value, "osc1_modgain"); 327 | break; 328 | 329 | case "synth-osc1-mod-value": 330 | synth.modulatorValues.osc1_modgain = freqModExp(value); 331 | if (synth.osc1_modgain) 332 | synth.osc1_modgain.gain.value = synth.modulatorValues.osc1_modgain; 333 | break; 334 | 335 | case "synth-osc2-mod-input": 336 | value = controlValue; 337 | synth.setModulator(value, "osc2_modgain"); 338 | break; 339 | 340 | case "synth-osc2-mod-value": 341 | synth.modulatorValues.osc2_modgain = freqModExp(value); 342 | if (synth.osc2_modgain) 343 | synth.osc2_modgain.gain.value = synth.modulatorValues.osc2_modgain; 344 | break; 345 | 346 | case "synth-filter-mod-input": 347 | value = controlValue; 348 | synth.setModulator(value, "filter_modgain"); 349 | break; 350 | 351 | case "synth-filter-mod-value": 352 | synth.setFilterModAmount(value); 353 | break; 354 | 355 | case "synth-amplifier-mod-input": 356 | value = controlValue; 357 | synth.setModulator(value, "ampAM_modgain"); 358 | break; 359 | 360 | case "synth-amplifier-mod-value": 361 | synth.modulatorValues.ampAM_modgain = value; 362 | if (synth.ampAM_modgain) 363 | synth.ampAM_modgain.gain.value = value; 364 | break; 365 | 366 | // FX 367 | case "synth-fx-type": 368 | value = controlValue; 369 | synth.addFX(value); 370 | break; 371 | 372 | case "synth-fx-amount": 373 | synth.setFXValue(value); 374 | break; 375 | 376 | case "synth-fx-rate": 377 | synth.setFXRate(value); 378 | break; 379 | 380 | case "synth-fx-sync": 381 | value = controlValue; 382 | synth.FXsync = value; 383 | synth.setFXRate(); 384 | break; 385 | 386 | case "synth-fx-wet": 387 | synth.values.FXWetValue = value; 388 | if (synth.FX) 389 | synth.FX.wet.value = synth.values.FXWetValue; 390 | break; 391 | 392 | default: 393 | console.log("Function not implemented for : " + paramId); 394 | return null; 395 | } 396 | 397 | return { id: paramId, value: value }; 398 | }; -------------------------------------------------------------------------------- /js/waveform-editor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function waveformEditor(songObj) { 4 | const padding = 12; 5 | let oscName = "osc1"; 6 | let partials = []; 7 | 8 | let oscCopyOptions = {}; 9 | for (let el of ["osc1", "osc2", "osc3", "lfo1", "lfo2"]) 10 | oscCopyOptions[el] = document.getElementById("opt-copy-" + el); 11 | 12 | let canvas = document.getElementById("canvas-osc-editor"); 13 | let ctx = canvas.getContext("2d"); 14 | 15 | let drawWidth = canvas.width - padding * 2; 16 | let drawHeight = canvas.height - padding * 2; 17 | 18 | let pressed = false; 19 | canvas.addEventListener("pointerdown", (e) => { pressed = true; pointerListener(e) }); 20 | canvas.addEventListener("pointermove", pointerListener); 21 | 22 | canvas.addEventListener("pointerup", pointerOffListener); 23 | canvas.addEventListener("pointercancel", pointerOffListener); 24 | canvas.addEventListener("pointerleave", pointerOffListener); 25 | 26 | let selectOscHarmonics = document.getElementById("select-osc-harmonics"); 27 | 28 | selectOscHarmonics.onchange = (e) => { 29 | partials.length = Number(e.target.value); 30 | 31 | for (let i = 0; i < partials.length; i++) 32 | if (!partials[i]) 33 | partials[i] = 0; 34 | 35 | applyPartialsToSynth(); 36 | drawBars(); 37 | drawPlot(); 38 | } 39 | 40 | document.getElementById("osc1-menu-open").onclick = () => { 41 | oscName = "osc1"; 42 | openEditor(); 43 | }; 44 | 45 | document.getElementById("osc2-menu-open").onclick = () => { 46 | oscName = "osc2"; 47 | openEditor(); 48 | }; 49 | 50 | document.getElementById("osc3-menu-open").onclick = () => { 51 | oscName = "osc3"; 52 | openEditor(); 53 | }; 54 | 55 | document.getElementById("lfo1-menu-open").onclick = () => { 56 | oscName = "lfo1"; 57 | openEditor(); 58 | }; 59 | 60 | document.getElementById("lfo2-menu-open").onclick = () => { 61 | oscName = "lfo2"; 62 | openEditor(); 63 | }; 64 | 65 | document.getElementById("button-osc-menu-close").onclick = () => { 66 | hideModal("oscillator-modal-menu"); 67 | }; 68 | 69 | function openEditor() { 70 | showModal("oscillator-modal-menu"); 71 | let synth = songObj.synths[songObj.currentSynthIndex]; 72 | 73 | readPartials(synth.partials[oscName]); 74 | selectOscHarmonics.value = partials.length; 75 | 76 | for (let key in oscCopyOptions) 77 | oscCopyOptions[key].hidden = (synth.partials[key].length <= 1) 78 | 79 | oscCopyOptions[oscName].hidden = true; 80 | } 81 | 82 | function readPartials(oscPartials) { 83 | partials = oscPartials.slice(); 84 | 85 | if (partials.length <= 1) 86 | while (partials.length < 16) 87 | partials.push(0); 88 | 89 | drawBars(); 90 | drawPlot(); 91 | } 92 | 93 | document.getElementById("select-harmonics-templates").onchange = (e) => { 94 | switch (e.target.value) { 95 | case "sine": 96 | partials.fill(0); 97 | partials[0] = 1; 98 | break; 99 | 100 | case "softsaw": 101 | partials = mkSaw(partials.length, true); 102 | break; 103 | 104 | case "triangle": 105 | partials = mkSaw(partials.length, true); 106 | sparse(partials, 1, 2); 107 | break; 108 | 109 | case "sawtooth": 110 | partials = mkSaw(partials.length, false); 111 | break; 112 | 113 | case "square": 114 | partials = mkSaw(partials.length, false); 115 | sparse(partials, 1, 2); 116 | break; 117 | 118 | case "pulse33": 119 | partials = mkSaw(partials.length, false); 120 | sparse(partials, 2, 3); 121 | break; 122 | 123 | case "pulse25": 124 | partials = mkSaw(partials.length, false); 125 | sparse(partials, 3, 4); 126 | break; 127 | 128 | case "pulse20": 129 | partials = mkSaw(partials.length, false); 130 | sparse(partials, 4, 5); 131 | break; 132 | 133 | case "strings1": 134 | partials = mkSaw(partials.length, false); 135 | sparse(partials, 2, 4); 136 | sparse(partials, 3, 4); 137 | break; 138 | 139 | case "strings2": 140 | partials = mkSaw(partials.length, false); 141 | sparse(partials, 2, 5); 142 | sparse(partials, 4, 5); 143 | break; 144 | 145 | case "linear": 146 | for (let i = 0; i < partials.length; i++) 147 | partials[i] = 1 - (i / partials.length) 148 | break; 149 | 150 | case "osc1": 151 | case "osc2": 152 | case "osc3": 153 | case "lfo1": 154 | case "lfo2": 155 | let synth = songObj.synths[songObj.currentSynthIndex]; 156 | partials = synth.partials[e.target.value].slice(); 157 | selectOscHarmonics.value = partials.length; 158 | break; 159 | 160 | } 161 | 162 | e.target.value = "template"; 163 | applyPartialsToSynth(); 164 | drawBars(); 165 | drawPlot(); 166 | 167 | function mkSaw(len, soft) { 168 | let wave = new Array(len); 169 | for (let i = 1; i <= len; i++) 170 | wave[i - 1] = 1 / (soft ? i * i : i); 171 | 172 | return wave; 173 | } 174 | 175 | function sparse(arr, start, step) { 176 | for (let i = start; i < arr.length; i += step) 177 | arr[i] = 0; 178 | } 179 | } 180 | 181 | function applyPartialsToSynth() { 182 | let synth = songObj.synths[songObj.currentSynthIndex]; 183 | let synthParams = songObj.synthParams[songObj.currentSynthIndex]; 184 | let paramId = "synth-" + oscName + "-partials"; 185 | synthParams[paramId] = partials.join(","); 186 | synth[oscName].partials = partials.slice(); 187 | synth.partials[oscName] = partials.slice(); 188 | } 189 | 190 | function pointerOffListener() { 191 | if (pressed) { 192 | pressed = false; 193 | drawPlot(); 194 | applyPartialsToSynth(); 195 | } 196 | } 197 | 198 | function pointerListener(e) { 199 | if (!pressed) 200 | return; 201 | 202 | let wdth = canvas.getBoundingClientRect().width; 203 | let hght = canvas.getBoundingClientRect().height; 204 | 205 | let x = e.offsetX / wdth * canvas.width - padding - 1; 206 | let y = e.offsetY / hght * canvas.height - padding - 1; 207 | 208 | let step = drawWidth / partials.length; 209 | let partialsIndex = Math.floor(x / step); 210 | if (partialsIndex < 0 || partialsIndex >= partials.length) 211 | return; 212 | 213 | let partial = 1 - y / drawHeight; 214 | partial = Math.min(1, partial); 215 | partial = Math.max(0, partial); 216 | partials[partialsIndex] = partial * partial; 217 | 218 | drawBars(); 219 | } 220 | 221 | function drawBars() { 222 | ctx.fillStyle = "black"; 223 | ctx.fillRect(0, 0, canvas.width, canvas.height); 224 | 225 | drawDecorations(ctx, canvas.width, canvas.height, padding, partials.length, oscName.toUpperCase()); 226 | 227 | ctx.fillStyle = "#347234"; 228 | let barWidth = drawWidth / partials.length; 229 | for (let i = 0; i < partials.length; i++) { 230 | let barHeight = Math.sqrt(partials[i]) * drawHeight; 231 | ctx.fillRect(padding + i * barWidth + 1, padding + drawHeight - barHeight, barWidth - 2, barHeight) 232 | } 233 | 234 | ctx.fillStyle = "#3e883e"; 235 | for (let i = 0; i < partials.length; i++) { 236 | let barHeight = partials[i] * drawHeight; 237 | ctx.fillRect(padding + i * barWidth + 2, padding + drawHeight - barHeight, barWidth - 4, barHeight) 238 | } 239 | } 240 | 241 | function drawPlot() { 242 | let [rr, ii] = _getRealImaginary(0, partials); 243 | let plotLine = new Array(drawWidth); 244 | let maxV = 0; 245 | for (let i = 0; i < drawWidth; i++) { 246 | plotLine[i] = _inverseFFT(rr, ii, i / drawWidth * 360 / (180 / Math.PI)); 247 | maxV = Math.max(maxV, plotLine[i]) 248 | } 249 | 250 | ctx.strokeStyle = "white"; 251 | ctx.beginPath(); 252 | 253 | ctx.moveTo(padding, drawHeight / 2 + padding); 254 | for (let i = 0; i < drawWidth; i++) { 255 | let yy = drawHeight / 2 + drawHeight / 2 * plotLine[i] / maxV; 256 | ctx.lineTo(padding + i, padding + yy); 257 | } 258 | 259 | ctx.lineTo(canvas.width - padding, drawHeight / 2 + padding); 260 | ctx.stroke(); 261 | } 262 | 263 | function drawDecorations(context, width, height, padding, divideTo, title) { 264 | let dwid = width - padding * 2; 265 | let dhei = height - padding * 2; 266 | let y, dy; 267 | 268 | context.strokeStyle = "#555"; 269 | context.strokeRect(1, 1, width - 2, height - 2); 270 | 271 | context.font = "34px sans-serif"; 272 | context.fillStyle = "#555"; 273 | context.fillText(title, padding + dwid / 4 * 3, padding + 32); 274 | 275 | context.font = "12px sans-serif"; 276 | context.fillStyle = "darkcyan"; 277 | dy = Math.sqrt(0.5); 278 | context.fillText(".50", width - padding - 20, padding + dhei - dhei * dy - 2); 279 | dy = Math.sqrt(0.75); 280 | context.fillText(".75", width - padding - 20, padding + dhei - dhei * dy - 2); 281 | dy = Math.sqrt(0.25); 282 | context.fillText(".25", width - padding - 20, padding + dhei - dhei * dy - 2); 283 | 284 | context.strokeStyle = "cyan"; 285 | context.beginPath(); 286 | 287 | dy = Math.sqrt(0.5); 288 | y = dhei - dhei * dy 289 | line(-10, y, dwid + 10, y) 290 | 291 | dy = Math.sqrt(0.75); 292 | y = dhei - dhei * dy 293 | line(15, y, dwid - 15, y) 294 | 295 | context.stroke(); 296 | 297 | context.strokeStyle = "darkcyan"; 298 | context.strokeRect(padding, padding, dwid, dhei); 299 | 300 | context.beginPath(); 301 | for (let i = 0; i <= divideTo; i++) { 302 | let len = width - padding * 2; 303 | let div = len / divideTo; 304 | if (i % 4 == 0) { 305 | line(div * i, dhei, div * i, dhei + 10); 306 | } 307 | } 308 | 309 | dy = Math.sqrt(0.125); 310 | y = dhei - dhei * dy 311 | line(40, y, dwid - 40, y) 312 | 313 | dy = Math.sqrt(0.625); 314 | y = dhei - dhei * dy 315 | line(40, y, dwid - 40, y) 316 | 317 | dy = Math.sqrt(0.375); 318 | y = dhei - dhei * dy 319 | line(40, y, dwid - 40, y) 320 | 321 | dy = Math.sqrt(0.875); 322 | y = dhei - dhei * dy 323 | line(40, y, dwid - 40, y) 324 | 325 | context.stroke(); 326 | 327 | context.strokeStyle = "#f88"; 328 | context.beginPath(); 329 | 330 | dy = 0.5; //Math.sqrt(0.25); 331 | y = dhei - dhei * dy 332 | line(0, y, dwid, y) 333 | context.stroke(); 334 | 335 | context.strokeStyle = "brown"; 336 | context.beginPath(); 337 | 338 | let dx = 0.5; 339 | let x = dwid - dwid * dx 340 | line(x, 0, x, dhei); 341 | 342 | line(dwid / 2 - 20, dhei / 4, dwid / 2 + 20, dhei / 4); 343 | line(dwid / 2 - 20, dhei / 4 * 3, dwid / 2 + 20, dhei / 4 * 3); 344 | 345 | line(dwid / 4, dhei / 2 - 20, dwid / 4, dhei / 2 + 20); 346 | line(dwid / 4 * 3, dhei / 2 - 20, dwid / 4 * 3, dhei / 2 + 20); 347 | 348 | context.stroke(); 349 | 350 | function line(x1, y1, x2, y2) { 351 | context.moveTo(Math.round(x1) + padding, Math.round(y1) + padding); 352 | context.lineTo(Math.round(x2) + padding, Math.round(y2) + padding); 353 | } 354 | } 355 | 356 | /* 357 | * Private functions from Tone.js (for drawing waveform on canvas) 358 | * https://github.com/Tonejs/Tone.js/blob/dev/Tone/source/oscillator/Oscillator.ts 359 | */ 360 | function _inverseFFT(real, imag, phase) { 361 | let sum = 0; 362 | const len = real.length; 363 | for (let i = 0; i < len; i++) { 364 | sum += 365 | real[i] * Math.cos(i * phase) + imag[i] * Math.sin(i * phase); 366 | } 367 | return sum; 368 | } 369 | 370 | function _getRealImaginary(phase, _partials) { 371 | let periodicWaveSize = _partials.length + 1; 372 | 373 | const real = new Float32Array(periodicWaveSize); 374 | const imag = new Float32Array(periodicWaveSize); 375 | 376 | if (_partials.length === 0) { 377 | return [real, imag]; 378 | } 379 | 380 | for (let n = 1; n < periodicWaveSize; ++n) { 381 | let b = _partials[n - 1]; 382 | 383 | if (b !== 0) { 384 | real[n] = -b * Math.sin(phase * n); 385 | imag[n] = b * Math.cos(phase * n); 386 | } else { 387 | real[n] = 0; 388 | imag[n] = 0; 389 | } 390 | } 391 | return [real, imag]; 392 | } 393 | } -------------------------------------------------------------------------------- /js/arrange-ui.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function ArrangeUi(songObj, onPatternSelectCallback, defaults) { 4 | const aheadSpace = defaults.emptyBarsBuffer; //8 5 | const minColumnCount = defaults.minSongBars; //32 6 | const maxPatternSteps = defaults.maxPatternSteps; //64 7 | 8 | let rowCount = 0; 9 | let columnCount = 0; 10 | 11 | let playbackMarkers = []; 12 | let previousMarker = 0; 13 | 14 | let loopStartPoint = null; 15 | let loopEndPoint = null; 16 | let loopStartIndex = -1; 17 | let loopLength = 0; 18 | 19 | let pressTimeout = null; 20 | let cancelClick = false; 21 | 22 | let table = document.createElement("TABLE"); 23 | 24 | table.oncontextmenu = () => false; 25 | table.addEventListener("click", arrangeEventListener); 26 | table.addEventListener("pointerdown", pointerdownListener); 27 | 28 | table.addEventListener("pointerup", pointerEndListener); 29 | table.addEventListener("pointercancel", pointerEndListener); 30 | table.addEventListener("pointerleave", pointerEndListener); 31 | table.addEventListener("touchend", pointerEndListener); 32 | 33 | function pointerEndListener() { 34 | clearTimeout(pressTimeout); 35 | pressTimeout = null; 36 | } 37 | 38 | document.getElementById("button-add-pattern").onclick = () => { 39 | let defaultName = songObj.generatePatternName(); 40 | showPrompt("Create pattern", (result) => { 41 | if (result === null) 42 | return; 43 | 44 | songObj.patterns[rowCount] = new Pattern(result, songObj.barSteps); 45 | addRow(result); 46 | showPattern(rowCount - 1, true); 47 | showToast("New pattern"); 48 | g_scrollToLastPattern(); 49 | }, defaultName); 50 | }; 51 | 52 | const insertColumns = (startPoint, length) => { 53 | let ins = []; 54 | for (let i = 0; i < length; i++) 55 | ins.push([]); 56 | 57 | songObj.song.splice(startPoint, 0, ...ins); 58 | this.fillSongView(); 59 | } 60 | 61 | const spliceColumns = (startPoint, length) => { 62 | for (let i = 0; i < songObj.patterns.length; i++) { 63 | let patternBars = Math.ceil(songObj.patterns[i].length / songObj.barSteps); 64 | 65 | for (let j = startPoint; j >= Math.max(0, startPoint - patternBars + 1); j--) 66 | if (songObj.song[j][i]) 67 | songObj.song[j][i] = false; 68 | } 69 | 70 | songObj.song.splice(startPoint, length); 71 | this.fillSongView(); 72 | } 73 | 74 | this.build = function () { 75 | let arrange = document.getElementById("arrange-main"); 76 | arrange.appendChild(table); 77 | 78 | document.getElementById("column-modal-menu").oncontextmenu = () => false; 79 | 80 | document.getElementById("button-column-menu-close").onclick = () => { 81 | hideModal("column-modal-menu"); 82 | } 83 | 84 | document.getElementById("button-insert-columns").onclick = () => { 85 | hideModal("column-modal-menu"); 86 | 87 | showPrompt("Insert columns:", result => { 88 | if (result === null || result === 0) 89 | return; 90 | 91 | let length = Math.floor(result); 92 | if (length > 0 && length < 500) 93 | insertColumns(songObj.arrangeStartPoint, length) 94 | else 95 | showAlert("Can not insert columns"); 96 | }, 1, "number"); 97 | } 98 | 99 | document.getElementById("button-remove-columns").onclick = () => { 100 | hideModal("column-modal-menu"); 101 | 102 | showPrompt("Delete columns:", result => { 103 | if (result === null || result === 0) 104 | return; 105 | 106 | let length = Math.floor(result); 107 | if (length > 0 && length < 500) 108 | spliceColumns(songObj.arrangeStartPoint, length) 109 | else 110 | showAlert("Can not delete columns"); 111 | }, 1, "number"); 112 | } 113 | } 114 | 115 | this.setMarker = function (index) { 116 | if (playbackMarkers[previousMarker]) 117 | playbackMarkers[previousMarker].style.backgroundColor = "#111"; 118 | 119 | if (index >= 0 && index < playbackMarkers.length) { 120 | playbackMarkers[index].style.backgroundColor = "#696969"; 121 | previousMarker = index; 122 | } 123 | } 124 | 125 | this.setLoopMarkers = function (startIndex, length) { 126 | if (startIndex !== undefined) { 127 | loopStartIndex = startIndex; 128 | loopLength = length || 0; 129 | } 130 | 131 | if (loopStartIndex == -1) { 132 | if (loopStartPoint) { 133 | loopStartPoint.classList.remove("loop-start-point"); 134 | loopStartPoint = null; 135 | } 136 | 137 | if (loopEndPoint) { 138 | loopEndPoint.classList.remove("loop-end-point"); 139 | loopEndPoint = null; 140 | } 141 | } else { 142 | let endIndex = loopStartIndex + loopLength - 1; 143 | 144 | loopStartPoint = document.getElementById("arr_col-" + loopStartIndex + "_header"); 145 | if (loopStartPoint) 146 | loopStartPoint.classList.add("loop-start-point"); 147 | 148 | loopEndPoint = document.getElementById("arr_col-" + endIndex + "_header"); 149 | if (loopEndPoint) 150 | loopEndPoint.classList.add("loop-end-point"); 151 | } 152 | }; 153 | 154 | this.fillSongView = function () { 155 | clearSongView(); 156 | 157 | let newSongLen = songObj.song.length; 158 | console.log("Track length: " + newSongLen + " bars"); 159 | 160 | for (let i = 0; i < songObj.patterns.length; i++) 161 | addRow(songObj.patterns[i].name); 162 | 163 | if (newSongLen > columnCount) { 164 | for (let i = columnCount; i < newSongLen; i++) { 165 | addColumn(); 166 | } 167 | } 168 | 169 | for (let i = songObj.song.length; i < columnCount; i++) { 170 | songObj.song.push([]); 171 | } 172 | 173 | for (let i = 0; i < songObj.song.length; i++) { 174 | for (let j = 0; j < songObj.song[i].length; j++) { 175 | if (songObj.song[i][j]) { 176 | drawBlock(i, j, true) 177 | } 178 | } 179 | } 180 | 181 | fitGridLength(); 182 | markDisabledCells(0, songObj.song.length - 1); 183 | g_markCurrentPattern(); 184 | this.setLoopMarkers(); 185 | } 186 | 187 | const reorderMenu = new ReorderMenu(songObj, this.fillSongView.bind(this)); 188 | 189 | function maxPatternBars() { 190 | return Math.ceil(maxPatternSteps / songObj.barSteps); 191 | } 192 | 193 | function buildGridHeader() { 194 | let th = document.createElement("TR"); 195 | 196 | let td = document.createElement("TD"); 197 | td.id = "arr_corner"; 198 | th.appendChild(td); 199 | table.appendChild(th); 200 | 201 | for (let j = 0; j < minColumnCount; j++) 202 | addColumn(); 203 | } 204 | 205 | function arrangeEventListener(event) { 206 | if (cancelClick) 207 | return; 208 | 209 | let tgt = event.target; 210 | if (tgt.nodeName != "TD") 211 | return; 212 | 213 | if (tgt.id == "arr_corner") { 214 | reorderMenu.showMenu(); 215 | return; 216 | } 217 | 218 | let idParts = tgt.id.split("_"); 219 | let col = Number(idParts[1].split("-")[1]); 220 | let row = Number(idParts[2].split("-")[1]); 221 | 222 | if (tgt.id.includes("header")) { 223 | setArrangeStartPoint(col); 224 | return; 225 | } 226 | 227 | if (tgt.id.includes("side")) { 228 | showPattern(row); 229 | return; 230 | } 231 | 232 | setArrangeBlock(col, row, tgt); 233 | 234 | if (col >= songObj.song.length - aheadSpace - maxPatternBars()) 235 | fitGridLength(); 236 | } 237 | 238 | function pointerdownListener(event) { 239 | cancelClick = false; 240 | let tgtId = event.target.id; 241 | if (!tgtId.includes("header")) 242 | return; 243 | 244 | pressTimeout = setTimeout(() => { 245 | cancelClick = true; 246 | 247 | let idParts = tgtId.split("_"); 248 | let col = Number(idParts[1].split("-")[1]); 249 | setArrangeStartPoint(col); 250 | 251 | updateTimerView(); 252 | showModal("column-modal-menu"); 253 | 254 | }, DEFAULT_PARAMS.pressDelay); 255 | } 256 | 257 | function updateTimerView() { 258 | let timers = document.getElementById("timers-area"); 259 | timers.innerHTML = ""; 260 | let span = document.createElement("SPAN"); 261 | let timeString = songObj.getStartPointTime() + " / " + songObj.getEndPointTime(); 262 | span.appendChild(document.createTextNode(timeString)); 263 | timers.appendChild(span); 264 | } 265 | 266 | function setArrangeStartPoint(col) { 267 | let prev = document.getElementById("arr_col-" + songObj.arrangeStartPoint + "_header"); 268 | if (prev) 269 | prev.classList.remove("play-start-point"); 270 | 271 | songObj.arrangeStartPoint = col; 272 | let next = document.getElementById("arr_col-" + col + "_header"); 273 | next.classList.add("play-start-point"); 274 | } 275 | 276 | function setArrangeBlock(col, row, targetCell) { 277 | let startPoint = col; 278 | 279 | if (songObj.song[col][row]) { 280 | songObj.song[col][row] = false; 281 | drawBlock(col, row, false); 282 | } else { 283 | if (targetCell.classList.contains("js-fill-tail")) { 284 | 285 | while (!songObj.song[startPoint][row]) 286 | startPoint--; 287 | 288 | songObj.song[startPoint][row] = false; 289 | drawBlock(startPoint, row, false); 290 | } else { 291 | let len = Math.ceil(songObj.patterns[row].length / songObj.barSteps); 292 | for (let i = col; i < Math.min(col + len, songObj.song.length); i++) { 293 | if (songObj.song[i][row]) { 294 | showToast("Can not insert block - no room"); 295 | return; 296 | } 297 | } 298 | 299 | if (songObj.checkSynthConflict(row, col)) { 300 | console.log("Pattern with same synth present in this bar."); 301 | showToast("Can not insert block - conflict with other cells"); 302 | return; 303 | } 304 | 305 | songObj.song[col][row] = true; 306 | if (col >= songObj.song.length - aheadSpace - maxPatternBars()) 307 | fitGridLength(); 308 | 309 | drawBlock(col, row, true); 310 | } 311 | } 312 | 313 | let addBars = Math.ceil(songObj.patterns[row].length / songObj.barSteps) - 1; 314 | markDisabledCells(startPoint, startPoint + addBars); 315 | } 316 | 317 | function drawBlock(col, row, isDraw) { 318 | let len = Math.ceil(songObj.patterns[row].length / songObj.barSteps); 319 | let startCell = document.getElementById("arr_col-" + col + "_row-" + row); 320 | 321 | if (isDraw) 322 | startCell.classList.add("js-fill-head"); 323 | else 324 | startCell.classList.remove("js-fill-head"); 325 | 326 | for (let i = 1; i < len; i++) { 327 | let cell = document.getElementById("arr_col-" + (col + i) + "_row-" + row); 328 | 329 | if (!cell) 330 | continue; 331 | 332 | if (isDraw) 333 | cell.classList.add("js-fill-tail"); 334 | else 335 | cell.classList.remove("js-fill-tail"); 336 | } 337 | } 338 | 339 | function fitGridLength() { 340 | let songLen = songObj.song.length; 341 | let newSongLen = Math.max(songObj.calcSongLength() + aheadSpace, minColumnCount); 342 | 343 | if (newSongLen > songLen) { 344 | for (let i = 0; i < newSongLen - songLen; i++) { 345 | addColumn(); 346 | songObj.song.push([]); 347 | } 348 | } 349 | 350 | if (newSongLen < songLen) { 351 | for (let i = 0; i < songLen - newSongLen; i++) { 352 | removeColumn(); 353 | songObj.song.pop(); 354 | } 355 | } 356 | 357 | if (songObj.arrangeStartPoint >= songObj.song.length) 358 | setArrangeStartPoint(0); 359 | } 360 | 361 | function showPattern(index, isNewPattern) { 362 | songObj.setCurrentPattern(index); 363 | g_markCurrentPattern(); 364 | onPatternSelectCallback(songObj.currentPattern, isNewPattern); 365 | g_switchTab("pattern"); 366 | } 367 | 368 | function addRow(name) { 369 | let tr = document.createElement("TR"); 370 | for (let j = 0; j <= columnCount; j++) { 371 | let td = document.createElement("TD"); 372 | 373 | if (j > 0) { 374 | td.id = "arr_col-" + (j - 1) + "_row-" + rowCount; 375 | } else { 376 | td.id = "arr_side_row-" + rowCount; 377 | td.classList.add("arrange-sidebar"); 378 | td.appendChild(document.createTextNode(name)); 379 | } 380 | 381 | tr.appendChild(td); 382 | } 383 | 384 | if (songObj.patterns[rowCount]) 385 | tr.classList.add("color-index-" + songObj.patterns[rowCount].colorIndex); 386 | 387 | table.appendChild(tr); 388 | rowCount++; 389 | } 390 | 391 | function addColumn() { 392 | let preCell = document.getElementById("arr_col-" + (columnCount - 1) + "_header"); 393 | if (!preCell) 394 | preCell = document.getElementById("arr_corner"); 395 | 396 | let td = document.createElement("TD"); 397 | td.id = "arr_col-" + columnCount + "_header"; 398 | td.classList.add("arrange-header"); 399 | preCell.after(td); 400 | 401 | if (columnCount == songObj.arrangeStartPoint) 402 | td.classList.add("play-start-point"); 403 | 404 | for (let i = 0; i < rowCount; i++) { 405 | preCell = document.getElementById("arr_col-" + (columnCount - 1) + "_row-" + i); 406 | let td = document.createElement("TD"); 407 | td.id = "arr_col-" + columnCount + "_row-" + i; 408 | preCell.after(td); 409 | } 410 | 411 | playbackMarkers.push(td); 412 | columnCount++; 413 | } 414 | 415 | function removeColumn() { 416 | let cell = document.getElementById("arr_col-" + (columnCount - 1) + "_header");; 417 | cell.remove(); 418 | 419 | for (let i = 0; i < rowCount; i++) { 420 | cell = document.getElementById("arr_col-" + (columnCount - 1) + "_row-" + i); 421 | cell.remove(); 422 | } 423 | 424 | playbackMarkers.pop(); 425 | columnCount--; 426 | } 427 | 428 | function clearSongView() { 429 | playbackMarkers = []; 430 | rowCount = 0; 431 | columnCount = 0; 432 | 433 | table.innerHTML = ""; 434 | buildGridHeader(); 435 | } 436 | 437 | function markDisabledCells(startPoint, endPoint) { 438 | songObj.calculateSynthFill(); 439 | for (let i = startPoint; i <= endPoint; i++) { 440 | for (let j = 0; j < songObj.patterns.length; j++) { 441 | let cell = document.getElementById("arr_col-" + i + "_row-" + j); 442 | 443 | if (!songObj.isArrangeCellFree(i, j)) { 444 | cell.classList.add("non-free-cell"); 445 | } else { 446 | cell.classList.remove("non-free-cell"); 447 | } 448 | } 449 | } 450 | } 451 | } -------------------------------------------------------------------------------- /js/song-object.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function SongObject() { 4 | this.song = []; 5 | this.patterns = []; 6 | this.synthParams = []; 7 | this.synths = []; 8 | this.synthNames = []; 9 | 10 | this.title = ""; 11 | this.bpm = 120; 12 | this.barSteps = 16; 13 | this.swing = 0; 14 | 15 | this.currentPattern = null; 16 | this.currentPatternIndex = 0; 17 | this.currentSynthIndex = 0; 18 | this.arrangeStartPoint = 0; 19 | this.synthFill = []; 20 | this.playableLength = 0; 21 | 22 | this.compressorThreshold = -30; 23 | this.compressorRatio = 3; 24 | this.compressor = new Tone.Compressor(this.compressorThreshold, this.compressorRatio); 25 | this.compressor.toDestination(); 26 | 27 | this.setBpm = function (bpm) { 28 | Tone.Transport.bpm.value = bpm; 29 | this.bpm = bpm; 30 | this.synths.forEach(e => e.setBpm(bpm)); 31 | } 32 | 33 | this.fillSong = function () { 34 | this.song = []; 35 | for (let i = 0; i < DEFAULT_PARAMS.minSongBars; i++) 36 | this.song.push([]); 37 | } 38 | 39 | this.createEmptySong = function () { 40 | this.fillSong(); 41 | this.createSynth("synth1"); 42 | this.patterns.push(new Pattern("A1")); 43 | this.setCurrentPattern(0); 44 | this.currentPattern.patternData[0].synthIndex = 0; 45 | } 46 | 47 | this.deleteCurrentPattern = function () { 48 | let deleteIndex = this.currentPatternIndex; 49 | this.patterns.splice(deleteIndex, 1); 50 | 51 | for (let i = 0; i < this.song.length; i++) { 52 | if (this.song[i].length >= deleteIndex + 1) { 53 | this.song[i].splice(deleteIndex, 1); 54 | } 55 | } 56 | 57 | this.setCurrentPattern(0); 58 | } 59 | 60 | this.generatePatternName = function (prefix) { 61 | let patternNames = this.patterns.map(e => e.name); 62 | 63 | let genPrefix = patternNames[patternNames.length - 1]; 64 | let separate = genPrefix.search(/-|\d|\s/); 65 | if (separate > 0) 66 | genPrefix = genPrefix.substring(0, separate); 67 | 68 | return generateName(prefix || genPrefix || "A", patternNames, 2); 69 | } 70 | 71 | this.createSynth = function (name, copyFromParams) { 72 | let newSynth = new Synth(this.compressor, this.bpm); 73 | let newSynthParamObj = {}; 74 | let params = copyFromParams || DEFAULT_PARAMS.synthState; 75 | 76 | for (let key in params) { 77 | newSynthParamObj[key] = params[key]; 78 | synthParamApply(key, newSynthParamObj[key], newSynth); 79 | } 80 | 81 | this.synthParams.push(newSynthParamObj); 82 | this.synths.push(newSynth); 83 | this.synthNames.push(name || ""); 84 | } 85 | 86 | this.deleteSynth = function (index) { 87 | this.synths[index].destroy(); 88 | 89 | this.synths.splice(index, 1); 90 | this.synthNames.splice(index, 1); 91 | this.synthParams.splice(index, 1); 92 | 93 | for (let i = 0; i < this.patterns.length; i++) { 94 | this.patterns[i].spliceSynth(index); 95 | } 96 | 97 | this.calculateSynthFill(); 98 | } 99 | 100 | this.generateSynthName = function (prefix, startNumber) { 101 | return generateName(prefix || "synth", this.synthNames, startNumber); 102 | } 103 | 104 | this.testSynthOnPattern = function (synthIndex) { 105 | let layerIndex = this.currentPattern.activeIndex; 106 | 107 | if (synthIndex === null) 108 | return true; 109 | 110 | if (this.currentPattern.patternData[layerIndex].synthIndex == synthIndex) 111 | return true; 112 | 113 | if (this.isSynthInCurrentPattern(synthIndex)) 114 | return false; 115 | 116 | let patternBars = Math.ceil(this.currentPattern.length / this.barSteps); 117 | for (let i = 0; i <= this.song.length - patternBars; i++) 118 | if (this.song[i][this.currentPatternIndex]) 119 | for (let j = i; j < i + patternBars; j++) 120 | if (this.synthFill[j][synthIndex]) 121 | return false; 122 | 123 | return true; 124 | } 125 | 126 | this.setCurrentLayerSynthIndex = function (synthIndex) { 127 | let layerIndex = this.currentPattern.activeIndex; 128 | if (this.testSynthOnPattern(synthIndex)) 129 | this.currentPattern.patternData[layerIndex].synthIndex = synthIndex; 130 | } 131 | 132 | this.isSynthInCurrentPattern = function (synthIndex) { 133 | if (synthIndex === null) 134 | return false; 135 | 136 | let pattern = this.currentPattern; 137 | for (let i = 0; i < pattern.patternData.length; i++) { 138 | if (pattern.patternData[i].synthIndex == synthIndex) 139 | return true; 140 | } 141 | 142 | return false; 143 | } 144 | 145 | this.getCurrentLayerSynthIndex = function () { 146 | let layerIndex = this.currentPattern.activeIndex; 147 | return this.currentPattern.patternData[layerIndex].synthIndex; 148 | } 149 | 150 | this.getCurrentLayerSynthName = function () { 151 | let index = this.getCurrentLayerSynthIndex(); 152 | return index === null ? null : this.synthNames[index]; 153 | } 154 | 155 | this.setCurrentPattern = function (index) { 156 | this.currentPatternIndex = index; 157 | this.currentPattern = this.patterns[index]; 158 | } 159 | 160 | this.setCurrentPatternLength = function (len) { 161 | let tmpLen = this.currentPattern.length; 162 | this.currentPattern.length = len; 163 | 164 | let collisions = this.calculateSynthFill(); 165 | 166 | let ptrnBars = Math.ceil(len / this.barSteps); 167 | for (let i = 0; i <= this.song.length - ptrnBars; i++) { 168 | if (this.song[i][this.currentPatternIndex]) 169 | for (let j = 1; j < ptrnBars; j++) 170 | if (this.song[i + j][this.currentPatternIndex]) 171 | collisions++; 172 | } 173 | 174 | if (len >= tmpLen && collisions > 0) { 175 | this.currentPattern.length = tmpLen; 176 | this.calculateSynthFill(); 177 | return false; 178 | } 179 | 180 | for (let seq of this.currentPattern.patternData) { 181 | seq.notes.length = len; 182 | seq.lengths.length = len; 183 | seq.volumes.length = len; 184 | } 185 | 186 | return true; 187 | } 188 | 189 | this.calcSongLength = function () { 190 | const maxBarsPerPattern = Math.ceil(DEFAULT_PARAMS.maxPatternSteps / this.barSteps); 191 | let maxLen = 0; 192 | 193 | for (let i = this.song.length - 1; i >= 0; i--) { 194 | let maxPatternLen = 0; 195 | 196 | for (let j = 0; j < this.song[i].length; j++) { 197 | if (this.song[i][j]) { 198 | maxPatternLen = Math.max(maxPatternLen, this.patterns[j].length); 199 | } 200 | } 201 | 202 | if (maxPatternLen != 0) { 203 | maxLen = Math.max(maxLen, i + Math.ceil(maxPatternLen / this.barSteps)); 204 | } 205 | 206 | if (i <= maxLen - maxBarsPerPattern) { 207 | return maxLen; 208 | } 209 | } 210 | 211 | return maxLen; 212 | } 213 | 214 | this.isSongEmpty = function () { 215 | if (this.patterns.length > 1) 216 | return false; 217 | 218 | if (this.calcSongLength() > 0) 219 | return false; 220 | 221 | if (this.patterns[0].patternData.length > 1) 222 | return false; 223 | 224 | for (let note of this.patterns[0].patternData[0].notes) 225 | if (note) 226 | return false; 227 | 228 | return true; 229 | } 230 | 231 | this.setBarLength = function (steps) { 232 | if (this.barSteps == steps) 233 | return; 234 | 235 | this.barSteps = Math.floor(steps); 236 | this.patterns = []; 237 | this.patterns.push(new Pattern("ptrn1", this.barSteps)); 238 | 239 | this.fillSong(); 240 | this.calculateSynthFill(); 241 | 242 | this.setCurrentPattern(0); 243 | this.setCurrentLayerSynthIndex(0); 244 | } 245 | 246 | this.calculateDuration = function (bars) { 247 | let beats = bars * this.barSteps / 4; 248 | return beats * (60 / this.bpm); 249 | } 250 | 251 | this.getSongDuration = function () { 252 | let totalBars = this.calcSongLength(); 253 | return Math.ceil(this.calculateDuration(totalBars) + this.calculateDuration(1)); 254 | } 255 | 256 | this.getStartPointTime = function () { 257 | let time = this.calculateDuration(this.arrangeStartPoint); 258 | return secondsToStr(time); 259 | } 260 | 261 | this.getEndPointTime = function () { 262 | let time = this.calculateDuration(this.calcSongLength()); 263 | return secondsToStr(time); 264 | } 265 | 266 | this.copyPattern = function (copyFromPattern, name) { 267 | let len = copyFromPattern.length; 268 | let ptrn = new Pattern(name, len); 269 | ptrn.colorIndex = copyFromPattern.colorIndex; 270 | ptrn.patternData = JSON.parse(JSON.stringify(copyFromPattern.patternData)); 271 | this.patterns.push(ptrn); 272 | this.setCurrentPattern(this.patterns.length - 1); 273 | } 274 | 275 | this.calculateSynthFill = function (statrPoint, endPoint) { 276 | let collisions = 0; 277 | if (statrPoint === undefined || this.song.length != this.synthFill.length) { 278 | this.synthFill = []; 279 | for (let i = 0; i < this.song.length; i++) { 280 | let col = []; 281 | col.length = this.synths.length; 282 | this.synthFill.push(col); 283 | } 284 | 285 | statrPoint = 0; 286 | endPoint = this.song.length - 1; 287 | } 288 | 289 | const setPatternFill = (pattern, startPosition) => { 290 | let patternSynthIndexes = getPatternSynthIndexes(pattern); 291 | 292 | let patternBars = Math.ceil(pattern.length / this.barSteps); 293 | for (let i = startPosition; i < startPosition + patternBars; i++) { 294 | for (let j = 0; j < patternSynthIndexes.length; j++) { 295 | let row = patternSynthIndexes[j]; 296 | 297 | if (this.synthFill[i][row]) 298 | collisions++; 299 | 300 | this.synthFill[i][row] = true; 301 | } 302 | } 303 | } 304 | 305 | for (let i = statrPoint; i <= endPoint; i++) { 306 | for (let j = 0; j < this.song[i].length; j++) { 307 | if (this.song[i][j]) { 308 | let ptrn = this.patterns[j]; 309 | setPatternFill(ptrn, i); 310 | } 311 | } 312 | } 313 | 314 | this.playableLength = this.calcSongLength(); 315 | return collisions; 316 | } 317 | 318 | this.checkSynthConflict = function (patternIndex, position) { 319 | let pattern = this.patterns[patternIndex]; 320 | let addBars = Math.ceil(pattern.length / this.barSteps) - 1; 321 | let endPoint = Math.min(this.song.length - 1, position + addBars); 322 | 323 | for (let i = position; i <= endPoint; i++) { 324 | if (!this.isArrangeCellFree(i, patternIndex)) 325 | return true; 326 | } 327 | 328 | return false; 329 | } 330 | 331 | this.isArrangeCellFree = function (col, row) { 332 | let pattern = this.patterns[row]; 333 | let patternSynthIndexes = getPatternSynthIndexes(pattern); 334 | 335 | for (let i = 0; i < patternSynthIndexes.length; i++) { 336 | if (this.synthFill[col][patternSynthIndexes[i]]) { 337 | return false; 338 | } 339 | } 340 | 341 | return true; 342 | } 343 | 344 | this.switchPatterns = function (indexOne, indexTwo) { 345 | switchElements(this.patterns, indexOne, indexTwo); 346 | 347 | if (this.currentPatternIndex == indexOne) 348 | this.setCurrentPattern(indexTwo) 349 | else if (this.currentPatternIndex == indexTwo) 350 | this.setCurrentPattern(indexOne) 351 | 352 | for (let i = 0; i < this.song.length; i++) 353 | switchElements(this.song[i], indexOne, indexTwo); 354 | 355 | function switchElements(array, indexO, indexT) { 356 | let tmp = array[indexO] || null; 357 | array[indexO] = array[indexT] || null; 358 | array[indexT] = tmp; 359 | } 360 | } 361 | 362 | this.movePattern = function (indexFrom, indexTo) { 363 | if (indexFrom < 0 || indexFrom >= this.patterns.length) { 364 | console.log("invalid pattern index: FROM " + indexFrom); 365 | return; 366 | } 367 | 368 | if (indexTo < 0 || indexTo >= this.patterns.length) { 369 | console.log("invalid pattern index: TO " + indexTo); 370 | return; 371 | } 372 | 373 | if (indexFrom < indexTo) { 374 | for (let i = indexFrom; i < indexTo; i++) 375 | this.switchPatterns(i, i + 1) 376 | } else { 377 | for (let i = indexFrom; i > indexTo; i--) 378 | this.switchPatterns(i - 1, i) 379 | } 380 | } 381 | 382 | this.sortPatternsByName = function (isDescending) { 383 | for (let i = 0; i < this.patterns.length; i++) { 384 | let tmpIndex = i; 385 | for (let j = i + 1; j < this.patterns.length; j++) { 386 | if (comparePatterns(this.patterns[j], this.patterns[tmpIndex], isDescending)) 387 | tmpIndex = j; 388 | } 389 | 390 | this.switchPatterns(i, tmpIndex); 391 | } 392 | 393 | function comparePatterns(one, two, isDescending) { 394 | // return true if one-two in order 395 | if (!isDescending && (one.name <= two.name)) 396 | return true; 397 | 398 | if (isDescending && (one.name >= two.name)) 399 | return true; 400 | 401 | return false; 402 | } 403 | } 404 | 405 | this.cleanup = function () { 406 | // Delete layers without notes or assigned synth 407 | for (let i = this.patterns.length - 1; i >= 0; i--) { 408 | for (let j = this.patterns[i].patternData.length - 1; j >= 0; j--) { 409 | 410 | let empty = true; 411 | for (let k = 0; k < this.patterns[i].length; k++) 412 | if (this.patterns[i].patternData[j].notes[k]) { 413 | empty = false; 414 | break; 415 | } 416 | 417 | if (empty) 418 | this.patterns[i].patternData[j].synthIndex = null; 419 | 420 | if (this.patterns[i].patternData[j].synthIndex === null) 421 | if (this.patterns[i].patternData.length > 1) { 422 | this.patterns[i].activeIndex = j; 423 | this.patterns[i].deleteActiveLayer(); 424 | } 425 | } 426 | 427 | if (this.patterns.length > 1) 428 | if (this.patterns[i].patternData[0].synthIndex === null) { 429 | this.setCurrentPattern(i) 430 | this.deleteCurrentPattern(); 431 | } 432 | } 433 | 434 | if (this.patterns.length <= 1) 435 | return; 436 | 437 | // Replace pattern duplicates with "origin" 438 | for (let i = 0; i < this.patterns.length - 1; i++) 439 | for (let j = i + 1; j < this.patterns.length; j++) 440 | if (comparePatterns(this.patterns[i], this.patterns[j])) 441 | for (let k = 0; k < this.song.length; k++) 442 | if (this.song[k][j]) { 443 | this.song[k][j] = false; 444 | this.song[k][i] = true; 445 | } 446 | 447 | // Delete unused patterns 448 | for (let i = this.patterns.length - 1; i >= 0; i--) { 449 | let used = false; 450 | for (let j = 0; j < this.song.length; j++) 451 | if (this.song[j][i]) { 452 | used = true; 453 | break; 454 | } 455 | 456 | if (!used && this.patterns.length > 1) { 457 | this.setCurrentPattern(i) 458 | this.deleteCurrentPattern(); 459 | } 460 | } 461 | } 462 | 463 | this.getCleanSynthParams = function (synthIndex) { 464 | let params = this.synthParams[synthIndex]; 465 | let packed = {}; 466 | for (let key in params) 467 | packed[key] = params[key]; 468 | 469 | for (let osc of ["osc1", "osc2", "osc3", "lfo1", "lfo2"]) 470 | if (packed["synth-" + osc + "-type"] != "custom") 471 | packed["synth-" + osc + "-partials"] = ""; 472 | 473 | return packed; 474 | } 475 | 476 | function comparePatterns(ptrn1, ptrn2) { 477 | if (ptrn1.length != ptrn2.length) 478 | return false; 479 | 480 | if (ptrn1.patternData.length != ptrn2.patternData.length) 481 | return false; 482 | 483 | for (let i = 0; i < ptrn1.patternData.length; i++) { 484 | if (ptrn1.patternData[i].synthIndex != ptrn2.patternData[i].synthIndex) 485 | return false; 486 | 487 | if (!compareArrays(ptrn1.patternData[i].notes, ptrn2.patternData[i].notes, false)) 488 | return false; 489 | 490 | if (!compareArrays(ptrn1.patternData[i].lengths, ptrn2.patternData[i].lengths, false)) 491 | return false; 492 | 493 | if (!compareArrays(ptrn1.patternData[i].volumes, ptrn2.patternData[i].volumes, false)) 494 | return false; 495 | 496 | if (!compareArrays(ptrn1.patternData[i].filtF, ptrn2.patternData[i].filtF, true)) 497 | return false; 498 | 499 | if (!compareArrays(ptrn1.patternData[i].filtQ, ptrn2.patternData[i].filtQ, true)) 500 | return false; 501 | } 502 | 503 | return true; 504 | } 505 | 506 | function compareArrays(arr1, arr2, isStrict) { 507 | let len = Math.max(arr1.length, arr2.length); 508 | 509 | for (let i = 0; i < len; i++) { 510 | let a = arr1[i] || null; 511 | let b = arr2[i] || null; 512 | 513 | if (isStrict) { 514 | if (arr1[i] === 0) 515 | a = 0; 516 | 517 | if (arr2[i] === 0) 518 | b = 0; 519 | } 520 | 521 | if (a !== b) 522 | return false; 523 | } 524 | 525 | return true; 526 | } 527 | 528 | function getPatternSynthIndexes(pattern) { 529 | let patternSynthIndexes = []; 530 | for (let i = 0; i < pattern.patternData.length; i++) { 531 | let synthIndex = pattern.patternData[i].synthIndex; 532 | if (synthIndex !== null) 533 | patternSynthIndexes.push(synthIndex); 534 | } 535 | 536 | return patternSynthIndexes; 537 | } 538 | 539 | function generateName(prefix, givenArray, startIndex) { 540 | let index = startIndex; 541 | 542 | if (startIndex === undefined) 543 | index = givenArray.length + 1; 544 | 545 | let name = String(prefix) + (index || ""); 546 | 547 | while (givenArray.some(e => e == name)) 548 | name = String(prefix) + ++index; 549 | 550 | return name; 551 | } 552 | 553 | function secondsToStr(time) { 554 | let minutes = String(Math.floor(time / 60)); 555 | let seconds = String(Math.floor(time % 60)); 556 | let decimal = String(Math.floor((time - Math.floor(time)) * 100)); 557 | 558 | if (minutes.length == 1) 559 | minutes = "0" + minutes; 560 | 561 | if (seconds.length == 1) 562 | seconds = "0" + seconds; 563 | 564 | if (decimal.length == 1) 565 | decimal = "0" + decimal; 566 | 567 | return minutes + ":" + seconds + "." + decimal; 568 | } 569 | } -------------------------------------------------------------------------------- /js/synth-presets.js: -------------------------------------------------------------------------------- 1 | SYNTH_PRESETS = [ 2 | { 3 | "-name": "[default]", 4 | "synth-osc1-type": "triangle" 5 | }, 6 | 7 | { 8 | "-name": "3-in-1", 9 | "synth-envelope-attack": 1.98, 10 | "synth-envelope-decay": 0.96, 11 | "synth-envelope-release": 8.92, 12 | "synth-filter-frequency": 1.86, 13 | "synth-filter-type": "lowpass", 14 | "synth-osc1-type": "sawtooth", 15 | "synth-osc2-detune": 10, 16 | "synth-osc2-level": 0.36, 17 | "synth-osc2-octave": -1, 18 | "synth-osc2-type": "sawtooth", 19 | "synth-osc3-detune": -500, 20 | "synth-osc3-level": 0.69, 21 | "synth-osc3-type": "sawtooth" 22 | }, 23 | 24 | { 25 | "-name": "blip", 26 | "synth-envelope-attack": 1.34, 27 | "synth-envelope-decay": 0, 28 | "synth-envelope-release": 7.78, 29 | "synth-filter-frequency": 2.98, 30 | "synth-filter-type": "lowpass", 31 | "synth-lfo1-frequency": 9, 32 | "synth-lfo1-type": "square", 33 | "synth-osc1-level": 0.5, 34 | "synth-osc1-mod-input": "lfo1", 35 | "synth-osc1-mod-value": 0.98, 36 | "synth-osc1-type": "square" 37 | }, 38 | 39 | { 40 | "-name": "blip paired", 41 | "synth-envelope-attack": 1.34, 42 | "synth-envelope-decay": 0, 43 | "synth-envelope-release": 7.78, 44 | "synth-filter-frequency": 2.98, 45 | "synth-filter-type": "lowpass", 46 | "synth-lfo1-frequency": 9, 47 | "synth-lfo1-type": "square", 48 | "synth-osc1-level": 0.5, 49 | "synth-osc1-mod-input": "lfo1", 50 | "synth-osc1-mod-value": 0.98, 51 | "synth-osc1-type": "square", 52 | "synth-osc2-level": 0.5, 53 | "synth-osc2-mod-input": "lfo1", 54 | "synth-osc2-mod-value": 0.42, 55 | "synth-osc2-type": "square" 56 | }, 57 | 58 | { 59 | "-name": "buzzy bass", 60 | "synth-envelope-attack": 0.42, 61 | "synth-envelope-release": 11.64, 62 | "synth-envelope-sustain": 0.8, 63 | "synth-filter-frequency": 2.08, 64 | "synth-filter-quality": 6.1, 65 | "synth-filter-type": "lowpass", 66 | "synth-osc1-level": 0.75, 67 | "synth-osc1-type": "sawtooth", 68 | "synth-osc2-detune": 10, 69 | "synth-osc2-level": 0.25, 70 | "synth-osc2-octave": -1, 71 | "synth-osc2-type": "sawtooth" 72 | }, 73 | 74 | { 75 | "-name": "buzzy lead", 76 | "synth-envelope-attack": 5.46, 77 | "synth-envelope-decay": 3.5, 78 | "synth-envelope-release": 8.46, 79 | "synth-envelope-sustain": 0.64, 80 | "synth-filter-frequency": 3, 81 | "synth-filter-mod-input": "envelopeMod", 82 | "synth-filter-mod-value": 2, 83 | "synth-filter-quality": 5.4, 84 | "synth-filter-type": "lowpass", 85 | "synth-mod-envelope-attack": 0, 86 | "synth-mod-envelope-release": 5.98, 87 | "synth-mod-envelope-type": "exponential", 88 | "synth-osc1-level": 0.51, 89 | "synth-osc1-mod-input": "osc2", 90 | "synth-osc1-mod-value": 3, 91 | "synth-osc1-type": "sawtooth", 92 | "synth-osc2-detune": 10, 93 | "synth-osc2-level": 0.25, 94 | "synth-osc2-octave": -2, 95 | "synth-osc2-type": "sawtooth" 96 | }, 97 | 98 | { 99 | "-name": "chorded pad", 100 | "synth-amplifier-gain": 0.8, 101 | "synth-envelope-attack": 4.92, 102 | "synth-envelope-decay": 6.44, 103 | "synth-envelope-release": 8.22, 104 | "synth-envelope-sustain": 0.63, 105 | "synth-filter-frequency": 1.98, 106 | "synth-filter-quality": 12.5, 107 | "synth-filter-type": "lowpass", 108 | "synth-osc1-level": 0.5, 109 | "synth-osc1-type": "sawtooth", 110 | "synth-osc2-detune": -500, 111 | "synth-osc2-level": 0.75, 112 | "synth-osc2-octave": 1, 113 | "synth-osc2-type": "square", 114 | "synth-osc3-detune": -400, 115 | "synth-osc3-level": 0.25, 116 | "synth-osc3-type": "sawtooth" 117 | }, 118 | 119 | { 120 | "-name": "clap", 121 | "synth-amplifier-gain": 1.1, 122 | "synth-envelope-attack": 1.06, 123 | "synth-envelope-decay": 5.4, 124 | "synth-envelope-release": 8.34, 125 | "synth-envelope-sustain": 0.03, 126 | "synth-filter-frequency": 2.38, 127 | "synth-filter-quality": 2.3, 128 | "synth-filter-type": "bandpass", 129 | "synth-noise-level": 1, 130 | "synth-noise-type": "white", 131 | "synth-osc1-type": "[none]" 132 | }, 133 | 134 | { 135 | "-name": "clarinet", 136 | "synth-envelope-attack": 4.92, 137 | "synth-envelope-decay": 6.44, 138 | "synth-envelope-release": 6.12, 139 | "synth-envelope-sustain": 0.63, 140 | "synth-filter-frequency": 1.78, 141 | "synth-filter-quality": 9.2, 142 | "synth-filter-type": "lowpass", 143 | "synth-osc1-level": 0.75, 144 | "synth-osc1-type": "square", 145 | "synth-osc2-level": 0.5, 146 | "synth-osc2-type": "sawtooth" 147 | }, 148 | 149 | { 150 | "-name": "ding", 151 | "synth-amplifier-mod-input": "osc2", 152 | "synth-amplifier-mod-value": 0.58, 153 | "synth-envelope-attack": 0.8, 154 | "synth-envelope-decay": 5.7, 155 | "synth-envelope-release": 8.86, 156 | "synth-envelope-sustain": 0.48, 157 | "synth-filter-frequency": 2.3, 158 | "synth-filter-quality": 9.1, 159 | "synth-filter-type": "lowpass", 160 | "synth-lfo1-frequency": 7.49, 161 | "synth-lfo1-type": "sine", 162 | "synth-osc1-level": 0.49, 163 | "synth-osc1-type": "square", 164 | "synth-osc2-mod-input": "lfo1", 165 | "synth-osc2-mod-value": 0.18, 166 | "synth-osc2-octave": 2, 167 | "synth-osc2-type": "square" 168 | }, 169 | 170 | { 171 | "-name": "e-piano", 172 | "synth-envelope-attack": 0.4, 173 | "synth-envelope-decay": 4.04, 174 | "synth-envelope-release": 10.44, 175 | "synth-envelope-sustain": 0.51, 176 | "synth-filter-frequency": 1.88, 177 | "synth-filter-mod-input": "envelopeMod", 178 | "synth-filter-mod-value": 1.58, 179 | "synth-filter-quality": 8.6, 180 | "synth-filter-type": "lowpass", 181 | "synth-mod-envelope-attack": 0.7, 182 | "synth-mod-envelope-decay": 3.36, 183 | "synth-mod-envelope-release": 3.34, 184 | "synth-mod-envelope-type": "exponential", 185 | "synth-osc1-level": 0.75, 186 | "synth-osc1-type": "triangle", 187 | "synth-osc2-detune": -10, 188 | "synth-osc2-level": 0.5, 189 | "synth-osc2-octave": 1, 190 | "synth-osc2-type": "triangle" 191 | }, 192 | 193 | { 194 | "-name": "FM pad", 195 | "synth-envelope-attack": 3.26, 196 | "synth-envelope-decay": 6.82, 197 | "synth-envelope-release": 8.36, 198 | "synth-envelope-sustain": 0.62, 199 | "synth-filter-frequency": 2.5, 200 | "synth-filter-mod-input": "envelopeMod", 201 | "synth-filter-mod-value": 3.1, 202 | "synth-filter-quality": 5.3, 203 | "synth-filter-type": "lowpass", 204 | "synth-mod-envelope-attack": 0, 205 | "synth-mod-envelope-decay": 7.2, 206 | "synth-mod-envelope-release": 7.14, 207 | "synth-mod-envelope-type": "exponential", 208 | "synth-osc1-mod-input": "osc2", 209 | "synth-osc1-mod-value": 3, 210 | "synth-osc1-type": "sine", 211 | "synth-osc2-octave": 3, 212 | "synth-osc2-type": "sine" 213 | }, 214 | 215 | { 216 | "-name": "harmonica", 217 | "synth-amplifier-gain": 0.8, 218 | "synth-envelope-attack": 4.92, 219 | "synth-envelope-decay": 6.44, 220 | "synth-envelope-release": 6.12, 221 | "synth-envelope-sustain": 0.63, 222 | "synth-filter-frequency": 3.54, 223 | "synth-filter-quality": 8.2, 224 | "synth-filter-type": "lowpass", 225 | "synth-osc1-level": 0.75, 226 | "synth-osc1-type": "square", 227 | "synth-osc2-detune": 15, 228 | "synth-osc2-level": 0.5, 229 | "synth-osc2-type": "sawtooth" 230 | }, 231 | 232 | { 233 | "-name": "hat cl", 234 | "synth-envelope-attack": 0, 235 | "synth-envelope-decay": 3.78, 236 | "synth-envelope-release": 5.34, 237 | "synth-envelope-sustain": 0.05, 238 | "synth-filter-frequency": 5, 239 | "synth-filter-quality": 12.2, 240 | "synth-filter-type": "highpass", 241 | "synth-noise-level": 1, 242 | "synth-noise-type": "white", 243 | "synth-osc1-type": "[none]" 244 | }, 245 | 246 | { 247 | "-name": "hat op", 248 | "synth-amplifier-gain": 1.1, 249 | "synth-envelope-attack": 0, 250 | "synth-envelope-decay": 5.54, 251 | "synth-envelope-release": 7.92, 252 | "synth-envelope-sustain": 0.1, 253 | "synth-filter-frequency": 4.74, 254 | "synth-filter-mod-input": "envelopeMod", 255 | "synth-filter-mod-value": 2.84, 256 | "synth-filter-quality": 12.5, 257 | "synth-filter-type": "highpass", 258 | "synth-mod-envelope-attack": 0, 259 | "synth-mod-envelope-decay": 4.9, 260 | "synth-mod-envelope-release": 4.88, 261 | "synth-mod-envelope-type": "exponential", 262 | "synth-noise-level": 1, 263 | "synth-noise-type": "white", 264 | "synth-osc1-type": "[none]" 265 | }, 266 | 267 | { 268 | "-name": "kick", 269 | "synth-envelope-attack": 0.06, 270 | "synth-envelope-decay": 6.84, 271 | "synth-envelope-release": 8.48, 272 | "synth-envelope-sustain": 0.06, 273 | "synth-mod-envelope-attack": 0, 274 | "synth-mod-envelope-type": "exponential", 275 | "synth-noise-level": 0.1, 276 | "synth-noise-type": "brown", 277 | "synth-osc1-mod-input": "envelopeMod", 278 | "synth-osc1-mod-value": 4.72, 279 | "synth-osc1-octave": -2, 280 | "synth-osc1-type": "sine" 281 | }, 282 | 283 | { 284 | "-name": "lead", 285 | "synth-envelope-attack": 0.6, 286 | "synth-envelope-decay": 5.46, 287 | "synth-envelope-release": 10.54, 288 | "synth-envelope-sustain": 0.63, 289 | "synth-filter-frequency": 1.64, 290 | "synth-filter-mod-input": "envelopeMod", 291 | "synth-filter-mod-value": -1, 292 | "synth-filter-type": "lowpass", 293 | "synth-mod-envelope-attack": 0.78, 294 | "synth-mod-envelope-decay": 4.76, 295 | "synth-mod-envelope-release": 4.8, 296 | "synth-mod-envelope-type": "linear", 297 | "synth-osc1-type": "sawtooth", 298 | "synth-osc2-detune": 10, 299 | "synth-osc2-level": 0.5, 300 | "synth-osc2-type": "sawtooth" 301 | }, 302 | 303 | { 304 | "-name": "metallic", 305 | "synth-envelope-attack": 0.94, 306 | "synth-envelope-decay": 5.52, 307 | "synth-envelope-release": 9.06, 308 | "synth-envelope-sustain": 0.05, 309 | "synth-filter-frequency": 2.64, 310 | "synth-filter-mod-input": "envelopeMod", 311 | "synth-filter-mod-value": 1.02, 312 | "synth-filter-quality": 13.1, 313 | "synth-filter-type": "highpass", 314 | "synth-mod-envelope-attack": 4.52, 315 | "synth-mod-envelope-decay": 4.48, 316 | "synth-mod-envelope-release": 4.46, 317 | "synth-mod-envelope-type": "linear", 318 | "synth-osc1-level": 0.5, 319 | "synth-osc1-type": "square", 320 | "synth-osc2-detune": -140, 321 | "synth-osc2-level": 0.25, 322 | "synth-osc2-octave": 1, 323 | "synth-osc2-type": "square", 324 | "synth-osc3-detune": -140, 325 | "synth-osc3-level": 0.25, 326 | "synth-osc3-octave": 2, 327 | "synth-osc3-type": "square" 328 | }, 329 | 330 | { 331 | "-name": "octa bass", 332 | "synth-envelope-decay": 5.1, 333 | "synth-envelope-release": 7.66, 334 | "synth-envelope-sustain": 0.6, 335 | "synth-filter-mod-input": "envelopeMod", 336 | "synth-filter-mod-value": 3.66, 337 | "synth-filter-type": "lowpass", 338 | "synth-mod-envelope-attack": 0, 339 | "synth-mod-envelope-type": "exponential", 340 | "synth-osc1-level": 0.72, 341 | "synth-osc1-type": "sawtooth", 342 | "synth-osc2-level": 0.25, 343 | "synth-osc2-octave": -1, 344 | "synth-osc2-type": "square" 345 | }, 346 | 347 | { 348 | "-name": "organ", 349 | "synth-envelope-attack": 5.42, 350 | "synth-envelope-decay": 6.42, 351 | "synth-envelope-release": 9.56, 352 | "synth-envelope-sustain": 0.71, 353 | "synth-filter-frequency": 1.38, 354 | "synth-filter-quality": 2.7, 355 | "synth-filter-type": "lowpass", 356 | "synth-osc1-level": 0.94, 357 | "synth-osc1-type": "sawtooth", 358 | "synth-osc2-detune": -5, 359 | "synth-osc2-level": 0.75, 360 | "synth-osc2-octave": 1, 361 | "synth-osc2-type": "sawtooth", 362 | "synth-osc3-detune": 10, 363 | "synth-osc3-level": 0.5, 364 | "synth-osc3-octave": -1, 365 | "synth-osc3-type": "sawtooth" 366 | }, 367 | 368 | { 369 | "-name": "pan flute", 370 | "synth-envelope-attack": 4.08, 371 | "synth-envelope-decay": 5.1, 372 | "synth-envelope-release": 5.98, 373 | "synth-envelope-sustain": 0.72, 374 | "synth-filter-frequency": 3.2, 375 | "synth-filter-quality": 1.8, 376 | "synth-filter-type": "lowpass", 377 | "synth-noise-level": 0.05, 378 | "synth-noise-type": "pink", 379 | "synth-osc1-type": "triangle" 380 | }, 381 | 382 | { 383 | "-name": "peak bass", 384 | "synth-envelope-release": 5.96, 385 | "synth-filter-mod-input": "envelopeMod", 386 | "synth-filter-mod-value": 1.48, 387 | "synth-filter-quality": 31, 388 | "synth-filter-type": "lowpass", 389 | "synth-mod-envelope-attack": 0, 390 | "synth-mod-envelope-decay": 7.48, 391 | "synth-mod-envelope-release": 7.46, 392 | "synth-mod-envelope-type": "exponential", 393 | "synth-osc1-level": 0.38, 394 | "synth-osc1-octave": -1, 395 | "synth-osc1-type": "sawtooth" 396 | }, 397 | 398 | { 399 | "-name": "piano", 400 | "synth-envelope-attack": 0.3, 401 | "synth-envelope-decay": 4, 402 | "synth-envelope-release": 9.34, 403 | "synth-envelope-sustain": 0.45, 404 | "synth-filter-frequency": 1.74, 405 | "synth-filter-quality": 6.7, 406 | "synth-filter-type": "lowpass", 407 | "synth-osc1-level": 0.62, 408 | "synth-osc1-type": "square", 409 | "synth-osc2-detune": 10, 410 | "synth-osc2-level": 0.38, 411 | "synth-osc2-octave": 1, 412 | "synth-osc2-type": "square" 413 | }, 414 | 415 | { 416 | "-name": "siren a", 417 | "synth-filter-frequency": 0.7, 418 | "synth-filter-mod-input": "envelopeMod", 419 | "synth-filter-mod-value": 2, 420 | "synth-filter-quality": 5.1, 421 | "synth-filter-type": "lowpass", 422 | "synth-lfo1-frequency": 6.62, 423 | "synth-lfo1-type": "triangle", 424 | "synth-mod-envelope-attack": 10.36, 425 | "synth-mod-envelope-decay": 10.36, 426 | "synth-mod-envelope-release": 0, 427 | "synth-mod-envelope-type": "linear", 428 | "synth-osc1-mod-input": "lfo1", 429 | "synth-osc1-mod-value": 2.32, 430 | "synth-osc1-type": "square" 431 | }, 432 | 433 | { 434 | "-name": "siren b", 435 | "synth-filter-frequency": 1.22, 436 | "synth-filter-mod-input": "envelopeMod", 437 | "synth-filter-mod-value": 2, 438 | "synth-filter-quality": 5.1, 439 | "synth-filter-type": "lowpass", 440 | "synth-lfo1-frequency": 4.55, 441 | "synth-lfo1-type": "square", 442 | "synth-mod-envelope-attack": 10.36, 443 | "synth-mod-envelope-decay": 10.36, 444 | "synth-mod-envelope-release": 0, 445 | "synth-mod-envelope-type": "linear", 446 | "synth-osc1-mod-input": "lfo1", 447 | "synth-osc1-mod-value": 1.62, 448 | "synth-osc1-type": "square" 449 | }, 450 | 451 | { 452 | "-name": "snap", 453 | "synth-envelope-attack": 2.84, 454 | "synth-envelope-decay": 5.26, 455 | "synth-envelope-release": 7.98, 456 | "synth-envelope-sustain": 0.03, 457 | "synth-filter-frequency": 3.32, 458 | "synth-filter-quality": 4.9, 459 | "synth-filter-type": "bandpass", 460 | "synth-noise-level": 1, 461 | "synth-noise-type": "white", 462 | "synth-osc1-type": "[none]" 463 | }, 464 | 465 | { 466 | "-name": "snare", 467 | "synth-amplifier-gain": 0.85, 468 | "synth-envelope-attack": 0, 469 | "synth-envelope-decay": 5.7, 470 | "synth-envelope-release": 8.26, 471 | "synth-envelope-sustain": 0.03, 472 | "synth-filter-frequency": 3.38, 473 | "synth-filter-mod-input": "envelopeMod", 474 | "synth-filter-mod-value": 4, 475 | "synth-filter-quality": 7.9, 476 | "synth-filter-type": "lowpass", 477 | "synth-mod-envelope-attack": 0, 478 | "synth-mod-envelope-decay": 4.78, 479 | "synth-mod-envelope-release": 4.68, 480 | "synth-mod-envelope-type": "exponential", 481 | "synth-noise-level": 1, 482 | "synth-noise-type": "white", 483 | "synth-osc1-type": "[none]" 484 | }, 485 | 486 | { 487 | "-name": "square bass", 488 | "synth-envelope-decay": 6.66, 489 | "synth-envelope-release": 8.22, 490 | "synth-envelope-sustain": 0.5, 491 | "synth-filter-mod-input": "envelopeMod", 492 | "synth-filter-mod-value": 4.74, 493 | "synth-filter-type": "lowpass", 494 | "synth-mod-envelope-attack": 0, 495 | "synth-mod-envelope-release": 5.8, 496 | "synth-mod-envelope-type": "exponential", 497 | "synth-osc1-octave": -1, 498 | "synth-osc1-type": "square" 499 | }, 500 | 501 | { 502 | "-name": "strings", 503 | "synth-envelope-attack": 9.62, 504 | "synth-envelope-decay": 3.5, 505 | "synth-envelope-release": 8.46, 506 | "synth-envelope-sustain": 0.85, 507 | "synth-filter-frequency": 3, 508 | "synth-filter-quality": 9.8, 509 | "synth-filter-type": "lowpass", 510 | "synth-osc1-type": "sawtooth", 511 | "synth-osc2-detune": 10, 512 | "synth-osc2-level": 0.69, 513 | "synth-osc2-type": "sawtooth" 514 | }, 515 | 516 | { 517 | "-name": "tick", 518 | "synth-amplifier-gain": 0.9, 519 | "synth-envelope-attack": 0, 520 | "synth-envelope-decay": 2.96, 521 | "synth-envelope-release": 2.86, 522 | "synth-envelope-sustain": 0, 523 | "synth-filter-frequency": 3.18, 524 | "synth-filter-type": "highpass", 525 | "synth-noise-level": 0.74, 526 | "synth-noise-type": "white", 527 | "synth-osc1-type": "[none]" 528 | }, 529 | 530 | { 531 | "-name": "tom", 532 | "synth-envelope-attack": 0.4, 533 | "synth-envelope-decay": 6.1, 534 | "synth-envelope-release": 6.84, 535 | "synth-envelope-sustain": 0, 536 | "synth-mod-envelope-attack": 0, 537 | "synth-mod-envelope-type": "exponential", 538 | "synth-osc1-mod-input": "envelopeMod", 539 | "synth-osc1-mod-value": 3.92, 540 | "synth-osc1-octave": -1, 541 | "synth-osc1-type": "sine" 542 | }, 543 | 544 | { 545 | "-name": "tuba", 546 | "synth-envelope-attack": 4.72, 547 | "synth-envelope-decay": 5.62, 548 | "synth-envelope-release": 9, 549 | "synth-envelope-sustain": 0.57, 550 | "synth-filter-frequency": 2, 551 | "synth-filter-mod-input": "envelopeMod", 552 | "synth-filter-mod-value": -1.34, 553 | "synth-filter-quality": 3.2, 554 | "synth-filter-type": "lowpass", 555 | "synth-mod-envelope-attack": 0, 556 | "synth-mod-envelope-decay": 5.5, 557 | "synth-mod-envelope-release": 5.62, 558 | "synth-mod-envelope-type": "linear", 559 | "synth-osc1-level": 0.70, 560 | "synth-osc1-type": "sawtooth", 561 | "synth-osc2-detune": -20, 562 | "synth-osc2-level": 0.95, 563 | "synth-osc2-type": "triangle" 564 | }, 565 | 566 | { 567 | "-name": "wind", 568 | "synth-envelope-attack": 8.84, 569 | "synth-envelope-release": 11.46, 570 | "synth-filter-frequency": 1.78, 571 | "synth-filter-mod-input": "lfo1", 572 | "synth-filter-mod-value": 0.98, 573 | "synth-filter-type": "lowpass", 574 | "synth-lfo1-frequency": 3.01, 575 | "synth-lfo1-type": "sine", 576 | "synth-noise-level": 0.75, 577 | "synth-noise-type": "pink", 578 | "synth-osc1-type": "[none]" 579 | }, 580 | 581 | { 582 | "-name": "wow bass", 583 | "synth-amplifier-gain": 0.9, 584 | "synth-envelope-attack": 1.48, 585 | "synth-envelope-release": 10.46, 586 | "synth-filter-frequency": 0.98, 587 | "synth-filter-mod-input": "envelopeMod", 588 | "synth-filter-mod-value": 1, 589 | "synth-filter-quality": 17.2, 590 | "synth-filter-type": "lowpass", 591 | "synth-mod-envelope-attack": 3.88, 592 | "synth-mod-envelope-decay": 8.04, 593 | "synth-mod-envelope-release": 8.08, 594 | "synth-mod-envelope-type": "exponential", 595 | "synth-osc1-octave": -1, 596 | "synth-osc1-type": "sawtooth" 597 | }, 598 | 599 | { 600 | "-name": "x-mod bass", 601 | "synth-envelope-attack": 0, 602 | "synth-envelope-decay": 3.14, 603 | "synth-envelope-release": 7.66, 604 | "synth-envelope-sustain": 0.6, 605 | "synth-filter-frequency": 1.28, 606 | "synth-filter-mod-input": "envelopeMod", 607 | "synth-filter-mod-value": 1.86, 608 | "synth-filter-quality": 13.3, 609 | "synth-filter-type": "lowpass", 610 | "synth-mod-envelope-attack": 0, 611 | "synth-mod-envelope-decay": 7.2, 612 | "synth-mod-envelope-release": 7.16, 613 | "synth-mod-envelope-type": "exponential", 614 | "synth-osc1-level": 0.61, 615 | "synth-osc1-mod-input": "osc2", 616 | "synth-osc1-mod-value": 1.98, 617 | "synth-osc1-octave": -1, 618 | "synth-osc1-type": "square", 619 | "synth-osc2-detune": -5, 620 | "synth-osc2-level": 1, 621 | "synth-osc2-octave": -1, 622 | "synth-osc2-type": "triangle" 623 | }, 624 | 625 | { 626 | "-name": "x-mod lead", 627 | "synth-envelope-decay": 4.86, 628 | "synth-envelope-release": 8.3, 629 | "synth-envelope-sustain": 0.44, 630 | "synth-filter-frequency": 2.2, 631 | "synth-filter-mod-input": "envelopeMod", 632 | "synth-filter-mod-value": 4.52, 633 | "synth-filter-quality": 8.2, 634 | "synth-filter-type": "lowpass", 635 | "synth-mod-envelope-attack": 0, 636 | "synth-mod-envelope-decay": 5.38, 637 | "synth-mod-envelope-release": 10.4, 638 | "synth-mod-envelope-sustain": 0.38, 639 | "synth-mod-envelope-type": "exponential", 640 | "synth-osc1-level": 0.5, 641 | "synth-osc1-mod-input": "osc2", 642 | "synth-osc1-mod-value": 3, 643 | "synth-osc1-type": "square", 644 | "synth-osc2-detune": -15, 645 | "synth-osc2-level": 0.5, 646 | "synth-osc2-type": "sawtooth" 647 | }, 648 | 649 | { 650 | "-name": "zap", 651 | "synth-envelope-attack": 0, 652 | "synth-envelope-decay": 7.56, 653 | "synth-envelope-release": 7.56, 654 | "synth-envelope-sustain": 0, 655 | "synth-filter-frequency": 0.68, 656 | "synth-filter-mod-input": "envelopeMod", 657 | "synth-filter-mod-value": 4, 658 | "synth-filter-quality": 3.7, 659 | "synth-filter-type": "lowpass", 660 | "synth-mod-envelope-attack": 0, 661 | "synth-mod-envelope-decay": 6.26, 662 | "synth-mod-envelope-release": 6.26, 663 | "synth-mod-envelope-type": "exponential", 664 | "synth-osc1-level": 0.95, 665 | "synth-osc1-mod-input": "envelopeMod", 666 | "synth-osc1-mod-value": 6, 667 | "synth-osc1-type": "square" 668 | } 669 | ]; --------------------------------------------------------------------------------