forked from BRNSystems/bosca-ceoil-js
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