├── .gitignore ├── LICENSE ├── README.md ├── dist ├── blackswan.js ├── example.js └── source │ ├── base.d.ts │ ├── brickwall-limiter.lib.d.ts │ ├── improviser.d.ts │ ├── notes.d.ts │ ├── piano-data.d.ts │ ├── player.d.ts │ ├── scheduler.d.ts │ ├── song.d.ts │ ├── style.d.ts │ ├── synth.d.ts │ └── validate.d.ts ├── examples ├── call-back-recursively.js ├── example-player.html ├── example-player.js ├── improvise-default.js ├── improvise.js ├── metronome.js ├── play-a-sequence.js ├── play-chords.js ├── play-dynamics.js ├── play-glissandos.js ├── play-la-cucaracha.js ├── play-legato-staccato.js ├── play-one-note.js ├── play-stop-restart.js ├── play-strange-notes.js ├── play-then-stop.js ├── repeat.js └── webpack.config.js ├── package-lock.json ├── package.json ├── source ├── base.ts ├── brickwall-limiter.lib.ts ├── improviser.ts ├── notes.ts ├── piano-data.ts ├── player.ts ├── scheduler.ts ├── song.ts ├── style.ts ├── synth.ts ├── validate.ts └── webpack.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Isaac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blackswan.js 2 | ## A library for expressive music composition in JavaScript 3 | 4 | ## About 5 | blackswan.js is named after "Black Swan Song" by the British band Athlete. Its intended use is for writing songs. Any song written in blackswan.js can easily be played back in compatible browsers (up-to-date Chrome is a sure bet). It has a simple piano synth built in and is easy to use if you have basic JavaScript skills. 6 | 7 | ## Principles 8 | - Writing a song in blackswan.js is more like writing sheet music than sound programming. 9 | - A decent almost-piano is baked in. 10 | - Structured improvisation is a first-class feature. 11 | - For power users, the synth and improviser are user-swappable with minimal headache. 12 | 13 | ## Installation 14 | 15 | Via NPM: 16 | 17 | `npm install --save blackswan-js` 18 | 19 | `import blackswan from 'blackswan-js'` 20 | 21 | Alternately, you can add a ` 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/example-player.js: -------------------------------------------------------------------------------- 1 | import { mysong as OneNote } from './play-one-note'; 2 | import { mysong as Sequence } from './play-a-sequence'; 3 | import { mysong as Chords } from './play-chords'; 4 | import { mysong as Repeat } from './repeat'; 5 | import { mysong as Improvise } from './improvise'; 6 | import { mysong as Recurse } from './call-back-recursively'; 7 | import { mysong as LegatoStaccato } from './play-legato-staccato'; 8 | import { mysong as Glissandos } from './play-glissandos'; 9 | import { mysong as Dynamics } from './play-dynamics'; 10 | import { mysong as StrangeNotes } from './play-strange-notes'; 11 | import { mysong as PlayThenStop } from './play-then-stop'; 12 | import { mysong as PlayStopRestart } from './play-stop-restart'; 13 | import { mysong as Metronome } from './metronome'; 14 | import { mysong as ImproviseDefault } from './improvise-default'; 15 | 16 | import { mysong as LaCucaracha } from './play-la-cucaracha'; 17 | 18 | PlayStopRestart.play(); 19 | -------------------------------------------------------------------------------- /examples/improvise-default.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('improvise with default settings'); 5 | 6 | mysong.at(0).improvises(blackswan.scale([ 7 | 'a4', 'b4', 'c5', 'e5' 8 | ]), 4); 9 | 10 | export { mysong } 11 | -------------------------------------------------------------------------------- /examples/improvise.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('improvise'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | let scale = blackswan.scale([ 9 | 'c4', 'e4', 'f4', 'g4', 'c5', 10 | ['e4', 'g4'] 11 | ], { 12 | durations: [1/4, 1/4, 1/2], 13 | style: [] 14 | }); 15 | 16 | mysong.at(0).improvises(scale, 4); 17 | 18 | export { mysong } 19 | -------------------------------------------------------------------------------- /examples/metronome.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('metronome'); 5 | mysong.setTempo(120); 6 | mysong.setTimeSignature(4, 4); 7 | 8 | mysong.at(0).repeats(blackswan.note('a3', 1/4), { every: 1/2, times: 20 }); 9 | mysong.at(0.25).repeats(blackswan.note('a4', 1/4), { every: 1/2, times: 20 }); 10 | 11 | export { mysong } 12 | -------------------------------------------------------------------------------- /examples/play-a-sequence.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('sequence'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | let sequence = blackswan.sequence([ 9 | blackswan.note('c4', 1/4), 10 | blackswan.note('d4', 1/4), 11 | blackswan.note('e4', 1/4), 12 | blackswan.rest(1/4), 13 | blackswan.note('f4', 1/4) 14 | ]); 15 | 16 | // Play c4 for two seconds (four measures) 17 | mysong.at(0).plays(sequence); 18 | 19 | export { mysong } 20 | -------------------------------------------------------------------------------- /examples/play-chords.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('chords'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | let sequence = blackswan.sequence([ 9 | blackswan.chord(['c4', 'e4', 'g4'], 1/4), 10 | blackswan.chord(['d4', 'f4', 'a4'], 1/4), 11 | blackswan.chord(['e4', 'g4', 'b4'], 1/4), 12 | blackswan.rest(1/4), 13 | blackswan.chord(['f4', 'a4', 'c5'], 1/4) 14 | ]); 15 | 16 | // Play c4 for two seconds (four measures) 17 | mysong.at(0).plays(sequence); 18 | 19 | export { mysong } 20 | -------------------------------------------------------------------------------- /examples/play-dynamics.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('dynamics'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | mysong.at(0).plays(blackswan.sequence([ 9 | blackswan.note('a4', 1/4, blackswan.as.Pianissimo), 10 | blackswan.note('a4', 1/4, blackswan.as.Piano), 11 | blackswan.note('a4', 1/4, blackswan.as.MezzoPiano), 12 | blackswan.note('a4', 1/4, blackswan.as.MezzoForte), 13 | blackswan.note('a4', 1/4, blackswan.as.Forte), 14 | blackswan.note('a4', 1/4, blackswan.as.Fortissimo), 15 | ])); 16 | 17 | export { mysong } 18 | -------------------------------------------------------------------------------- /examples/play-glissandos.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('glissandos'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | function createNotes(octave) { 9 | let noteNames = ['c', 'd', 'e', 'f', 'g', 'a', 'b']; 10 | return noteNames.map((name) => name + octave); 11 | } 12 | 13 | let lowerNotes = createNotes(1).concat(createNotes(2)).concat(createNotes(3)); 14 | let middleNotes = createNotes(4).concat(createNotes(5)); 15 | let highNotes = createNotes(6).concat(createNotes(7)); 16 | 17 | function createGliss(notesArr) { 18 | let glissArr = []; 19 | 20 | for (let note of notesArr) { 21 | glissArr.push(blackswan.note(note, 3/80)); 22 | } 23 | 24 | return blackswan.sequence(glissArr); 25 | } 26 | 27 | mysong.at(0).plays(createGliss(lowerNotes)); 28 | mysong.at(1).plays(createGliss(middleNotes)); 29 | mysong.at(2).plays(createGliss(highNotes)); 30 | 31 | export { mysong } 32 | -------------------------------------------------------------------------------- /examples/play-la-cucaracha.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('La Cucaracha'); 5 | 6 | // Kick up the tempo, arriba! 7 | mysong.setTempo(200); 8 | 9 | let la = blackswan.note('c4', 1/8); 10 | let cu = la; 11 | let ca = cu; 12 | let ra = blackswan.note('f4', 3/8); 13 | let cha = blackswan.note('a4', 1/4); 14 | 15 | let climb = blackswan.note('f4', 1/4); 16 | let ing = blackswan.note('f4', 1/8); 17 | let up = blackswan.note('e4', 1/8); 18 | let and = up; 19 | let down = blackswan.note('d4', 1/8); 20 | let the = down; 21 | let wall = blackswan.note('c4', 3/8); 22 | 23 | let sequence = blackswan.sequence([ 24 | la, cu, ca, ra, cha, la, cu, ca, ra, cha, 25 | blackswan.rest(3/8), 26 | climb, ing, up, and, down, the, wall 27 | ]); 28 | 29 | mysong.at(0).plays(sequence); 30 | 31 | export { mysong } 32 | -------------------------------------------------------------------------------- /examples/play-legato-staccato.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('legato and staccato'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | // Play a few staccato notes 9 | let staccatoSeq = blackswan.sequence([ 10 | blackswan.note('c4', 1/4, blackswan.as.Staccato), 11 | blackswan.note('e4', 1/4, blackswan.as.Staccato), 12 | blackswan.note('c4', 1/4, blackswan.as.Staccato) 13 | ]); 14 | 15 | // Play a couple of legato chords 16 | let legatoSeq = blackswan.sequence([ 17 | blackswan.chord(['c4', 'f4', 'g4'], 1/4, blackswan.as.Legato), 18 | blackswan.chord(['c4', 'e4', 'g4'], 1/4, blackswan.as.Legato) 19 | ]); 20 | 21 | mysong.at(0).plays(staccatoSeq); 22 | mysong.at(0.75).plays(legatoSeq); 23 | 24 | export { mysong } 25 | -------------------------------------------------------------------------------- /examples/play-one-note.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('one note'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | // Play c4 as a whole note 9 | mysong.at(0).plays(blackswan.note('c4', 1)); 10 | 11 | export { mysong } 12 | -------------------------------------------------------------------------------- /examples/play-stop-restart.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('play then stop'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | // Play c4 for a while 9 | mysong.at(0).plays(blackswan.note('c4', 4)); 10 | setTimeout(() => { 11 | mysong.stop() 12 | }, 1000) 13 | setTimeout(() => { 14 | mysong.play() 15 | }, 2000) 16 | 17 | export { mysong } 18 | -------------------------------------------------------------------------------- /examples/play-strange-notes.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('strange notes'); 5 | 6 | mysong.at(0).plays(blackswan.sequence([ 7 | blackswan.note('c', 1/4), 8 | blackswan.note('c#', 1/4), 9 | blackswan.note('c@4', 1/4), 10 | blackswan.note('cb4', 1/4), 11 | blackswan.note('A5', 1/4), 12 | blackswan.note('b#', 1/4) 13 | ])); 14 | 15 | export { mysong } 16 | -------------------------------------------------------------------------------- /examples/play-then-stop.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('play then stop'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | // Play c4 for a while 9 | mysong.at(0).plays(blackswan.note('c4', 16)); 10 | mysong.at(1).callback(() => { 11 | mysong.stop() 12 | }) 13 | 14 | export { mysong } 15 | -------------------------------------------------------------------------------- /examples/repeat.js: -------------------------------------------------------------------------------- 1 | import { Song } from '../source/song'; 2 | import { blackswan } from '../source/base'; 3 | 4 | let mysong = blackswan.song('repeats'); 5 | 6 | // Default tempo and time signature will be fine. 7 | 8 | let note = blackswan.note('c4', 1/4); 9 | let chord = blackswan.chord(['e4', 'g4'], 1/4); 10 | 11 | // Repeat c4 four times, then repeat the chord twice 12 | mysong.at(0).repeats(note, { every: 1/4, times: 4 }); 13 | mysong.at(0.25).repeats(chord, { every: 1/4, times: 2 }); 14 | 15 | export { mysong } 16 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "development", 5 | entry: { 6 | example: path.resolve(__dirname, "./example-player.js") 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, "../dist"), 10 | filename: "[name].js" 11 | }, 12 | resolve: { 13 | extensions: [".ts", ".tsx", ".js"] 14 | }, 15 | module: { 16 | rules: [ 17 | // all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader' 18 | { test: /\.tsx?$/, loader: "ts-loader" } 19 | ] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blackswan-js", 3 | "version": "0.0.12", 4 | "description": "A library for expressive music composition with JavaScript", 5 | "main": "dist/blackswan.js", 6 | "types": "dist/source/base.d.ts", 7 | "directories": { 8 | "example": "examples" 9 | }, 10 | "dependencies": { 11 | "web-audio-scheduler": "1.4.0" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^7.0.31", 15 | "require": "^2.4.20", 16 | "ts-loader": "^4.2.0", 17 | "typescript": "^2.7.2", 18 | "webpack": "^4.5.0", 19 | "webpack-cli": "^2.0.14" 20 | }, 21 | "scripts": { 22 | "build": "webpack --config ./source/webpack.config.js", 23 | "build:examples": "webpack --config ./examples/webpack.config.js", 24 | "watch": "webpack --config ./webpack.config.js --watch" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/isaaclyman/blackswan-js.git" 29 | }, 30 | "author": "Isaac Lyman", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/isaaclyman/blackswan-js/issues" 34 | }, 35 | "homepage": "https://github.com/isaaclyman/blackswan-js#readme" 36 | } 37 | -------------------------------------------------------------------------------- /source/base.ts: -------------------------------------------------------------------------------- 1 | // This file contains the external interface for blackswan.js 2 | import { Style } from './style'; 3 | import { Scale, ImproviseConfig, Improviser } from './improviser'; 4 | import { Notes } from './notes'; 5 | import { Player } from './player'; 6 | import { Actions, Rest, Scheduler, Sequence, TimedChord, TimedNote } from './scheduler'; 7 | import { DefaultSongData, Song, TimeSignature } from './song'; 8 | import { Note, Synth } from './synth'; 9 | 10 | export interface Settings { 11 | setGain: (gain: (frequency: number, style: Style[], masterGain: GainNode) => GainNode) => void; 12 | setImproviser: (improviser: (scale: Scale, duration: number) => Sequence) => void; 13 | setOscillator: (oscillator: (frequency: number, style: Style[], gainNode: GainNode) => OscillatorNode) => void; 14 | setPlayer: (player: (note: Note, startSeconds: number, stopSeconds: number) => void) => void; 15 | } 16 | 17 | let Base = (function (window) { 18 | /* song initializer and instance members */ 19 | 20 | function createSong(title: string): Song { 21 | let metadata = DefaultSongData(); 22 | metadata.Title = title; 23 | 24 | let song: Song = { 25 | at: getActions, 26 | play: Player.play, 27 | stop: Player.stop, 28 | setTimeSignature: setTimeSignature, 29 | setTempo: setTempo, 30 | _master: [], 31 | _metadata: metadata, 32 | }; 33 | 34 | return song; 35 | } 36 | 37 | function getActions(this: Song, measure: number): Actions { 38 | return Scheduler.GetActions(this, measure); 39 | } 40 | 41 | function setTimeSignature(this: Song, numerator: number, denominator: number): void { 42 | let timeSignature: TimeSignature = { 43 | beatsPerMeasure: numerator, 44 | noteValue: denominator 45 | }; 46 | 47 | this._metadata.TimeSignature = timeSignature; 48 | } 49 | 50 | function setTempo(this: Song, tempo: number): void { 51 | this._metadata.Tempo = tempo; 52 | } 53 | 54 | /* blackswan static functions */ 55 | 56 | function chord(notes: string[], duration: number, ...config: Style[]): TimedChord { 57 | return { 58 | Notes: notes.map((n) => Synth.SynthesizeNote(Notes.getFrequency(n), config)), 59 | Duration: duration, 60 | } as TimedChord; 61 | } 62 | 63 | function note(noteName: string, duration: number, ...config: Style[]): TimedNote { 64 | let note = Synth.SynthesizeNote(Notes.getFrequency(noteName), config); 65 | 66 | return { 67 | Duration: duration, 68 | Note: note 69 | } as TimedNote; 70 | } 71 | 72 | function rest(duration: number): Rest { 73 | return { 74 | Duration: duration 75 | } as Rest; 76 | } 77 | 78 | function scale(playables: Array>, config?: ImproviseConfig) { 79 | return { 80 | Config: config || { 81 | durations: [1/4], 82 | style: [] 83 | }, 84 | Playables: playables 85 | } as Scale; 86 | } 87 | 88 | function sequence(sequence: Sequence): Sequence { 89 | return sequence; 90 | } 91 | 92 | let settings: Settings = { 93 | setGain: Synth.SetGain, 94 | setImproviser: Improviser.setImproviser, 95 | setOscillator: Synth.SetOscillator, 96 | setPlayer: Synth.SetPlayer 97 | }; 98 | 99 | let Base = { 100 | as: Style, 101 | chord, 102 | note, 103 | rest, 104 | scale, 105 | sequence, 106 | settings, 107 | song: createSong, 108 | }; 109 | 110 | // Create the blackswan global, for people choosing to use it that way 111 | (window).blackswan = Base; 112 | 113 | return Base; 114 | })(window); 115 | 116 | export { Base as blackswan }; 117 | export default Base; 118 | -------------------------------------------------------------------------------- /source/brickwall-limiter.lib.ts: -------------------------------------------------------------------------------- 1 | var sampleRate = 44100; // Hz 2 | var preGain = 0; //db 3 | var postGain = 0; //db 4 | var attackTime = 0; //s 5 | var releaseTime = 0.05; //s 6 | var threshold = -2; //dB 7 | var lookAheadTime = 0.005; //s 5ms hard-coded 8 | var delayBuffer = new DelayBuffer(lookAheadTime * sampleRate); 9 | 10 | function DelayBuffer(this: any, n: number) { 11 | n = Math.floor(n); 12 | this._array = new Float32Array(2 * n); 13 | this.length = this._array.length; // can be optimized! 14 | this.readPointer = 0; 15 | this.writePointer = n - 1; 16 | for (var i=0; i 0){ 97 | 98 | //write signal into buffer and read delayed signal 99 | for (var i = 0; i < out.length; i++){ 100 | 101 | delayBuffer.push(out[i]); 102 | out[i] = delayBuffer.read(); 103 | 104 | } 105 | } 106 | 107 | //limiter mode: slope is 1 108 | var slope = 1; 109 | 110 | for (var i=0; i>; 13 | } 14 | 15 | function getRandomElement(elements: Array): T { 16 | // maximum is exclusive here, so highest number returned will be 17 | // (notes.length - 1) 18 | let max = elements.length; 19 | let index = Math.floor(Math.random() * max); 20 | 21 | return elements[index]; 22 | } 23 | 24 | let _improviser = function getImprovisedSequence(scale: Scale, duration: number): Sequence { 25 | let cursor = 0; 26 | let nextDuration: number = getRandomElement(scale.Config.durations); 27 | let sequence: Sequence = []; 28 | 29 | while (cursor + nextDuration < duration) { 30 | let nextPlay = getRandomElement(scale.Playables); 31 | 32 | if (Array.isArray(nextPlay)) { 33 | let nextChord: TimedChord = blackswan.chord(nextPlay, nextDuration, ...scale.Config.style); 34 | sequence.push(nextChord); 35 | } else { 36 | let nextNote: TimedNote = blackswan.note(nextPlay, nextDuration, ...scale.Config.style); 37 | sequence.push(nextNote); 38 | } 39 | 40 | cursor += nextDuration; 41 | nextDuration = getRandomElement(scale.Config.durations); 42 | } 43 | 44 | return sequence; 45 | } 46 | 47 | function improvise(scale: Scale, duration: number): Sequence { 48 | return _improviser(scale, duration); 49 | } 50 | 51 | function setImproviser(improviser: (scale: Scale, duration: number) => Sequence): void { 52 | _improviser = improviser; 53 | } 54 | 55 | let Improviser = { 56 | improvise, 57 | setImproviser 58 | }; 59 | 60 | export { Improviser }; 61 | -------------------------------------------------------------------------------- /source/notes.ts: -------------------------------------------------------------------------------- 1 | import { PianoData } from './piano-data'; 2 | import { Validate } from './validate'; 3 | 4 | function createNote(noteName: string, frequency: number, overrideExisting: boolean = false): void { 5 | if (!overrideExisting && !!PianoData.NoteMap[noteName]) { 6 | throw Error(` 7 | The note "${noteName}" already exists and cannot be overwritten unless 8 | the 'overrideExisting' parameter is set to 'true'.` 9 | ); 10 | } 11 | 12 | PianoData.NoteMap[noteName] = frequency; 13 | return; 14 | } 15 | 16 | let _octave = 4; 17 | function setOctave(newOctave: number|string): void { 18 | Validate.Octave(newOctave); 19 | _octave = Number(newOctave); 20 | } 21 | 22 | function getNoteIndex(note: string): number { 23 | Validate.Note(note); 24 | let index = PianoData.Notes.indexOf(note); 25 | return index; 26 | } 27 | 28 | function getNextNote(note: string): string { 29 | let currentNote = getNoteIndex(note); 30 | return currentNote + 1 < PianoData.Notes.length 31 | ? PianoData.Notes[currentNote + 1] 32 | : PianoData.Notes[0]; 33 | } 34 | 35 | function getPrevNote(note: string): string { 36 | let currentNote = getNoteIndex(note); 37 | return currentNote > 0 38 | ? PianoData.Notes[currentNote - 1] 39 | : PianoData.Notes[PianoData.Notes.length - 1]; 40 | } 41 | 42 | function getFrequency(noteName: string): number { 43 | let byKey = PianoData.NoteMap[noteName]; 44 | if (byKey) { 45 | return byKey; 46 | } 47 | 48 | let key: string, 49 | note: string, 50 | sign: string = '', 51 | octave: number; 52 | 53 | note = noteName[0].toLowerCase(); 54 | if (noteName.length === 3) { 55 | sign = noteName[1]; 56 | Validate.Sign(sign); 57 | octave = Number(noteName[2]); 58 | Validate.Octave(octave); 59 | } else if (noteName.length === 2) { 60 | if (!!~PianoData.FlatSigns.indexOf(noteName[1]) || !!~PianoData.SharpSigns.indexOf(noteName[1])) { 61 | sign = noteName[1]; 62 | octave = _octave; 63 | } else { 64 | octave = Number(noteName[1]); 65 | } 66 | } else if (noteName.length === 1) { 67 | octave = _octave; 68 | } else { 69 | throw Error(`Unrecognized note format "${noteName}".`); 70 | } 71 | 72 | if (!!~'abdeg'.indexOf(note) && !!~PianoData.FlatSigns.indexOf(sign)) { 73 | // If it's ABDEG-flat, transform into a sharp 74 | // This will never change octaves, because C is not in this group 75 | key = getPrevNote(note) + 76 | '#' + 77 | octave; 78 | return PianoData.NoteMap[key]; 79 | } else if (!!~'cf'.indexOf(note) && !!~PianoData.FlatSigns.indexOf(sign)) { 80 | // If it's CF-flat, transform into the next note down 81 | // This changes octaves if the note is a C 82 | key = getPrevNote(note) + 83 | (note === 'c' ? 84 | (octave - 1).toString() : 85 | octave.toString()); 86 | 87 | return PianoData.NoteMap[key]; 88 | } else if (!!~'be'.indexOf(note) && !!~PianoData.SharpSigns.indexOf(sign)) { 89 | // If it's BE-sharp, transform it into the next note up 90 | // This changes octaves if the note is a B 91 | key = getNextNote(note) + 92 | (note === 'b' ? 93 | (octave + 1).toString() : 94 | octave.toString()); 95 | return PianoData.NoteMap[key]; 96 | } else { 97 | key = note + (sign || '') + (octave || ''); 98 | let byConstructedKey = PianoData.NoteMap[key]; 99 | 100 | if (byConstructedKey) { 101 | return byConstructedKey; 102 | } 103 | } 104 | 105 | throw Error(`The note "${noteName}" is unknown.`); 106 | } 107 | 108 | let Notes = { 109 | createNote, 110 | getFrequency, 111 | setOctave 112 | }; 113 | 114 | export { Notes }; 115 | -------------------------------------------------------------------------------- /source/piano-data.ts: -------------------------------------------------------------------------------- 1 | // Only sharps are noted here; flats can be converted on-the-fly 2 | let _noteMap: { [name: string]: number } = { 3 | 'a0': 27.5, 'a#0': 29.1353, 'b0': 30.8677, 'c1': 32.7032, 'c#1': 34.6478, 4 | 'd1': 36.7081, 'd#1': 38.8909, 'e1': 41.2034, 'f1': 43.6535, 'f#1': 46.2493, 5 | 'g1': 48.9994, 'g#1': 51.9131, 'a1': 55.0, 'a#1': 58.2705, 'b1': 61.7354, 6 | 'c2': 65.4064, 'c#2': 69.2957, 'd2': 73.4162, 'd#2': 77.7817, 'e2': 82.4069, 7 | 'f2': 87.3071, 'f#2': 92.4986, 'g2': 97.9989, 'g#2': 103.826, 'a2': 110.0, 8 | 'a#2': 116.541, 'b2': 123.471, 'c3': 130.813, 'c#3': 138.591, 'd3': 146.832, 9 | 'd#3': 155.563, 'e3': 164.814, 'f3': 174.614, 'f#3': 184.997, 'g3': 195.998, 10 | 'g#3': 207.562, 'a3': 220.0, 'a#3': 233.082, 'b3': 246.942, 'c4': 261.626, 11 | 'c#4': 277.183, 'd4': 293.665, 'd#4': 311.127, 'e4': 329.628, 'f4': 349.228, 12 | 'f#4': 369.994, 'g4': 391.995, 'g#4': 415.305, 'a4': 440.0, 'a#4': 466.164, 13 | 'b4': 493.883, 'c5': 523.251, 'c#5': 554.365, 'd5': 587.330, 'd#5': 622.254, 14 | 'e5': 659.255, 'f5': 698.456, 'f#5': 739.989, 'g5': 783.991, 'g#5': 830.609, 15 | 'a5': 880.0, 'a#5': 932.328, 'b5': 987.767, 'c6': 1046.5, 'c#6': 1108.73, 16 | 'd6': 1174.66, 'd#6': 1244.51, 'e6': 1318.51, 'f6': 1396.91, 'f#6': 1479.98, 17 | 'g6': 1567.98, 'g#6': 1661.22, 'a6': 1760.0, 'a#6': 1864.66, 'b6': 1975.53, 18 | 'c7': 2093.00, 'c#7': 2217.46, 'd7': 2349.32, 'd#7': 2489.02, 'e7': 2637.02, 19 | 'f7': 2793.83, 'f#7': 2959.96, 'g7': 3135.96, 'g#7': 3322.44, 'a7': 3520.0, 20 | 'a#7': 3729.31, 'b7': 3951.07, 'c8': 4186.01 21 | }; 22 | 23 | let _notes: string[] = [ 24 | 'a', 'b', 'c', 'd', 'e', 'f', 'g' 25 | ]; 26 | 27 | let _flatSigns = ['b', '@']; 28 | let _sharpSigns = ['#']; 29 | 30 | let PianoData = { 31 | FlatSigns: _flatSigns, 32 | NoteMap: _noteMap, 33 | Notes: _notes, 34 | SharpSigns: _sharpSigns 35 | }; 36 | 37 | export { PianoData }; 38 | -------------------------------------------------------------------------------- /source/player.ts: -------------------------------------------------------------------------------- 1 | import { Song } from './song'; 2 | import { Note, Synth } from './synth'; 3 | 4 | function play(this: Song, when?: number) { 5 | if (when === undefined) { 6 | when = Synth.Context.currentTime; 7 | } 8 | 9 | for (let track of this._master) { 10 | for (let note of track.Notes) { 11 | playAt(note, track.WhenSeconds + when, track.DurationSeconds); 12 | } 13 | } 14 | } 15 | 16 | function stop(this: Song) { 17 | for (let track of this._master) { 18 | for (let note of track.Notes) { 19 | note.Stop() 20 | } 21 | } 22 | } 23 | 24 | function playAt(note: Note, whenSeconds: number, durationSeconds: number, startingAtSeconds: number = 0) { 25 | let startSeconds = Math.max(whenSeconds - startingAtSeconds, 0); 26 | let stopSeconds = Math.max((whenSeconds + durationSeconds) - startingAtSeconds, 0); 27 | 28 | if (stopSeconds === 0) { 29 | return; 30 | } 31 | 32 | note.Play(startSeconds, stopSeconds); 33 | } 34 | 35 | let Player = { 36 | play, 37 | stop 38 | }; 39 | 40 | export { Player }; 41 | -------------------------------------------------------------------------------- /source/scheduler.ts: -------------------------------------------------------------------------------- 1 | const WebAudioScheduler = require('web-audio-scheduler'); 2 | 3 | import { Improviser, Scale } from './improviser'; 4 | import { Song } from './song'; 5 | import { Note, Synth } from './synth'; 6 | import { Validate } from './validate'; 7 | 8 | export interface ActionContext { 9 | Measure: number; 10 | Song: Song; 11 | } 12 | 13 | export interface Actions { 14 | callback: (callback: Function) => void; 15 | improvises: (improvisable: Scale, duration: number) => void; 16 | plays: (playable: TimedNote|TimedChord|Sequence) => void; 17 | repeats: (repeatable: TimedNote|TimedChord, config: RepeatConfig) => void; 18 | } 19 | 20 | export interface Moment { 21 | Duration: number; 22 | } 23 | 24 | export interface RepeatConfig { 25 | every: number; 26 | times: number; 27 | } 28 | 29 | export interface Rest extends Moment { } 30 | 31 | export interface Sequence extends Array { } 32 | 33 | export interface TimedChord extends Moment { 34 | Notes: Note[]; 35 | } 36 | 37 | export interface TimedNote extends Moment { 38 | Note: Note; 39 | } 40 | 41 | export interface Track { 42 | Notes: Note[]; 43 | WhenSeconds: number; // in seconds 44 | DurationSeconds: number; // also in seconds 45 | } 46 | 47 | function improvises(context: ActionContext, improvisable: Scale, duration: number): void { 48 | let improvisedSequence = Improviser.improvise(improvisable, duration); 49 | plays(context, improvisedSequence); 50 | } 51 | 52 | function plays(context: ActionContext, playable: TimedNote|TimedChord|Sequence): void { 53 | let tracksToAdd = getTracks(context, playable); 54 | context.Song._master = context.Song._master.concat(tracksToAdd); 55 | } 56 | 57 | function repeats(context: ActionContext, repeatable: TimedNote|TimedChord, config: RepeatConfig): void { 58 | let tracksToAdd: Track[] = []; 59 | let baseTrack: Track = getTracks(context, repeatable)[0]; 60 | 61 | for (let index = 0; index < config.times; index++) { 62 | let track: Track = { 63 | Notes: baseTrack.Notes, 64 | DurationSeconds: baseTrack.DurationSeconds, 65 | WhenSeconds: baseTrack.WhenSeconds + (index * noteValueToSeconds(config.every, context.Song)) 66 | }; 67 | tracksToAdd.push(track); 68 | } 69 | 70 | context.Song._master = context.Song._master.concat(tracksToAdd); 71 | } 72 | 73 | function noteValueToMeasures(noteValue: number, song: Song): number { 74 | return noteValue * ( 75 | song._metadata.TimeSignature.noteValue / 76 | song._metadata.TimeSignature.beatsPerMeasure 77 | ); 78 | } 79 | 80 | function noteValueToSeconds(noteValue: number, song: Song): number { 81 | let beatsPerMinute = song._metadata.Tempo; 82 | let secondsPerMinute = 60; 83 | let secondsPerBeat = secondsPerMinute / beatsPerMinute; 84 | 85 | let beats = noteValue * song._metadata.TimeSignature.noteValue; 86 | 87 | return secondsPerBeat * beats; 88 | } 89 | 90 | function measuresToSeconds(measures: number, song: Song): number { 91 | let beatsPerMinute = song._metadata.Tempo; 92 | let secondsPerMinute = 60; 93 | let beatsPerSecond = beatsPerMinute / secondsPerMinute; 94 | let beatsPerMeasure = song._metadata.TimeSignature.beatsPerMeasure; 95 | let secondsPerMeasure = beatsPerMeasure / beatsPerSecond; 96 | 97 | return secondsPerMeasure * measures; 98 | } 99 | 100 | function getTracks(context: ActionContext, playable: TimedNote|TimedChord|Sequence): Track[] { 101 | let WhenSeconds = measuresToSeconds(context.Measure, context.Song); 102 | 103 | if (Validate.isTimedNote(playable)) { 104 | let track: Track = { 105 | Notes: [playable.Note], 106 | DurationSeconds: noteValueToSeconds(playable.Duration, context.Song), 107 | WhenSeconds: WhenSeconds 108 | }; 109 | return [track]; 110 | } else if (Validate.isTimedChord(playable)) { 111 | let track: Track = { 112 | Notes: playable.Notes, 113 | DurationSeconds: noteValueToSeconds(playable.Duration, context.Song), 114 | WhenSeconds: WhenSeconds 115 | }; 116 | return [track]; 117 | } else { 118 | return playable.map((item, index) => { 119 | if (index > 0) { 120 | context.Measure += noteValueToMeasures(playable[index - 1].Duration, context.Song); 121 | } 122 | 123 | if (Validate.isTimedNote(item)) { 124 | return getTracks(context, item); 125 | } else if (Validate.isTimedChord(item)) { 126 | return getTracks(context, item); 127 | } else { 128 | let track: Track = { 129 | Notes: [], 130 | DurationSeconds: noteValueToSeconds(item.Duration, context.Song), 131 | WhenSeconds: WhenSeconds 132 | }; 133 | return [track]; 134 | } 135 | }).reduce((a, b) => a.concat(b), []); 136 | } 137 | } 138 | 139 | function getActions(song: Song, measure: number): Actions { 140 | let context = Synth.Context; 141 | let scheduler = new WebAudioScheduler({ context }); 142 | scheduler.start(); 143 | 144 | let actionContext: ActionContext = { 145 | Measure: measure, 146 | Song: song 147 | }; 148 | 149 | let actions: Actions = { 150 | callback: function(callback: Function) { 151 | let whenSeconds = measuresToSeconds(measure, song); 152 | scheduler.insert(whenSeconds + context.currentTime, callback); 153 | }, 154 | improvises: function(improvisable: Scale, duration: number) { 155 | return improvises(actionContext, improvisable, duration); 156 | }, 157 | plays: function (playable: TimedNote|TimedChord|Sequence) { 158 | return plays(actionContext, playable); 159 | }, 160 | repeats: function (repeatable: TimedNote|TimedChord, config: RepeatConfig) { 161 | return repeats(actionContext, repeatable, config); 162 | } 163 | } as Actions; 164 | 165 | return actions; 166 | } 167 | 168 | let Scheduler = { 169 | GetActions: getActions, 170 | }; 171 | 172 | export { Scheduler }; 173 | -------------------------------------------------------------------------------- /source/song.ts: -------------------------------------------------------------------------------- 1 | import { Actions, Track } from './scheduler'; 2 | 3 | export interface Song { 4 | at: (this: Song, measure: number) => Actions, 5 | play: () => void, 6 | stop: () => void, 7 | setTimeSignature: (numerator: number, denominator: number) => void, 8 | setTempo: (tempo: number) => void, 9 | _master: Track[], 10 | _metadata: SongMetadata, 11 | } 12 | 13 | export interface SongMetadata { 14 | Tempo: number, 15 | TimeSignature: TimeSignature, 16 | Title: string, 17 | } 18 | 19 | export interface TimeSignature { 20 | beatsPerMeasure: number; 21 | noteValue: number; 22 | } 23 | 24 | let _commonTime: TimeSignature = { 25 | beatsPerMeasure: 4, 26 | noteValue: 4 27 | }; 28 | 29 | let _defaultTempo = 120; 30 | 31 | let _defaultTitle = 'Unnamed'; 32 | 33 | function DefaultSongData(): SongMetadata { 34 | let metadata: SongMetadata = { 35 | Tempo: _defaultTempo, 36 | TimeSignature: _commonTime, 37 | Title: _defaultTitle, 38 | }; 39 | 40 | return metadata; 41 | } 42 | 43 | export { DefaultSongData }; 44 | -------------------------------------------------------------------------------- /source/style.ts: -------------------------------------------------------------------------------- 1 | enum Style { 2 | None, 3 | Legato, 4 | Staccato, 5 | Pianissimo, 6 | Piano, 7 | MezzoPiano, 8 | MezzoForte, 9 | Forte, 10 | Fortissimo 11 | }; 12 | 13 | export interface StyleDynamicsDict { 14 | [style: number]: number; 15 | } 16 | 17 | let StyleDynamics: StyleDynamicsDict = { 18 | [Style.Pianissimo]: -3, 19 | [Style.Piano]: -2, 20 | [Style.MezzoPiano]: -1, 21 | [Style.MezzoForte]: 1, 22 | [Style.Forte]: 2, 23 | [Style.Fortissimo]: 3 24 | }; 25 | 26 | export { Style, StyleDynamics }; 27 | -------------------------------------------------------------------------------- /source/synth.ts: -------------------------------------------------------------------------------- 1 | // This file contains code for generating the piano synth. 2 | // All variables are pluggable so that a user-configured synth 3 | // can be used seamlessly, and arbitrary notes can be played with 4 | // arbitrary articulation and dynamics. 5 | 6 | import Limit from './brickwall-limiter.lib'; 7 | 8 | import { Style, StyleDynamics } from './style'; 9 | 10 | export interface Note { 11 | Frequency: number; 12 | GetNodeChain: (this: Note) => NodeChain; 13 | Play: (this: Note, startSeconds: number, stopSeconds: number) => void; 14 | Stop: (this: Note) => void; 15 | Style: Style[]; 16 | _nodeChain: NodeChain; 17 | _started: boolean; 18 | } 19 | 20 | export interface NodeChain { 21 | Gain: GainNode; 22 | Oscillator: OscillatorNode; 23 | } 24 | 25 | let _context = new AudioContext(); 26 | 27 | let masterGain = _context.createGain(); 28 | masterGain.gain.value = 0.7; 29 | 30 | let brickwallLimiter = _context.createScriptProcessor(4096, 1, 1); 31 | brickwallLimiter.onaudioprocess = Limit; 32 | 33 | brickwallLimiter.connect(_context.destination); 34 | masterGain.connect(brickwallLimiter); 35 | 36 | 37 | function defaultGain(frequency: number, style: Style[], masterGain: GainNode): GainNode { 38 | let gainNode = _context.createGain(); 39 | gainNode.gain.value = 0.2; 40 | 41 | style.some((st) => { 42 | let dynamics: number = StyleDynamics[st]; 43 | if (dynamics) { 44 | gainNode.gain.value += dynamics * 0.12; 45 | return true; 46 | } 47 | return false; 48 | }); 49 | 50 | // Lower frequencies are too quiet and higher frequencies are too loud. 51 | // To solve this, let's modify the gain based on the frequency. 52 | 53 | // Frequency is exponential, i.e. frequency = note ** 2 54 | // We'll square root everything to make it linear 55 | let frequencyLinear = Math.sqrt(frequency); 56 | let maxFrequencyLinear = Math.sqrt(4200); 57 | 58 | // Then we take the percent of max frequency and place it in the range [-f + g, f + g] 59 | // where f + g is the maximum amount we want to increase gain 60 | // do magic to it if f + g > 0 61 | // then add that to the gain. 62 | let frequencyScale = frequencyLinear / maxFrequencyLinear; 63 | let frequencyFactor = 0.25; 64 | let frequencyOffset = 0.08; 65 | let frequencyModifier = (frequencyScale * -frequencyFactor) + (frequencyFactor / 2) + frequencyOffset; 66 | 67 | if (frequencyModifier > 0) { 68 | frequencyModifier = ((frequencyModifier + 1) ** 3) - 1; 69 | } 70 | 71 | gainNode.gain.value += frequencyModifier; 72 | 73 | gainNode.connect(masterGain); 74 | 75 | return gainNode; 76 | } 77 | 78 | function defaultOscillator(frequency: number, _style: Style[], gainNode: GainNode): OscillatorNode { 79 | let oscillator = _context.createOscillator(); 80 | oscillator.frequency.value = frequency; 81 | oscillator.type = 'sine'; 82 | 83 | oscillator.connect(gainNode); 84 | 85 | return oscillator; 86 | } 87 | 88 | function defaultPlayer(note: Note, startSeconds: number, stopSeconds: number): void { 89 | let nodes = note.GetNodeChain(); 90 | 91 | let noteDuration = stopSeconds - startSeconds, 92 | noteFadePct = 0.04, 93 | noteStopTime = stopSeconds; 94 | 95 | if (!!~note.Style.indexOf(Style.Legato)) { 96 | noteFadePct = 0.01; 97 | noteStopTime = stopSeconds; 98 | } else if (!!~note.Style.indexOf(Style.Staccato)) { 99 | noteFadePct = 0.01; 100 | noteStopTime = startSeconds + 0.15; 101 | } 102 | 103 | let noteFadeTime = noteFadePct * noteDuration; 104 | 105 | let maxGain = nodes.Gain.gain.value; 106 | 107 | nodes.Gain.gain.value = 0; 108 | nodes.Gain.gain.setTargetAtTime(maxGain, startSeconds, noteFadeTime); 109 | nodes.Gain.gain.setTargetAtTime(0, noteStopTime - (noteFadeTime * 4), noteFadeTime); 110 | 111 | nodes.Oscillator.start(startSeconds); 112 | nodes.Oscillator.stop(noteStopTime); 113 | } 114 | 115 | let _gain = defaultGain; 116 | let _oscillator = defaultOscillator; 117 | let _player = defaultPlayer; 118 | 119 | function synthesizeNote(frequency: number, style: Style[]): Note { 120 | let note: Note = { 121 | Frequency: frequency, 122 | GetNodeChain: function(this: Note): NodeChain { 123 | if (this._nodeChain) { 124 | return this._nodeChain 125 | } 126 | 127 | let gain = _gain(frequency, style, masterGain); 128 | let oscillator = _oscillator(frequency, style, gain); 129 | 130 | let nodeChain: NodeChain = { 131 | Gain: gain, 132 | Oscillator: oscillator 133 | }; 134 | 135 | this._nodeChain = nodeChain 136 | return this._nodeChain; 137 | }, 138 | Play: function(this: Note, startSeconds: number, stopSeconds: number): void { 139 | _player(this, startSeconds, stopSeconds); 140 | this._started = true 141 | }, 142 | Stop: function(this: Note): void { 143 | if (this._started) { 144 | this.GetNodeChain().Oscillator.stop() 145 | } 146 | this._nodeChain = null 147 | }, 148 | Style: style, 149 | _nodeChain: null, 150 | _started: false 151 | }; 152 | 153 | return note; 154 | } 155 | 156 | function setGain(gain: (frequency: number, style: Style[], masterGain: GainNode) => GainNode): void { 157 | _gain = gain; 158 | } 159 | 160 | function setOscillator(oscillator: (frequency: number, style: Style[], gainNode: GainNode) => OscillatorNode): void { 161 | _oscillator = oscillator; 162 | } 163 | 164 | function setPlayer(player: (note: Note, startSeconds: number, stopSeconds: number) => void) { 165 | _player = player; 166 | } 167 | 168 | let Synth = { 169 | Context: _context, 170 | SetGain: setGain, 171 | SetOscillator: setOscillator, 172 | SetPlayer: setPlayer, 173 | SynthesizeNote: synthesizeNote, 174 | }; 175 | 176 | export { Synth }; 177 | -------------------------------------------------------------------------------- /source/validate.ts: -------------------------------------------------------------------------------- 1 | import { PianoData } from './piano-data'; 2 | import { Rest, Sequence, TimedChord, TimedNote } from './scheduler'; 3 | 4 | function isTimedChord(value: TimedNote|TimedChord|Sequence|Rest): value is TimedChord { 5 | return (value).Notes !== undefined; 6 | } 7 | 8 | function isTimedNote(value: TimedNote|TimedChord|Sequence|Rest): value is TimedNote { 9 | return (value).Note !== undefined; 10 | } 11 | 12 | function validateNote(note: string): void { 13 | if (!~PianoData.Notes.indexOf(note)) { 14 | throw Error(`"${note}" is not a valid note name between "a" and "g".`); 15 | } 16 | } 17 | 18 | function validateOctave(octave: number|string): void { 19 | octave = Number(octave); 20 | 21 | if (isNaN(octave)) { 22 | throw Error(`Invalid octave "${octave}".`); 23 | } 24 | 25 | if (octave > 8 || octave < 0) { 26 | throw Error(`This octave is out of range (0 - 8) on a standard piano.`); 27 | } 28 | } 29 | 30 | function validateSign(sign: string): void { 31 | if (!~PianoData.FlatSigns.indexOf(sign) && !~PianoData.SharpSigns.indexOf(sign)) { 32 | throw Error(`Invalid sign "${sign}".`); 33 | } 34 | } 35 | 36 | let Validate = { 37 | isTimedChord, 38 | isTimedNote, 39 | Note: validateNote, 40 | Octave: validateOctave, 41 | Sign: validateSign 42 | }; 43 | 44 | export { Validate }; 45 | -------------------------------------------------------------------------------- /source/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "production", 5 | entry: { 6 | blackswan: path.resolve(__dirname, "./base.ts") 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, "../dist"), 10 | filename: "[name].js", 11 | library: "blackswan", 12 | libraryTarget: "umd", 13 | umdNamedDefine: true 14 | }, 15 | resolve: { 16 | extensions: [".ts", ".tsx", ".js"] 17 | }, 18 | module: { 19 | rules: [ 20 | // all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader' 21 | { 22 | test: /\.tsx?$/, 23 | loader: "ts-loader", 24 | options: { 25 | compilerOptions: { 26 | declaration: true, 27 | declarationDir: path.resolve(__dirname, "../") 28 | } 29 | } 30 | } 31 | ] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "ES6", 5 | "sourceMap": true, 6 | "outDir": "./dist", 7 | "rootDir": "./", 8 | "strict": false, 9 | "alwaysStrict": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitThis": true, 13 | "noImplicitReturns": true, 14 | "noImplicitAny": false, 15 | "moduleResolution": "Node", 16 | "baseUrl": "./", 17 | "paths": {} 18 | }, 19 | "include": [ 20 | "source/**/*.ts" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------