├── .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 |
--------------------------------------------------------------------------------
/img/range8marks.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/img/range10marks.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/img/range12marks.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/img/range8marks2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/img/adsr.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
44 |
--------------------------------------------------------------------------------
/img/noise.svg:
--------------------------------------------------------------------------------
1 |
2 |
49 |
--------------------------------------------------------------------------------
/img/effect.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/img/modulation-osc.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 | 
42 | 
43 | 
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 |
59 |
--------------------------------------------------------------------------------
/img/oscillator.svg:
--------------------------------------------------------------------------------
1 |
2 |
59 |
--------------------------------------------------------------------------------
/img/gear.svg:
--------------------------------------------------------------------------------
1 |
2 |
39 |
--------------------------------------------------------------------------------
/img/modulation-amp.svg:
--------------------------------------------------------------------------------
1 |
2 |
61 |
--------------------------------------------------------------------------------
/img/modulation-filt.svg:
--------------------------------------------------------------------------------
1 |
2 |
64 |
--------------------------------------------------------------------------------
/img/filter.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 | ];
--------------------------------------------------------------------------------