Initial commit

This commit is contained in:
mtkennerly 2019-07-16 03:31:45 -04:00
commit 178455555b
24 changed files with 6168 additions and 0 deletions

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

@ -0,0 +1,5 @@
node_modules/
out/
assets/
audio/**/*_data/
*.aup

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

@ -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

Binary file not shown.

BIN
audio/midi.piano1/c2.flac Normal file

Binary file not shown.

BIN
audio/midi.piano1/c3.flac Normal file

Binary file not shown.

BIN
audio/midi.piano1/c4.flac Normal file

Binary file not shown.

BIN
audio/midi.piano1/c5.flac Normal file

Binary file not shown.

BIN
audio/midi.piano1/c6.flac Normal file

Binary file not shown.

BIN
audio/midi.piano1/c7.flac Normal file

Binary file not shown.

BIN
audio/midi.piano1/c8.flac Normal file

Binary file not shown.

BIN
audio/midi.piano1/c9.flac Normal file

Binary file not shown.

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

@ -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

File diff suppressed because it is too large Load Diff

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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')
}
};