mirror of
https://github.com/mtkennerly/bosca-ceoil-js.git
synced 2024-12-22 22:12:22 +01:00
Initial commit
This commit is contained in:
commit
178455555b
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{feature,json,md,yaml,yml}]
|
||||||
|
indent_size = 2
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
out/
|
||||||
|
assets/
|
||||||
|
audio/**/*_data/
|
||||||
|
*.aup
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Matthew T. Kennerly (mtkennerly)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
38
README.md
Normal file
38
README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Bosca Coeil JS
|
||||||
|
This project is an HTML/CSS/JavaScript (TypeScript) rewrite of
|
||||||
|
[Bosca Coeil](https://github.com/TerryCavanagh/boscaceoil) using samples
|
||||||
|
of the preset instruments from [SiON](https://github.com/keim/SiON)
|
||||||
|
(rather than a port of SiON itself).
|
||||||
|
|
||||||
|
It is still a prototype, so significant functionality is missing.
|
||||||
|
|
||||||
|
## Sample creation
|
||||||
|
This was how the SiON samples were recorded:
|
||||||
|
|
||||||
|
* 100% system volume.
|
||||||
|
* Create a Bosca Coeil song with 18 patterns, where every even pattern
|
||||||
|
is blank. Every odd pattern is a single, full-measure note, starting
|
||||||
|
from C1 and going up to C9.
|
||||||
|
* Record the song in Audacity and use the Sound Finder function to split
|
||||||
|
the song into one segment per note, with these settings:
|
||||||
|
* Silence threshold (-dB): `70.0`
|
||||||
|
* Minimum silence duration( seconds): `0.300`
|
||||||
|
* Label starting point: `0.010`
|
||||||
|
* Label ending point: `0.010`
|
||||||
|
* Add label at the end: `0` (no)
|
||||||
|
* Export all segments as FLAC at level 5 and 16-bit depth.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
* [Node](https://nodejs.org/en)
|
||||||
|
|
||||||
|
Initial setup:
|
||||||
|
|
||||||
|
* `npm install`
|
||||||
|
* `npm run assets`
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
* `npm run dev`
|
||||||
|
* Open `http://127.0.0.1:8080/index.html` in your browser
|
BIN
audio/midi.piano1/c1.flac
Normal file
BIN
audio/midi.piano1/c1.flac
Normal file
Binary file not shown.
BIN
audio/midi.piano1/c2.flac
Normal file
BIN
audio/midi.piano1/c2.flac
Normal file
Binary file not shown.
BIN
audio/midi.piano1/c3.flac
Normal file
BIN
audio/midi.piano1/c3.flac
Normal file
Binary file not shown.
BIN
audio/midi.piano1/c4.flac
Normal file
BIN
audio/midi.piano1/c4.flac
Normal file
Binary file not shown.
BIN
audio/midi.piano1/c5.flac
Normal file
BIN
audio/midi.piano1/c5.flac
Normal file
Binary file not shown.
BIN
audio/midi.piano1/c6.flac
Normal file
BIN
audio/midi.piano1/c6.flac
Normal file
Binary file not shown.
BIN
audio/midi.piano1/c7.flac
Normal file
BIN
audio/midi.piano1/c7.flac
Normal file
Binary file not shown.
BIN
audio/midi.piano1/c8.flac
Normal file
BIN
audio/midi.piano1/c8.flac
Normal file
Binary file not shown.
BIN
audio/midi.piano1/c9.flac
Normal file
BIN
audio/midi.piano1/c9.flac
Normal file
Binary file not shown.
111
index.css
Normal file
111
index.css
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortInput {
|
||||||
|
width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexInput {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playButton, #effectsButton, #helpButton {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playButton, #helpButton {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#effectsMenu {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
border: 1px solid black;
|
||||||
|
width: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #6D858D;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border: 1px solid black;
|
||||||
|
overflow: hidden;
|
||||||
|
color: transparent;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.note-C\#, td.note-D\#, td.note-F\#, td.note-G\#, td.note-A\# {
|
||||||
|
background-color: #2B2197;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.note-C, td.note-D, td.note-E, td.note-F, td.note-G, td.note-A, td.note-B {
|
||||||
|
background-color: #3B27EE;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.noteLong {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.active {
|
||||||
|
background-color: #FFFFC0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.playing {
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.playingLong {
|
||||||
|
color: #FFFFC0;
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td:nth-of-type(4n) {
|
||||||
|
border-right: 3px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:nth-of-type(12n) {
|
||||||
|
border-bottom: 3px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaWorkspace {
|
||||||
|
background-color: transparent;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#patternWorkspace {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table#pattern {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MDL hides these by default. */
|
||||||
|
|
||||||
|
input[type=number]::-webkit-outer-spin-button, input[type=number]::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: inner-spin-button !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#helpModal {
|
||||||
|
width: 600px;
|
||||||
|
}
|
146
index.html
Normal file
146
index.html
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<script src="/assets/index.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/md-icons.css">
|
||||||
|
<link rel="stylesheet" href="/assets/material.indigo-pink.min.css">
|
||||||
|
<script defer src="/assets/material.min.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/mdl-selectfield.min.css">
|
||||||
|
<script defer src="/assets/mdl-selectfield.min.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/index.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="mui-form--inline metaWorkspace">
|
||||||
|
<button class="mdl-button mdl-js-button mdl-button--raised mdl-button--primary mdl-js-ripple-effect"
|
||||||
|
id="playButton">
|
||||||
|
Play
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mdl-selectfield mdl-js-selectfield mdl-selectfield--floating-label">
|
||||||
|
<select class="mdl-selectfield__select" id="instruments">
|
||||||
|
<option value="midi.piano1">midi.piano1</option>
|
||||||
|
</select>
|
||||||
|
<label class="mdl-selectfield__label">Instrument</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label shortInput">
|
||||||
|
<input class="mdl-textfield__input" id="bpm" type="number" min="10" max="220" step="5" value="120"
|
||||||
|
required />
|
||||||
|
<label class="mdl-textfield__label">BPM</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label shortInput">
|
||||||
|
<input class="mdl-textfield__input" id="volume" type="number" min="0" max="200" step="5" value="100"
|
||||||
|
required />
|
||||||
|
<label class="mdl-textfield__label">Volume</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label shortInput">
|
||||||
|
<input class="mdl-textfield__input" id="swing" type="number" min="0" max="1" step="0.1" value="0"
|
||||||
|
required />
|
||||||
|
<label class="mdl-textfield__label">Swing</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label shortInput">
|
||||||
|
<input class="mdl-textfield__input" id="resonance" type="number" min="0" max="1" step="0.05" value="0"
|
||||||
|
required />
|
||||||
|
<label class="mdl-textfield__label">Resonance</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label shortInput">
|
||||||
|
<input class="mdl-textfield__input" id="dampening" type="number" min="0" max="3000" step="100"
|
||||||
|
value="3000" required />
|
||||||
|
<label class="mdl-textfield__label">Dampening</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="effectsButton"
|
||||||
|
class="mdl-button mdl-js-button mdl-button--raised mdl-button--primary mdl-js-ripple-effect">
|
||||||
|
Effects <i class="material-icons">arrow_drop_down</i>
|
||||||
|
</button>
|
||||||
|
<ul id="effectsMenu" class="mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effect"
|
||||||
|
for="effectsButton">
|
||||||
|
<li>
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label flexInput">
|
||||||
|
<input class="mdl-textfield__input" id="delayEffect" type="number" min="0" max="1" step="0.05"
|
||||||
|
value="0" required />
|
||||||
|
<label class="mdl-textfield__label">Delay</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label flexInput">
|
||||||
|
<input class="mdl-textfield__input" id="chorusEffect" type="number" min="0" max="1" step="0.05"
|
||||||
|
value="0" required />
|
||||||
|
<label class="mdl-textfield__label">Chorus</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label flexInput">
|
||||||
|
<input class="mdl-textfield__input" id="reverbEffect" type="number" min="0" max="1" step="0.05"
|
||||||
|
value="0" required />
|
||||||
|
<label class="mdl-textfield__label">Reverb</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label flexInput">
|
||||||
|
<input class="mdl-textfield__input" id="distortionEffect" type="number" min="0" max="8" step="1"
|
||||||
|
value="0" required />
|
||||||
|
<label class="mdl-textfield__label">Distortion</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label flexInput">
|
||||||
|
<input class="mdl-textfield__input" id="lowBoostEffect" type="number" min="0" max="3000"
|
||||||
|
step="100" value="0" required />
|
||||||
|
<label class="mdl-textfield__label">Low boost</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label flexInput">
|
||||||
|
<input class="mdl-textfield__input" id="compressorEffect" type="number" min="-100" max="0"
|
||||||
|
step="5" value="0" required />
|
||||||
|
<label class="mdl-textfield__label">Compressor</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label flexInput">
|
||||||
|
<input class="mdl-textfield__input" id="highPassEffect" type="number" min="0" max="3000"
|
||||||
|
step="100" value="0" required />
|
||||||
|
<label class="mdl-textfield__label">High pass</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="mdl-button mdl-js-button mdl-button--raised mdl-button--primary mdl-js-ripple-effect"
|
||||||
|
id="helpButton">
|
||||||
|
Help
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="patternWorkspace">
|
||||||
|
<table id="pattern">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog id="helpModal" class="mdl-dialog">
|
||||||
|
<h4 class="mdl-dialog__title">Help</h4>
|
||||||
|
<div class="mdl-dialog__content">
|
||||||
|
<ul>
|
||||||
|
<li>Click in a box to add or remove a note.</li>
|
||||||
|
<li>Shift click to extend a note, and ctrl-shift click to shorten it.</li>
|
||||||
|
<li>Press the space bar to play or stop the song.</li>
|
||||||
|
<li>After clicking in an entry field, use the arrow keys to quickly change the value.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mdl-dialog__actions">
|
||||||
|
<button type="button" class="mdl-button close">Close</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
5345
package-lock.json
generated
Normal file
5345
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file
43
package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "bosca-coeil-js",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "TypeScript port of SiON",
|
||||||
|
"main": "out/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"assets": "node task-assets.js",
|
||||||
|
"compile": "tsc -p ./",
|
||||||
|
"deploy": "node task-deploy.js",
|
||||||
|
"dev": "concurrently npm:watch npm:webpack-dev npm:serve",
|
||||||
|
"serve": "http-server",
|
||||||
|
"start": "node out/index.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"watch": "tsc -watch -p ./",
|
||||||
|
"webpack": "webpack --mode development",
|
||||||
|
"webpack-dev": "webpack --mode development --watch"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/mtkennerly/bosca-coeil-js.git"
|
||||||
|
},
|
||||||
|
"author": "Matthew T. Kennerly",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/mtkennerly/bosca-coeil-js/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/mtkennerly/bosca-coeil-js#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"tone": "^13.4.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^12.6.3",
|
||||||
|
"@types/tone": "git+https://github.com/Tonejs/TypeScript.git",
|
||||||
|
"concurrently": "^4.1.1",
|
||||||
|
"gh-pages": "^2.0.1",
|
||||||
|
"http-server": "^0.11.1",
|
||||||
|
"ts-loader": "^6.0.4",
|
||||||
|
"tslint": "^5.18.0",
|
||||||
|
"typescript": "^3.5.2",
|
||||||
|
"webpack": "^4.35.2",
|
||||||
|
"webpack-cli": "^3.3.5"
|
||||||
|
}
|
||||||
|
}
|
13
src/index.ts
Normal file
13
src/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import * as tone from 'tone';
|
||||||
|
|
||||||
|
const notes = ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9"];
|
||||||
|
|
||||||
|
export function getSampler(instrument: string, extension: string = "flac", baseUrl: string = "audio/"): tone.Sampler {
|
||||||
|
let samples: { [key: string]: string } = {};
|
||||||
|
for (const note of notes) {
|
||||||
|
samples[note] = `${instrument}/${note.toLowerCase()}.${extension}`;
|
||||||
|
}
|
||||||
|
let sampler = new tone.Sampler(samples, undefined, baseUrl);
|
||||||
|
(sampler as any).curve = "linear";
|
||||||
|
return sampler;
|
||||||
|
}
|
324
src/player.ts
Normal file
324
src/player.ts
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import * as tone from 'tone';
|
||||||
|
import { getSampler } from "./index";
|
||||||
|
|
||||||
|
let playing = false;
|
||||||
|
const letters = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
||||||
|
const chords = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
|
const volume = new tone.Volume(0);
|
||||||
|
const lowPass = new tone.LowpassCombFilter(0, 0);
|
||||||
|
|
||||||
|
const delayEffect = new tone.FeedbackDelay(0, 0);
|
||||||
|
const chorusEffect = new tone.Chorus();
|
||||||
|
chorusEffect.wet.value = 0;
|
||||||
|
const reverbEffect = new tone.Freeverb(0, 3000);
|
||||||
|
reverbEffect.wet.value = 0;
|
||||||
|
const distortionEffect = new tone.BitCrusher(4);
|
||||||
|
distortionEffect.wet.value = 0;
|
||||||
|
const lowBoostEffect = new tone.Filter(0, "lowshelf");
|
||||||
|
const compressorEffect = new tone.Compressor(0);
|
||||||
|
const highPassEffect = new tone.Filter(0, "highpass");
|
||||||
|
|
||||||
|
interface Note {
|
||||||
|
length: number;
|
||||||
|
scheduledEvent: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sampler = getSampler("midi.piano1");
|
||||||
|
let patterns: { [key: number]: { [key: string]: Array<Note> } } = {};
|
||||||
|
for (const chord of chords) {
|
||||||
|
patterns[chord] = {};
|
||||||
|
for (const letter of letters) {
|
||||||
|
patterns[chord][letter] = Array.from({ length: 16 }, () => { return { length: 0, scheduledEvent: null }; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAudio() {
|
||||||
|
if (playing) {
|
||||||
|
tone.Transport.stop();
|
||||||
|
tone.Transport.position = "0";
|
||||||
|
} else {
|
||||||
|
resumeAudioContext();
|
||||||
|
tone.Transport.loopEnd = '1m';
|
||||||
|
tone.Transport.loop = true;
|
||||||
|
}
|
||||||
|
tone.Transport.toggle(0);
|
||||||
|
playing = !playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeAudioContext() {
|
||||||
|
let ac = (tone as any).context;
|
||||||
|
if (ac.state !== "running") {
|
||||||
|
ac.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBpm() {
|
||||||
|
let bpmField = document.getElementById("bpm");
|
||||||
|
if (bpmField !== null && bpmField instanceof HTMLInputElement) {
|
||||||
|
tone.Transport.bpm.value = parseInt(bpmField.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVolume() {
|
||||||
|
const field = document.getElementById("volume");
|
||||||
|
if (field !== null && field instanceof HTMLInputElement) {
|
||||||
|
let newValue = parseInt(field.value);
|
||||||
|
if (newValue === 0) {
|
||||||
|
volume.mute = true;
|
||||||
|
} else {
|
||||||
|
volume.volume.value = (newValue - 100) / 5;
|
||||||
|
volume.mute = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSwing() {
|
||||||
|
const field = document.getElementById("swing");
|
||||||
|
if (field !== null && field instanceof HTMLInputElement) {
|
||||||
|
tone.Transport.swing = parseFloat(field.value);
|
||||||
|
tone.Transport.swingSubdivision = "16n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setResonance() {
|
||||||
|
const field = document.getElementById("resonance");
|
||||||
|
if (field !== null && field instanceof HTMLInputElement) {
|
||||||
|
lowPass.resonance.value = parseFloat(field.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDampening() {
|
||||||
|
const field = document.getElementById("dampening");
|
||||||
|
if (field !== null && field instanceof HTMLInputElement) {
|
||||||
|
lowPass.dampening.value = parseInt(field.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePlayButton() {
|
||||||
|
let playButton = document.getElementById("playButton");
|
||||||
|
if (playButton !== null) {
|
||||||
|
if (playButton.textContent !== null && playButton.textContent.trim().toLowerCase() === "play") {
|
||||||
|
playButton.textContent = "Stop";
|
||||||
|
} else {
|
||||||
|
playButton.textContent = "Play";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEffect(effect: string, value: number) {
|
||||||
|
switch (effect) {
|
||||||
|
case "delay":
|
||||||
|
delayEffect.delayTime.value = value === 0 ? 0 : tone.Time("8n") * 2 * value;
|
||||||
|
delayEffect.feedback.value = 0.15 * 2 * value;
|
||||||
|
break;
|
||||||
|
case "chorus":
|
||||||
|
chorusEffect.wet.value = value;
|
||||||
|
break;
|
||||||
|
case "reverb":
|
||||||
|
reverbEffect.roomSize.value = value * 0.9;
|
||||||
|
reverbEffect.wet.value = value;
|
||||||
|
break;
|
||||||
|
case "distortion":
|
||||||
|
distortionEffect.wet.value = value === 0 ? 0 : 1;
|
||||||
|
distortionEffect.bits = value;
|
||||||
|
break;
|
||||||
|
case "lowBoost":
|
||||||
|
lowBoostEffect.frequency.value = value;
|
||||||
|
lowBoostEffect.gain.value = value === 0 ? 0 : 20;
|
||||||
|
break;
|
||||||
|
case "compressor":
|
||||||
|
compressorEffect.threshold.value = value;
|
||||||
|
break;
|
||||||
|
case "highPass":
|
||||||
|
highPassEffect.frequency.value = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNote(chord: number, letter: string, index: number, length: number) {
|
||||||
|
unscheduleNote(chord, letter, index);
|
||||||
|
console.log(`scheduleNote(chord=${chord}, letter=${letter}, index=${index}, length=${length})`);
|
||||||
|
patterns[chord][letter][index]["length"] = length;
|
||||||
|
|
||||||
|
patterns[chord][letter][index]["scheduledEvent"] = tone.Transport.schedule(
|
||||||
|
time => {
|
||||||
|
sampler.triggerAttackRelease(`${letter}${chord}`, tone.Time("16n") * length, time);
|
||||||
|
tone.Draw.schedule(() => {
|
||||||
|
const noteElement = document.querySelector(`#${letter.replace("#", "\\#")}-${chord}-${index}`);
|
||||||
|
if (noteElement !== null) {
|
||||||
|
if (length <= 1) {
|
||||||
|
noteElement.classList.add("playing");
|
||||||
|
} else {
|
||||||
|
noteElement.classList.add("playingLong");
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
noteElement.classList.remove("playing");
|
||||||
|
noteElement.classList.remove("playingLong");
|
||||||
|
}, 100 * length);
|
||||||
|
}
|
||||||
|
}, time);
|
||||||
|
},
|
||||||
|
`0:0:${index}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unscheduleNote(chord: number, letter: string, index: number) {
|
||||||
|
patterns[chord][letter][index]["length"] = 0;
|
||||||
|
const schedulee = patterns[chord][letter][index]["scheduledEvent"];
|
||||||
|
if (schedulee !== null) {
|
||||||
|
tone.Transport.clear(schedulee);
|
||||||
|
patterns[chord][letter][index]["scheduledEvent"] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickNoteCell(event: MouseEvent, cell: HTMLTableCellElement, chord: number, letter: string, index: number) {
|
||||||
|
let length = patterns[chord][letter][index]["length"];
|
||||||
|
console.log(`onClickNoteCell(chord=${chord}, letter=${letter}, index=${index}) | length ${length}`);
|
||||||
|
|
||||||
|
if (event.shiftKey && event.ctrlKey) {
|
||||||
|
length = Math.max(length - 1, 0);
|
||||||
|
} else if (event.shiftKey) {
|
||||||
|
length = Math.max(Math.min(length + 1, 16), 2);
|
||||||
|
} else if (length > 0) {
|
||||||
|
length = 0;
|
||||||
|
} else {
|
||||||
|
length = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.innerHTML = length.toString();
|
||||||
|
|
||||||
|
if (length <= 1) {
|
||||||
|
cell.classList.remove("noteLong");
|
||||||
|
} else {
|
||||||
|
cell.classList.add("noteLong");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length > 0 && length !== patterns[chord][letter][index]["length"]) {
|
||||||
|
cell.classList.add("active");
|
||||||
|
scheduleNote(chord, letter, index, length);
|
||||||
|
} else if (length === 0) {
|
||||||
|
cell.classList.remove("active");
|
||||||
|
unscheduleNote(chord, letter, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLoad() {
|
||||||
|
sampler.chain(
|
||||||
|
volume,
|
||||||
|
lowPass,
|
||||||
|
delayEffect,
|
||||||
|
chorusEffect,
|
||||||
|
reverbEffect,
|
||||||
|
distortionEffect,
|
||||||
|
lowBoostEffect,
|
||||||
|
compressorEffect,
|
||||||
|
highPassEffect,
|
||||||
|
tone.Master
|
||||||
|
);
|
||||||
|
|
||||||
|
document.onkeypress = event => {
|
||||||
|
if (event.keyCode === 32) {
|
||||||
|
toggleAudio();
|
||||||
|
togglePlayButton();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let playButton = document.getElementById("playButton");
|
||||||
|
if (playButton !== null) {
|
||||||
|
playButton.addEventListener("click", toggleAudio);
|
||||||
|
playButton.addEventListener("click", togglePlayButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
let helpButton = document.getElementById("helpButton");
|
||||||
|
let helpModal = document.getElementById("helpModal");
|
||||||
|
if (helpButton !== null && helpModal !== null) {
|
||||||
|
helpButton.addEventListener("click", () => { (helpModal as any).showModal(); });
|
||||||
|
const helpModalClose = helpModal.querySelector(".close");
|
||||||
|
if (helpModalClose !== null) {
|
||||||
|
helpModalClose.addEventListener("click", () => {
|
||||||
|
(helpModal as any).close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bpmField = document.getElementById("bpm");
|
||||||
|
if (bpmField !== null) {
|
||||||
|
bpmField.addEventListener("change", setBpm);
|
||||||
|
}
|
||||||
|
|
||||||
|
let effectsMenu = document.getElementById("effectsMenu");
|
||||||
|
if (effectsMenu !== null) {
|
||||||
|
effectsMenu.addEventListener("click", event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const effects = ["delay", "chorus", "reverb", "distortion", "lowBoost", "compressor", "highPass"];
|
||||||
|
for (const effect of effects) {
|
||||||
|
const effectField = document.getElementById(`${effect}Effect`);
|
||||||
|
if (effectField !== null && effectField instanceof HTMLInputElement) {
|
||||||
|
effectField.addEventListener("change", () => {
|
||||||
|
setEffect(effect, parseFloat(effectField.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let patternTable = document.getElementById("pattern");
|
||||||
|
if (patternTable !== null && patternTable instanceof HTMLTableElement) {
|
||||||
|
for (const chord of chords.slice().reverse()) {
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
|
||||||
|
for (const letter of letters.slice().reverse()) {
|
||||||
|
let row = patternTable.insertRow();
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
let chordHeader = document.createElement("th");
|
||||||
|
chordHeader.rowSpan = letters.length;
|
||||||
|
chordHeader.innerHTML = chord.toString();
|
||||||
|
row.appendChild(chordHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
let letterHeader = document.createElement("th");
|
||||||
|
letterHeader.innerHTML = letter;
|
||||||
|
row.appendChild(letterHeader);
|
||||||
|
|
||||||
|
for (const i of Array(16).keys()) {
|
||||||
|
let cell = row.insertCell();
|
||||||
|
cell.id = `${letter}-${chord}-${i}`;
|
||||||
|
cell.classList.add(`note-${letter}`);
|
||||||
|
cell.onclick = event => { onClickNoteCell(event, cell, chord, letter, i); };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const centralRow = document.getElementById("F-5-0");
|
||||||
|
if (centralRow !== null) {
|
||||||
|
centralRow.scrollIntoView({ "behavior": "smooth", "block": "center" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let volumeField = document.getElementById("volume");
|
||||||
|
if (volumeField !== null) {
|
||||||
|
volumeField.addEventListener("change", setVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
let swingField = document.getElementById("swing");
|
||||||
|
if (swingField !== null) {
|
||||||
|
swingField.addEventListener("change", setSwing);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resonanceField = document.getElementById("resonance");
|
||||||
|
if (resonanceField !== null) {
|
||||||
|
resonanceField.addEventListener("change", setResonance);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dampeningField = document.getElementById("dampening");
|
||||||
|
if (dampeningField !== null) {
|
||||||
|
dampeningField.addEventListener("change", setDampening);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = onLoad;
|
36
task-assets.js
Normal file
36
task-assets.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
var http = require("http");
|
||||||
|
var https = require("https");
|
||||||
|
var fs = require("fs");
|
||||||
|
var assets = './assets';
|
||||||
|
|
||||||
|
function download(filename, url) {
|
||||||
|
var file = fs.createWriteStream(filename);
|
||||||
|
var protocol = url.startsWith("https://") ? https : http;
|
||||||
|
protocol.get(url, response => {
|
||||||
|
response.pipe(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(assets)) {
|
||||||
|
fs.mkdirSync(assets);
|
||||||
|
}
|
||||||
|
download(
|
||||||
|
`${assets}/md-icons.css`,
|
||||||
|
"https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||||
|
);
|
||||||
|
download(
|
||||||
|
`${assets}/material.indigo-pink.min.css`,
|
||||||
|
"https://code.getmdl.io/1.3.0/material.indigo-pink.min.css"
|
||||||
|
);
|
||||||
|
download(
|
||||||
|
`${assets}/material.min.js`,
|
||||||
|
"https://code.getmdl.io/1.3.0/material.min.js"
|
||||||
|
);
|
||||||
|
download(
|
||||||
|
`${assets}/mdl-selectfield.min.css`,
|
||||||
|
"https://cdn.rawgit.com/kybarg/mdl-selectfield/mdl-menu-implementation/mdl-selectfield.min.css"
|
||||||
|
);
|
||||||
|
download(
|
||||||
|
`${assets}/mdl-selectfield.min.js`,
|
||||||
|
"https://cdn.rawgit.com/kybarg/mdl-selectfield/mdl-menu-implementation/mdl-selectfield.min.js"
|
||||||
|
);
|
16
task-deploy.js
Normal file
16
task-deploy.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
var ghPages = require("gh-pages");
|
||||||
|
|
||||||
|
ghPages.publish(
|
||||||
|
".",
|
||||||
|
{
|
||||||
|
"src": [
|
||||||
|
"index.html",
|
||||||
|
"assets/*.css",
|
||||||
|
"assets/*.js",
|
||||||
|
"audio/**/*.flac",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
console.log(`Failure: ${err}`);
|
||||||
|
}
|
||||||
|
);
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"outDir": "out",
|
||||||
|
"lib": [
|
||||||
|
"es6",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"sourceMap": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
".vscode-test"
|
||||||
|
]
|
||||||
|
}
|
15
tslint.json
Normal file
15
tslint.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"no-string-throw": true,
|
||||||
|
"no-unused-expression": true,
|
||||||
|
"no-duplicate-variable": true,
|
||||||
|
"curly": true,
|
||||||
|
"class-name": true,
|
||||||
|
"semicolon": [
|
||||||
|
true,
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"triple-equals": true
|
||||||
|
},
|
||||||
|
"defaultSeverity": "warning"
|
||||||
|
}
|
24
webpack.config.js
Normal file
24
webpack.config.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
path = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
index: './src/player.ts'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js']
|
||||||
|
},
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
output: {
|
||||||
|
filename: '[name].js',
|
||||||
|
path: path.resolve(__dirname, 'assets')
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user