mirror of
https://github.com/bvanroll/rpiRadio.git
synced 2025-08-30 20:42:44 +00:00
Initial Commit
This commit is contained in:
11
ProjectNow/NodeServer/node_modules/synth-js/.npmignore
generated
vendored
Normal file
11
ProjectNow/NodeServer/node_modules/synth-js/.npmignore
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
*.DS_Store
|
||||
._*
|
||||
|
||||
examples
|
||||
|
||||
node_modules
|
||||
coverage
|
||||
npm-debug.log*
|
||||
|
||||
.npm
|
||||
.env
|
19
ProjectNow/NodeServer/node_modules/synth-js/LICENSE
generated
vendored
Normal file
19
ProjectNow/NodeServer/node_modules/synth-js/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2017 Patrick Roberts
|
||||
|
||||
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.
|
133
ProjectNow/NodeServer/node_modules/synth-js/README.md
generated
vendored
Normal file
133
ProjectNow/NodeServer/node_modules/synth-js/README.md
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
# synth-js
|
||||
|
||||
Command-line utility and Node module for generating raw audio data from MIDI files.
|
||||
|
||||
## Installation
|
||||
|
||||
### Command-line utility:
|
||||
|
||||
```bash
|
||||
$ npm link synth-js
|
||||
```
|
||||
|
||||
### Node module:
|
||||
|
||||
```bash
|
||||
$ npm install --save synth-js
|
||||
```
|
||||
|
||||
### JavaScript:
|
||||
|
||||
```html
|
||||
<script src="synth.min.js"></script>
|
||||
```
|
||||
|
||||
After including the file from `dst/synth.min.js`, the global variable `synth` will be initialized.
|
||||
|
||||
## Usage
|
||||
|
||||
### Command-line utility:
|
||||
|
||||
```bash
|
||||
# assuming song.mid in cwd
|
||||
$ synth -i song
|
||||
# now song.wav contains raw audio data for song.mid
|
||||
```
|
||||
|
||||
### Node module:
|
||||
|
||||
```js
|
||||
const synth = require('synth-js');
|
||||
const fs = require('fs');
|
||||
|
||||
let midBuffer = fs.readFileSync('song.mid');
|
||||
// convert midi buffer to wav buffer
|
||||
let wavBuffer = synth.midiToWav(midiBuffer).toBuffer();
|
||||
|
||||
fs.writeFileSync('song.wav', wavBuffer, {encoding: 'binary'});
|
||||
```
|
||||
|
||||
### JavaScript:
|
||||
|
||||
```html
|
||||
<style>
|
||||
#wav:after {
|
||||
content: " " attr(download);
|
||||
}
|
||||
|
||||
#wav:not([href]) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<input type="file" id="midi" accept="audio/midi">
|
||||
<a id="wav">Download</a>
|
||||
<script>
|
||||
var audio;
|
||||
var input = document.getElementById('midi');
|
||||
var anchor = document.getElementById('wav');
|
||||
|
||||
input.addEventListener('change', function change() {
|
||||
// clean up previous song, if any
|
||||
if (anchor.hasAttribute('href')) {
|
||||
URL.revokeObjectURL(anchor.href);
|
||||
anchor.removeAttribute('href');
|
||||
|
||||
if (audio && !audio.paused) {
|
||||
audio.pause();
|
||||
}
|
||||
}
|
||||
|
||||
// check if file exists
|
||||
if (input.files.length > 0) {
|
||||
var reader = new FileReader();
|
||||
var midName = input.files[0].name;
|
||||
// replace file extension with .wav
|
||||
var wavName = midName.replace(/\..+?$/, '.wav');
|
||||
|
||||
// set callback for array buffer
|
||||
reader.addEventListener('load', function load(event) {
|
||||
// convert midi arraybuffer to wav blob
|
||||
var wav = synth.midiToWav(event.target.result).toBlob();
|
||||
// create a temporary URL to the wav file
|
||||
var src = URL.createObjectURL(wav);
|
||||
|
||||
audio = new Audio(src);
|
||||
audio.play();
|
||||
|
||||
anchor.setAttribute('href', src);
|
||||
});
|
||||
|
||||
// read the file as an array buffer
|
||||
reader.readAsArrayBuffer(input.files[0]);
|
||||
|
||||
// set the name of the wav file
|
||||
anchor.setAttribute('download', wavName);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
See the demo [here][browser-demo].
|
||||
|
||||
## FAQ
|
||||
|
||||
### Where can I find documentation?
|
||||
|
||||
Currently, documentation only exists for the command-line utility.
|
||||
To access it, use `man`:
|
||||
|
||||
```bash
|
||||
$ man synth
|
||||
```
|
||||
|
||||
For Node or JavaScript, refer to the `src/` directory for accessible APIs:
|
||||
|
||||
* `synth.WAV()`
|
||||
* `synth.MIDIStream()`
|
||||
* `synth.midiToWav()`
|
||||
|
||||
## License
|
||||
|
||||
Available under the MIT License
|
||||
|
||||
[browser-demo]: https://jsfiddle.net/patrob10114/o5r1adyz/show/
|
8
ProjectNow/NodeServer/node_modules/synth-js/bin/defaults.json
generated
vendored
Normal file
8
ProjectNow/NodeServer/node_modules/synth-js/bin/defaults.json
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"DryRun": false,
|
||||
"help": false,
|
||||
"sampleRate": 44100,
|
||||
"bitsPerSample": 16,
|
||||
"duration": 1,
|
||||
"Skip": []
|
||||
}
|
56
ProjectNow/NodeServer/node_modules/synth-js/bin/synth.js
generated
vendored
Executable file
56
ProjectNow/NodeServer/node_modules/synth-js/bin/synth.js
generated
vendored
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const midiToWav = require('../src/midi2wav');
|
||||
const getArgs = require('./utils/args');
|
||||
|
||||
try {
|
||||
const args = getArgs(process.argv);
|
||||
|
||||
if (args.help || args.input === undefined) {
|
||||
return console.log(fs.readFileSync(`${__dirname}/../man/help.txt`, {encoding: 'utf8'}));
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
console.log(JSON.stringify(args, null, 2));
|
||||
}
|
||||
|
||||
if (typeof args.input === 'string') {
|
||||
fs.readFile(`${args.input}.mid`, {encoding: null}, (error, data) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
console.log('parsing MIDI...');
|
||||
}
|
||||
|
||||
const wav = midiToWav(data, args);
|
||||
|
||||
if (args.DryRun) {
|
||||
return console.log('dry run complete');
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
console.log('writing buffer...');
|
||||
}
|
||||
|
||||
const buffer = wav.toBuffer();
|
||||
|
||||
fs.writeFile(`${args.output}.wav`, buffer, {encoding: null}, (error) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
console.log('success');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
throw new ReferenceError('no input file specified');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error.stack);
|
||||
}
|
90
ProjectNow/NodeServer/node_modules/synth-js/bin/utils/args.js
generated
vendored
Normal file
90
ProjectNow/NodeServer/node_modules/synth-js/bin/utils/args.js
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
function argumentToVerboseFlag(argument) {
|
||||
const camelCase = /^[\w]|\B[A-Z]/g;
|
||||
|
||||
return argument.replace(camelCase, (character, index) => `-${'-'.charAt(index)}${character.toLowerCase()}`);
|
||||
}
|
||||
|
||||
function argumentToConciseFlag(argument) {
|
||||
const beginning = /^(.).*$/;
|
||||
|
||||
return argument.replace(beginning, (match, character) => `-${character}`);
|
||||
}
|
||||
|
||||
// utility function to convert
|
||||
// command-line arguments to object
|
||||
module.exports = function getArguments(argv) {
|
||||
const argumentNames = [
|
||||
'input',
|
||||
'output',
|
||||
'sampleRate',
|
||||
'bitsPerSample',
|
||||
'duration',
|
||||
'Skip',
|
||||
'verbose',
|
||||
'DryRun',
|
||||
'help'
|
||||
];
|
||||
|
||||
const defaultArgs = require('../defaults.json');
|
||||
|
||||
const flagToArgument = {};
|
||||
|
||||
argumentNames.forEach((argumentName) => {
|
||||
flagToArgument[argumentToVerboseFlag(argumentName)] = argumentName;
|
||||
flagToArgument[argumentToConciseFlag(argumentName)] = argumentName;
|
||||
});
|
||||
|
||||
const args = {};
|
||||
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
let flag = argv[i];
|
||||
let arg = flagToArgument[flag];
|
||||
|
||||
switch (arg) {
|
||||
case 'input':
|
||||
case 'output':
|
||||
args[arg] = argv[++i];
|
||||
break;
|
||||
case 'verbose':
|
||||
case 'DryRun':
|
||||
case 'help':
|
||||
args[arg] = true;
|
||||
break;
|
||||
case 'Skip':
|
||||
try {
|
||||
args[arg] = JSON.parse(argv[++i]);
|
||||
} catch (error) {
|
||||
try {
|
||||
args[arg] = require(process.cwd() + '/' + argv[i]);
|
||||
} catch (error) {
|
||||
throw new SyntaxError(`unable to parse argument for ${flag}: ${argv[i]}`);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'sampleRate':
|
||||
case 'bitsPerSample':
|
||||
case 'duration':
|
||||
args[arg] = +argv[++i];
|
||||
|
||||
if (isNaN(args[arg])) {
|
||||
try {
|
||||
args[arg] = require(process.cwd() + '/' + argv[i]);
|
||||
} catch (error) {
|
||||
throw new TypeError(`invalid argument for ${flag}: ${argv[i]}`);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ReferenceError(`unrecognized flag: ${flag}`);
|
||||
}
|
||||
}
|
||||
|
||||
// default to default arguments
|
||||
// default output is same as input, verbose is same as DryRun
|
||||
// specified arguments will override defaults
|
||||
return Object.assign({}, defaultArgs, {output: args.input, verbose: args.DryRun}, args);
|
||||
};
|
3
ProjectNow/NodeServer/node_modules/synth-js/dst/synth.min.js
generated
vendored
Normal file
3
ProjectNow/NodeServer/node_modules/synth-js/dst/synth.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ProjectNow/NodeServer/node_modules/synth-js/dst/synth.min.js.map
generated
vendored
Normal file
1
ProjectNow/NodeServer/node_modules/synth-js/dst/synth.min.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
10
ProjectNow/NodeServer/node_modules/synth-js/gulp/config.json
generated
vendored
Normal file
10
ProjectNow/NodeServer/node_modules/synth-js/gulp/config.json
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"paths": {
|
||||
"entry": "./index.js",
|
||||
"dest": "./dst"
|
||||
},
|
||||
"names": {
|
||||
"app": "synth.min.js",
|
||||
"glob": "synth"
|
||||
}
|
||||
}
|
35
ProjectNow/NodeServer/node_modules/synth-js/gulp/tasks/browserify.js
generated
vendored
Normal file
35
ProjectNow/NodeServer/node_modules/synth-js/gulp/tasks/browserify.js
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
const config = require('../config');
|
||||
|
||||
const gulp = require('gulp');
|
||||
const browserify = require('browserify');
|
||||
const babelify = require('babelify');
|
||||
const source = require('vinyl-source-stream');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const uglify = require('gulp-uglify');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const gutil = require('gulp-util');
|
||||
|
||||
gulp.task('browserify', function () {
|
||||
// set up the browserify instance on a task basis
|
||||
let b = browserify({
|
||||
entries: config.paths.entry,
|
||||
debug: true,
|
||||
standalone: config.names.glob,
|
||||
transform: [
|
||||
babelify.configure({
|
||||
presets: ['es2015'],
|
||||
sourceMapsAbsolute: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return b.bundle()
|
||||
.pipe(source(config.names.app))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
// Add transformation tasks to the pipeline here.
|
||||
.pipe(uglify())
|
||||
.on('error', gutil.log)
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(config.paths.dest));
|
||||
});
|
3
ProjectNow/NodeServer/node_modules/synth-js/gulp/tasks/default.js
generated
vendored
Normal file
3
ProjectNow/NodeServer/node_modules/synth-js/gulp/tasks/default.js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
const gulp = require('gulp');
|
||||
|
||||
gulp.task('default', ['browserify']);
|
5
ProjectNow/NodeServer/node_modules/synth-js/gulpfile.js
generated
vendored
Normal file
5
ProjectNow/NodeServer/node_modules/synth-js/gulpfile.js
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
// Module to require whole directories
|
||||
const requireDir = require('require-dir');
|
||||
|
||||
// Pulling in all tasks from the tasks folder
|
||||
requireDir('./gulp/tasks', { recurse: true });
|
7
ProjectNow/NodeServer/node_modules/synth-js/index.js
generated
vendored
Normal file
7
ProjectNow/NodeServer/node_modules/synth-js/index.js
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
WAV: require('./src/wav'),
|
||||
MIDIStream: require('./src/midi'),
|
||||
midiToWav: require('./src/midi2wav')
|
||||
};
|
74
ProjectNow/NodeServer/node_modules/synth-js/man/help.txt
generated
vendored
Normal file
74
ProjectNow/NodeServer/node_modules/synth-js/man/help.txt
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
NAME
|
||||
synth - JavaScript MIDI-to-WAV synthesizer
|
||||
|
||||
SYNOPSIS
|
||||
synth -i filename [options]
|
||||
|
||||
DESCRIPTION
|
||||
Parses MIDI file and synthesizes sinusoidal raw audio in WAV format.
|
||||
|
||||
OPTIONS
|
||||
-i, --input filename
|
||||
[Required] Specify path of MIDI file (without extension) as input
|
||||
|
||||
-o, --output filename
|
||||
Specify path of WAV file (without extension) as output
|
||||
Default to input
|
||||
|
||||
-s, --sample-rate number
|
||||
Specify explicitly the sample rate in Hz of WAV output.
|
||||
Default 44100
|
||||
|
||||
-b, --bits-per-sample number
|
||||
Specify explicitly the bits per sample of WAV output.
|
||||
Default 16
|
||||
|
||||
-d, --duration number
|
||||
Specify explicitly the relative note duration before rest.
|
||||
Default 1
|
||||
|
||||
-S, --skip json|file
|
||||
Specify either JSON literal or path of JSON file containing meta events or
|
||||
source file with which to match and omit MIDI tracks from WAV output. If
|
||||
source file is specified, imports function that accepts a track and
|
||||
returns a boolean specifying whether to skip or not.
|
||||
Default []
|
||||
|
||||
Example Usage:
|
||||
synth -i file -S [{\"text\":\"Drums\"},{\"text\":\"Timpani\"}]
|
||||
synth -i file -S skip.json
|
||||
|
||||
skip.json:
|
||||
|
||||
[
|
||||
{
|
||||
"text": "Drums"
|
||||
},
|
||||
{
|
||||
"text": "Timpani"
|
||||
}
|
||||
]
|
||||
|
||||
synth -i file -S skip.js
|
||||
|
||||
skip.js:
|
||||
|
||||
module.exports = function (track) {
|
||||
// ...logic for determining whether to skip
|
||||
return skip;
|
||||
};
|
||||
|
||||
-v, --verbose
|
||||
Verbose output
|
||||
|
||||
-D, --dry-run
|
||||
Run verbosely without generating WAV file
|
||||
|
||||
-h, --help
|
||||
Display the help page
|
||||
|
||||
AUTHOR
|
||||
Patrick Roberts
|
||||
|
||||
COPYRIGHT
|
||||
Synth is available under the MIT License.
|
66
ProjectNow/NodeServer/node_modules/synth-js/man/synth.1
generated
vendored
Normal file
66
ProjectNow/NodeServer/node_modules/synth-js/man/synth.1
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
.TH SYNTH 1
|
||||
.SH NAME
|
||||
\fIsynth\fR \- JavaScript MIDI\-to\-WAV synthesizer
|
||||
.SH SYNOPSIS
|
||||
.B synth
|
||||
\-i
|
||||
.BR filename
|
||||
[options]
|
||||
.SH DESCRIPTION
|
||||
.B \fIsynth\fR
|
||||
parses MIDI file and synthesizes sinusoidal raw audio in WAV format.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.BR \-i ", " \-\-input " " \fIfilename\fR
|
||||
[Required] Specify path of MIDI file (without extension) as input
|
||||
.TP
|
||||
.BR \-o ", " \-\-output " " \fIfilename\fR
|
||||
Specify path of WAV file (without extension) as output
|
||||
.PP
|
||||
.RS
|
||||
Default to \fIinput\fR
|
||||
.RE
|
||||
.TP
|
||||
.BR \-s ", " \-\-sample\-rate " " \fInumber\fR
|
||||
Specify explicitly the sample rate in Hz of WAV output.
|
||||
.PP
|
||||
.RS
|
||||
Default 44100
|
||||
.RE
|
||||
.TP
|
||||
.BR \-b ", " \-\-bits\-per\-sample " " \fInumber\fR
|
||||
Specify explicitly the bits per sample of WAV output.
|
||||
.PP
|
||||
.RS
|
||||
Default 16
|
||||
.RE
|
||||
.TP
|
||||
.BR \-d ", " \-\-duration " " \fInumber\fR
|
||||
Specify explicitly the relative note duration before rest.
|
||||
.PP
|
||||
.RS
|
||||
Default 1
|
||||
.RE
|
||||
.TP
|
||||
.BR \-S ", " \-\-skip " " \fIjson\fR|\fIfile\fR
|
||||
Specify either JSON literal or path of JSON file containing meta events or
|
||||
source file with which to match and omit MIDI tracks from WAV output. If
|
||||
source file is specified, imports function that accepts a track and
|
||||
returns a boolean specifying whether to skip or not.
|
||||
.PP
|
||||
.RS
|
||||
Default []
|
||||
.RE
|
||||
.TP
|
||||
.BR \-v ", " \-\-verbose
|
||||
Verbose output
|
||||
.TP
|
||||
.BR \-D ", " \-\-dry\-run
|
||||
Run verbosely without generating WAV file
|
||||
.TP
|
||||
.BR \-h ", " \-\-help
|
||||
Display the help page
|
||||
.SH AUTHOR
|
||||
Patrick Roberts
|
||||
.SH COPYRIGHT
|
||||
.B \fIsynth\fR is available under the MIT License.
|
95
ProjectNow/NodeServer/node_modules/synth-js/package.json
generated
vendored
Normal file
95
ProjectNow/NodeServer/node_modules/synth-js/package.json
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"_from": "synth-js",
|
||||
"_id": "synth-js@0.1.1",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha1-7i5nIc5nMMDlPVF3vrQB5JPJAO8=",
|
||||
"_location": "/synth-js",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "tag",
|
||||
"registry": true,
|
||||
"raw": "synth-js",
|
||||
"name": "synth-js",
|
||||
"escapedName": "synth-js",
|
||||
"rawSpec": "",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "latest"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"#USER",
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/synth-js/-/synth-js-0.1.1.tgz",
|
||||
"_shasum": "ee2e6721ce6730c0e53d5177beb401e493c900ef",
|
||||
"_spec": "synth-js",
|
||||
"_where": "/home/beppe/Documents/Python/proj/1718PROJrpiRadio/NodeServer",
|
||||
"author": {
|
||||
"name": "Patrick Roberts"
|
||||
},
|
||||
"bin": {
|
||||
"synth": "./bin/synth.js"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/patrickroberts/synth-js/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {},
|
||||
"deprecated": false,
|
||||
"description": "high performance MIDI parser and WAV encoder",
|
||||
"devDependencies": {
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babelify": "^7.3.0",
|
||||
"browserify": "^14.3.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-sourcemaps": "^2.6.0",
|
||||
"gulp-uglify": "^2.1.2",
|
||||
"gulp-util": "^3.0.8",
|
||||
"require-dir": "^0.3.1",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.2",
|
||||
"npm": "^4.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/patrickroberts/synth-js#readme",
|
||||
"keywords": [
|
||||
"wav",
|
||||
"midi",
|
||||
"riff",
|
||||
"audio",
|
||||
"music"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"man": [
|
||||
"./man/synth.1"
|
||||
],
|
||||
"name": "synth-js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/patrickroberts/synth-js.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublishOnly": "gulp"
|
||||
},
|
||||
"version": "0.1.1",
|
||||
"warnings": [
|
||||
{
|
||||
"code": "ENOTSUP",
|
||||
"required": {
|
||||
"node": ">=6.9.2",
|
||||
"npm": "^4.0.0"
|
||||
},
|
||||
"pkgid": "synth-js@0.1.1"
|
||||
},
|
||||
{
|
||||
"code": "ENOTSUP",
|
||||
"required": {
|
||||
"node": ">=6.9.2",
|
||||
"npm": "^4.0.0"
|
||||
},
|
||||
"pkgid": "synth-js@0.1.1"
|
||||
}
|
||||
]
|
||||
}
|
345
ProjectNow/NodeServer/node_modules/synth-js/src/midi.js
generated
vendored
Normal file
345
ProjectNow/NodeServer/node_modules/synth-js/src/midi.js
generated
vendored
Normal file
@@ -0,0 +1,345 @@
|
||||
'use strict';
|
||||
|
||||
class MIDIStream {
|
||||
constructor(buffer) {
|
||||
this.data = new Uint8Array(buffer);
|
||||
this.byteOffset = 0;
|
||||
this.lastEventTypeByte = 0x00;
|
||||
}
|
||||
|
||||
readString(byteLength) {
|
||||
var byteOffset = this.byteOffset;
|
||||
|
||||
for (var i = 0, str = ''; i < byteLength; i++) {
|
||||
str += String.fromCharCode(this.data[byteOffset + i]);
|
||||
}
|
||||
|
||||
this.byteOffset += byteLength;
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
readUint32() {
|
||||
var byteOffset = this.byteOffset;
|
||||
var value = (
|
||||
(this.data[byteOffset ] << 24) |
|
||||
(this.data[byteOffset + 1] << 16) |
|
||||
(this.data[byteOffset + 2] << 8) |
|
||||
(this.data[byteOffset + 3] )
|
||||
);
|
||||
|
||||
this.byteOffset += 4;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
readUint24() {
|
||||
var byteOffset = this.byteOffset;
|
||||
var value = (
|
||||
(this.data[byteOffset ] << 16) |
|
||||
(this.data[byteOffset + 1] << 8) |
|
||||
(this.data[byteOffset + 2] )
|
||||
);
|
||||
|
||||
this.byteOffset += 3;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
readUint16() {
|
||||
var byteOffset = this.byteOffset;
|
||||
var value = (
|
||||
(this.data[byteOffset ] << 8) |
|
||||
(this.data[byteOffset + 1] )
|
||||
);
|
||||
|
||||
this.byteOffset += 2;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
readUint8() {
|
||||
var byteOffset = this.byteOffset;
|
||||
var value = this.data[byteOffset];
|
||||
|
||||
this.byteOffset += 1;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
readInt8() {
|
||||
var byteOffset = this.byteOffset;
|
||||
var value = this.data[byteOffset];
|
||||
|
||||
if (value & 0x80 === 0x80) {
|
||||
value ^= 0xFFFFFF00;
|
||||
}
|
||||
|
||||
this.byteOffset += 1;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
readVarUint() {
|
||||
var value = 0;
|
||||
var uint8;
|
||||
|
||||
do {
|
||||
uint8 = this.readUint8();
|
||||
value = (value << 7) + (uint8 & 0x7F);
|
||||
} while ((uint8 & 0x80) === 0x80);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
skip(byteLength) {
|
||||
this.byteOffset += byteLength;
|
||||
}
|
||||
|
||||
readChunk() {
|
||||
var id = this.readString(4);
|
||||
var length = this.readUint32();
|
||||
var byteOffset = this.byteOffset;
|
||||
|
||||
this.byteOffset += length;
|
||||
|
||||
var data = this.data.slice(byteOffset, this.byteOffset);
|
||||
|
||||
return {
|
||||
id: id,
|
||||
length: length,
|
||||
data: data.buffer
|
||||
};
|
||||
}
|
||||
|
||||
readEvent() {
|
||||
var event = {};
|
||||
|
||||
event.delta = this.readVarUint();
|
||||
|
||||
var eventTypeByte = this.readUint8();
|
||||
|
||||
// system event
|
||||
if ((eventTypeByte & 0xF0) === 0xF0) {
|
||||
switch (eventTypeByte) {
|
||||
// meta event
|
||||
case 0xFF:
|
||||
event.type = 'meta';
|
||||
|
||||
var subTypeByte = this.readUint8();
|
||||
var length = this.readVarUint();
|
||||
|
||||
switch (subTypeByte) {
|
||||
case 0x00:
|
||||
event.subType = 'sequenceNumber';
|
||||
if (length === 2)
|
||||
event.value = this.readUint16();
|
||||
else
|
||||
this.skip(length);
|
||||
break;
|
||||
case 0x01:
|
||||
event.subType = 'text';
|
||||
event.value = this.readString(length);
|
||||
break;
|
||||
case 0x02:
|
||||
event.subType = 'copyrightNotice';
|
||||
event.value = this.readString(length);
|
||||
break;
|
||||
case 0x03:
|
||||
event.subType = 'trackName';
|
||||
event.value = this.readString(length);
|
||||
break;
|
||||
case 0x04:
|
||||
event.subType = 'instrumentName';
|
||||
event.value = this.readString(length);
|
||||
break;
|
||||
case 0x05:
|
||||
event.subType = 'lyrics';
|
||||
event.value = this.readString(length);
|
||||
break;
|
||||
case 0x06:
|
||||
event.subType = 'marker';
|
||||
event.value = this.readString(length);
|
||||
break;
|
||||
case 0x07:
|
||||
event.subType = 'cuePoint';
|
||||
event.value = this.readString(length);
|
||||
break;
|
||||
case 0x20:
|
||||
event.subType = 'midiChannelPrefix';
|
||||
if (length === 1)
|
||||
event.value = this.readUint8();
|
||||
else
|
||||
this.skip(length);
|
||||
break;
|
||||
case 0x2F:
|
||||
event.subType = 'endOfTrack';
|
||||
if (length > 0)
|
||||
this.skip(length);
|
||||
break;
|
||||
case 0x51:
|
||||
event.subType = 'setTempo';
|
||||
if (length === 3)
|
||||
event.value = this.readUint24();
|
||||
else
|
||||
this.skip(length)
|
||||
break;
|
||||
case 0x54:
|
||||
event.subType = 'smpteOffset';
|
||||
if (length === 5) {
|
||||
var hourByte = this.readUint8();
|
||||
event.value = {
|
||||
frameRate: ({
|
||||
0x00: 24,
|
||||
0x01: 25,
|
||||
0x02: 29.97,
|
||||
0x03: 30
|
||||
}[hourByte >>> 6]),
|
||||
hour: (hourByte & 0x3F),
|
||||
minute: this.readUint8(),
|
||||
second: this.readUint8(),
|
||||
frame: this.readUint8(),
|
||||
subFrame: this.readUint8()
|
||||
};
|
||||
} else {
|
||||
this.skip(length);
|
||||
}
|
||||
break;
|
||||
case 0x58:
|
||||
event.subType = 'timeSignature';
|
||||
if (length === 4) {
|
||||
event.value = {
|
||||
numerator: this.readUint8(),
|
||||
denominator: 1 << this.readUint8(),
|
||||
metronome: this.readUint8(),
|
||||
thirtyseconds: this.readUint8()
|
||||
};
|
||||
} else {
|
||||
this.skip(length);
|
||||
}
|
||||
break;
|
||||
case 0x59:
|
||||
event.subType = 'keySignature';
|
||||
if (length === 2) {
|
||||
event.value = {
|
||||
key: this.readInt8(),
|
||||
scale: this.readUint8()
|
||||
};
|
||||
} else {
|
||||
this.skip(length);
|
||||
}
|
||||
break;
|
||||
case 0x7F:
|
||||
event.subType = 'sequencerSpecific';
|
||||
event.value = this.readString(length);
|
||||
break;
|
||||
default:
|
||||
event.subType = 'unknown';
|
||||
event.value = this.readString(length);
|
||||
}
|
||||
break;
|
||||
// sysex event
|
||||
case 0xF0:
|
||||
event.type = 'sysEx';
|
||||
|
||||
var length = this.readVarUint();
|
||||
|
||||
event.value = this.readString(length);
|
||||
|
||||
break;
|
||||
case 0xF7:
|
||||
event.type = 'dividedSysEx';
|
||||
|
||||
var length = this.readVarUint();
|
||||
|
||||
event.value = this.readString(length);
|
||||
|
||||
break;
|
||||
default:
|
||||
event.type = 'unknown';
|
||||
|
||||
var length = this.readVarUint();
|
||||
|
||||
event.value = this.readString(length);
|
||||
}
|
||||
// channel event
|
||||
} else {
|
||||
var param;
|
||||
|
||||
// if the high bit is low
|
||||
// use running event type mode
|
||||
if ((eventTypeByte & 0x80) === 0x00) {
|
||||
param = eventTypeByte;
|
||||
eventTypeByte = this.lastEventTypeByte;
|
||||
} else {
|
||||
param = this.readUint8();
|
||||
this.lastEventTypeByte = eventTypeByte;
|
||||
}
|
||||
|
||||
var eventType = eventTypeByte >> 4;
|
||||
|
||||
event.channel = eventTypeByte & 0x0F;
|
||||
event.type = 'channel';
|
||||
|
||||
switch (eventType) {
|
||||
case 0x08:
|
||||
event.subType = 'noteOff';
|
||||
|
||||
event.value = {
|
||||
noteNumber: param,
|
||||
velocity: this.readUint8()
|
||||
};
|
||||
break;
|
||||
case 0x09:
|
||||
event.value = {
|
||||
noteNumber: param,
|
||||
velocity: this.readUint8()
|
||||
};
|
||||
|
||||
// some midi implementations use a noteOn
|
||||
// event with 0 velocity to denote noteOff
|
||||
if (event.value.velocity === 0) {
|
||||
event.subType = 'noteOff';
|
||||
} else {
|
||||
event.subType = 'noteOn';
|
||||
}
|
||||
break;
|
||||
case 0x0A:
|
||||
event.subType = 'noteAftertouch';
|
||||
|
||||
event.value = {
|
||||
noteNumber: param,
|
||||
amount: this.readUint8()
|
||||
};
|
||||
break;
|
||||
case 0x0B:
|
||||
event.subType = 'controller';
|
||||
|
||||
event.value = {
|
||||
controllerNumber: param,
|
||||
controllerValue: this.readUint8()
|
||||
};
|
||||
break;
|
||||
case 0x0C:
|
||||
event.subType = 'programChange';
|
||||
event.value = param;
|
||||
break;
|
||||
case 0x0D:
|
||||
event.subType = 'channelAftertouch';
|
||||
event.value = param;
|
||||
break;
|
||||
case 0x0E:
|
||||
event.subType = 'pitchBend';
|
||||
event.value = param + (this.readUint8() << 7);
|
||||
break;
|
||||
default:
|
||||
event.subType = 'unknown';
|
||||
event.value = (param << 8) + this.readUint8();
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = MIDIStream;
|
231
ProjectNow/NodeServer/node_modules/synth-js/src/midi2wav.js
generated
vendored
Normal file
231
ProjectNow/NodeServer/node_modules/synth-js/src/midi2wav.js
generated
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
'use strict';
|
||||
|
||||
const WAV = require('./wav');
|
||||
const MIDIStream = require('./midi');
|
||||
const Timer = require('./utils/timer');
|
||||
|
||||
module.exports = function midiToWav(buffer, args = {}) {
|
||||
if (args.verbose) {
|
||||
console.log('parsing MIDI header...');
|
||||
}
|
||||
|
||||
const midiStream = new MIDIStream(buffer);
|
||||
const header = midiStream.readChunk();
|
||||
|
||||
if (header.id !== 'MThd' || header.length !== 6) {
|
||||
throw new SyntaxError('malformed header');
|
||||
}
|
||||
|
||||
const headerStream = new MIDIStream(header.data);
|
||||
const formatType = headerStream.readUint16();
|
||||
const trackCount = headerStream.readUint16();
|
||||
const timeDivision = headerStream.readUint16();
|
||||
const tracks = [];
|
||||
const progression = [];
|
||||
const events = [];
|
||||
let maxAmplitude;
|
||||
|
||||
for (let i = 0; i < trackCount; i++) {
|
||||
if (args.verbose) {
|
||||
console.log(`parsing track ${i + 1}...`);
|
||||
}
|
||||
|
||||
const trackChunk = midiStream.readChunk();
|
||||
|
||||
if (trackChunk.id !== 'MTrk') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const trackStream = new MIDIStream(trackChunk.data);
|
||||
const track = [];
|
||||
let keep = true;
|
||||
|
||||
// determine whether applied filter will remove the current track while populating it
|
||||
while (keep && trackStream.byteOffset < trackChunk.length) {
|
||||
let event = trackStream.readEvent();
|
||||
track.push(event);
|
||||
|
||||
if (typeof event.value === 'string') {
|
||||
if (args.verbose) {
|
||||
console.log(`{"${event.subType}":"${event.value}"}`);
|
||||
}
|
||||
|
||||
if (Array.isArray(args.Skip)) {
|
||||
for (let t = 0; t < args.Skip.length; t++) {
|
||||
if (args.Skip[t][event.subType] === event.value) {
|
||||
if (args.verbose) {
|
||||
console.log(`skip match found: {"${event.subType}":"${event.value}"}`);
|
||||
}
|
||||
|
||||
keep = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof args.Skip === 'function') {
|
||||
keep = !args.Skip(track);
|
||||
}
|
||||
|
||||
if (keep) {
|
||||
tracks.push(track);
|
||||
} else if (args.verbose) {
|
||||
console.log(`skipping track ${i + 1}...`);
|
||||
}
|
||||
}
|
||||
|
||||
if (timeDivision >>> 15 === 0) {
|
||||
// use microseconds per beat
|
||||
const timer = new Timer(timeDivision);
|
||||
|
||||
if (args.verbose) {
|
||||
console.log('initializing timer...');
|
||||
}
|
||||
|
||||
// set up timer with setTempo events
|
||||
for (let i = 0, delta = 0, ticks = 0, event; i < tracks[0].length; i++) {
|
||||
event = tracks[0][i];
|
||||
delta += event.delta;
|
||||
ticks += event.delta;
|
||||
|
||||
if (event.subType === 'setTempo') {
|
||||
timer.addCriticalPoint(delta, event.value);
|
||||
delta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// generate note data
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (args.verbose) {
|
||||
console.log(`generating progression from track ${i + 1}...`);
|
||||
}
|
||||
|
||||
let track = tracks[i];
|
||||
let delta = 0;
|
||||
let map = new Map();
|
||||
|
||||
for (let j = 0; j < track.length; j++) {
|
||||
let event = track[j];
|
||||
delta += event.delta;
|
||||
|
||||
if (event.type === 'channel') {
|
||||
const semitone = event.value.noteNumber;
|
||||
|
||||
if (event.subType === 'noteOn') {
|
||||
let velocity = event.value.velocity;
|
||||
let offset = timer.getTime(delta);
|
||||
|
||||
// use stack for simultaneous identical notes
|
||||
if (map.has(semitone)) {
|
||||
map.get(semitone).push({offset, velocity});
|
||||
} else {
|
||||
map.set(semitone, [{offset, velocity}]);
|
||||
}
|
||||
|
||||
// to determine maximum total velocity for normalizing volume
|
||||
events.push({velocity, delta, note: true});
|
||||
} else if (event.subType === 'noteOff') {
|
||||
let note = map.get(semitone).pop();
|
||||
|
||||
progression.push({
|
||||
note: WAV.note(semitone),
|
||||
time: timer.getTime(delta) - note.offset,
|
||||
amplitude: note.velocity / 128,
|
||||
offset: note.offset,
|
||||
});
|
||||
|
||||
// to determine maximum total velocity for normalizing volume
|
||||
events.push({velocity: note.velocity, delta, note: false});
|
||||
}
|
||||
} else if (args.verbose && event.type === 'meta') {
|
||||
if (typeof event.value === 'string') {
|
||||
console.log(`${timer.getTime(delta).toFixed(2)}s ${event.subType}: ${event.value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
console.log('normalizing volume...');
|
||||
}
|
||||
|
||||
events.sort(function (a, b) {
|
||||
return a.delta - b.delta || a.note - b.note;
|
||||
});
|
||||
|
||||
if (args.verbose) {
|
||||
console.log('total notes:', progression.length);
|
||||
console.log('total time:', timer.getTime(events[events.length - 1].delta), 'seconds');
|
||||
}
|
||||
|
||||
let maxVelocity = 1;
|
||||
let maxVelocityTime = 0;
|
||||
let velocity = 1;
|
||||
let maxChord = 0;
|
||||
let maxChordTime = 0;
|
||||
let chord = 0;
|
||||
|
||||
for (const event of events) {
|
||||
if (event.note) {
|
||||
velocity += event.velocity;
|
||||
chord++;
|
||||
|
||||
if (velocity > maxVelocity) {
|
||||
maxVelocity = velocity;
|
||||
maxVelocityTime = timer.getTime(event.delta);
|
||||
}
|
||||
|
||||
if (chord > maxChord) {
|
||||
maxChord = chord;
|
||||
maxChordTime = timer.getTime(event.delta);
|
||||
}
|
||||
} else {
|
||||
velocity -= event.velocity;
|
||||
chord--;
|
||||
}
|
||||
}
|
||||
|
||||
// scaling factor for amplitude
|
||||
maxAmplitude = 128 / maxVelocity;
|
||||
|
||||
if (args.verbose) {
|
||||
console.log('setting volume to', maxAmplitude);
|
||||
console.log(' maximum chord of', maxChord, 'at', maxChordTime, 'seconds');
|
||||
console.log(' maximum velocity of', maxVelocity - 1, 'at', maxVelocityTime, 'seconds');
|
||||
}
|
||||
} else {
|
||||
// use frames per second
|
||||
// not yet implemented
|
||||
|
||||
console.log('Detected unsupported MIDI timing mode');
|
||||
|
||||
return null;
|
||||
|
||||
/*
|
||||
let framesPerSecond = (division >>> 8) & 0x7F;
|
||||
let ticksPerFrame = division & 0xFF;
|
||||
|
||||
if (framesPerSecond === 29) {
|
||||
framesPerSecond = 29.97;
|
||||
}
|
||||
|
||||
// seconds per tick = 1 / frames per second / ticks per frame
|
||||
secsPerTick = 1 / framesPerSecond / ticksPerFrame;
|
||||
*/
|
||||
}
|
||||
|
||||
// set to mono
|
||||
args.channels = 1;
|
||||
|
||||
if (args.verbose) {
|
||||
console.log('generating WAV buffer...');
|
||||
}
|
||||
|
||||
const wav = new WAV(args.channels, args.sampleRate, args.bitsPerSample);
|
||||
|
||||
wav.writeProgression(progression, maxAmplitude, [0], true, true, args.duration);
|
||||
|
||||
return wav;
|
||||
};
|
47
ProjectNow/NodeServer/node_modules/synth-js/src/utils/timer.js
generated
vendored
Normal file
47
ProjectNow/NodeServer/node_modules/synth-js/src/utils/timer.js
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
// utility class to calculate time from delta ticks
|
||||
// when MIDI file has several `setTempo` events
|
||||
class Timer {
|
||||
constructor(ticksPerBeat) {
|
||||
this.ticksPerBeat = ticksPerBeat;
|
||||
this.criticalPoints = [];
|
||||
}
|
||||
|
||||
// delta represents ticks since last time change
|
||||
addCriticalPoint(delta, microsecondsPerBeat) {
|
||||
this.criticalPoints.push({
|
||||
delta,
|
||||
microsecondsPerBeat
|
||||
});
|
||||
}
|
||||
|
||||
getTime(delta) {
|
||||
const microsecondsPerSecond = 1000000;
|
||||
let time = 0;
|
||||
// midi standard initializes file with this value
|
||||
let microsecondsPerBeat = 500000;
|
||||
|
||||
// iterate through time changes while decrementing delta ticks to 0
|
||||
for (let i = 0, criticalPoint; i < this.criticalPoints.length && delta > 0; i++) {
|
||||
criticalPoint = this.criticalPoints[i];
|
||||
|
||||
// incrementally calculate the time passed for each range of timing
|
||||
if (delta >= criticalPoint.delta) {
|
||||
time += criticalPoint.delta * microsecondsPerBeat / this.ticksPerBeat / microsecondsPerSecond;
|
||||
delta -= criticalPoint.delta;
|
||||
} else {
|
||||
time += delta * microsecondsPerBeat / this.ticksPerBeat / microsecondsPerSecond;
|
||||
delta = 0;
|
||||
}
|
||||
|
||||
microsecondsPerBeat = criticalPoint.microsecondsPerBeat;
|
||||
}
|
||||
|
||||
time += delta * microsecondsPerBeat / this.ticksPerBeat / microsecondsPerSecond;
|
||||
|
||||
return time;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Timer;
|
508
ProjectNow/NodeServer/node_modules/synth-js/src/wav.js
generated
vendored
Normal file
508
ProjectNow/NodeServer/node_modules/synth-js/src/wav.js
generated
vendored
Normal file
@@ -0,0 +1,508 @@
|
||||
'use strict';
|
||||
|
||||
class WAV {
|
||||
static semitone(note = 'REST') {
|
||||
// matches occurence of A through G
|
||||
// followed by positive or negative integer
|
||||
// followed by 0 to 2 occurences of flat or sharp
|
||||
const re = /^([A-G])(\-?\d+)(b{0,2}|#{0,2})$/;
|
||||
|
||||
// if semitone is unrecognized, assume REST
|
||||
if (!re.test(note)) {
|
||||
return -Infinity;
|
||||
}
|
||||
|
||||
// parse substrings of note
|
||||
const [, tone, octave, accidental] = note.match(re);
|
||||
|
||||
// semitone indexed relative to A4 == 69 for compatibility with MIDI
|
||||
const tones = {C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11};
|
||||
const octaves = {'-1': 0, 0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10, 10: 11};
|
||||
const accidentals = {bb: -2, b: -1, '': 0, '#': 1, '##': 2};
|
||||
|
||||
// if semitone is unrecognized, assume REST
|
||||
if (tones[tone] === undefined || octaves[octave] === undefined || accidentals[accidental] === undefined) {
|
||||
return -Infinity;
|
||||
}
|
||||
|
||||
// return calculated index
|
||||
return tones[tone] + octaves[octave] * 12 + accidentals[accidental];
|
||||
}
|
||||
|
||||
static note(semitone = -Infinity) {
|
||||
const octaves = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
const tones = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
|
||||
const octaveIndex = Math.floor(semitone / 12);
|
||||
const toneIndex = Math.floor(semitone - octaveIndex * 12);
|
||||
|
||||
const octave = octaves[octaveIndex];
|
||||
const tone = tones[toneIndex];
|
||||
|
||||
// by default assume REST
|
||||
if (octave === undefined || tone === undefined) {
|
||||
return 'REST';
|
||||
}
|
||||
|
||||
// tone followed by octave followed by accidental
|
||||
return tone.charAt(0) + octave.toString() + tone.charAt(1);
|
||||
}
|
||||
|
||||
// converts semitone index to frequency in Hz
|
||||
static frequency(semitone = -Infinity) {
|
||||
// A4 is 440 Hz, 12 semitones per octave
|
||||
return 440 * Math.pow(2, (semitone - 69) / 12);
|
||||
}
|
||||
|
||||
constructor(numChannels = 1, sampleRate = 44100, bitsPerSample = 16, littleEndian = true, data = []) {
|
||||
var bytesPerSample = bitsPerSample >>> 3;
|
||||
// WAV header is always 44 bytes
|
||||
this.header = new ArrayBuffer(44);
|
||||
// flexible container for reading / writing raw bytes in header
|
||||
this.view = new DataView(this.header);
|
||||
// leave sound data as non typed array for more flexibility
|
||||
this.data = data;
|
||||
|
||||
// initialize as non-configurable because it
|
||||
// causes script to freeze when using parsed
|
||||
// chunk sizes with wrong endianess assumed
|
||||
Object.defineProperty(this, 'littleEndian', {
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
value: littleEndian,
|
||||
writable: false
|
||||
});
|
||||
|
||||
// initial write index in data array
|
||||
this.pointer = 0;
|
||||
|
||||
// WAV header properties
|
||||
this.ChunkID = littleEndian ? 'RIFF' : 'RIFX';
|
||||
this.ChunkSize = this.header.byteLength - 8;
|
||||
this.Format = 'WAVE';
|
||||
this.SubChunk1ID = 'fmt ';
|
||||
this.SubChunk1Size = 16;
|
||||
this.AudioFormat = 1;
|
||||
this.NumChannels = numChannels;
|
||||
this.SampleRate = sampleRate;
|
||||
this.ByteRate = numChannels * sampleRate * bytesPerSample;
|
||||
this.BlockAlign = numChannels * bytesPerSample;
|
||||
this.BitsPerSample = bitsPerSample;
|
||||
this.SubChunk2ID = 'data';
|
||||
this.SubChunk2Size = data.length * bytesPerSample;
|
||||
}
|
||||
|
||||
// internal setter for writing strings as raw bytes to header
|
||||
setString(str, byteLength = str.length, byteOffset = 0) {
|
||||
for (var i = 0; i < byteLength; i++) {
|
||||
this.view.setUint8(byteOffset + i, str.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
// internal getter for reading raw bytes as strings from header
|
||||
getString(byteLength, byteOffset = 0) {
|
||||
for (var i = 0, str = ''; i < byteLength; i++) {
|
||||
str += String.fromCharCode(this.view.getUint8(byteOffset + i));
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
// header property mutators
|
||||
|
||||
// 4 bytes at offset of 0 bytes
|
||||
set ChunkID(str) {
|
||||
this.setString(str, 4, 0);
|
||||
}
|
||||
|
||||
get ChunkID() {
|
||||
return this.getString(4, 0);
|
||||
}
|
||||
|
||||
// 4 bytes at offset of 4 bytes
|
||||
set ChunkSize(uint) {
|
||||
this.view.setUint32(4, uint, this.littleEndian);
|
||||
}
|
||||
|
||||
get ChunkSize() {
|
||||
return this.view.getUint32(4, this.littleEndian);
|
||||
}
|
||||
|
||||
// 4 bytes at offset of 8 bytes
|
||||
set Format(str) {
|
||||
this.setString(str, 4, 8);
|
||||
}
|
||||
|
||||
get Format() {
|
||||
return this.getString(4, 8);
|
||||
}
|
||||
|
||||
// 4 bytes at offset of 12 bytes
|
||||
set SubChunk1ID(str) {
|
||||
this.setString(str, 4, 12);
|
||||
}
|
||||
|
||||
get SubChunk1ID() {
|
||||
return this.getString(4, 12);
|
||||
}
|
||||
|
||||
// 4 bytes at offset of 16 bytes
|
||||
set SubChunk1Size(uint) {
|
||||
this.view.setUint32(16, uint, this.littleEndian);
|
||||
}
|
||||
|
||||
get SubChunk1Size() {
|
||||
return this.view.getUint32(16, this.littleEndian);
|
||||
}
|
||||
|
||||
// 2 bytes at offset of 20 bytes
|
||||
set AudioFormat(uint) {
|
||||
this.view.setUint16(20, uint, this.littleEndian);
|
||||
}
|
||||
|
||||
get AudioFormat() {
|
||||
return this.view.getUint16(20, this.littleEndian);
|
||||
}
|
||||
|
||||
// 2 bytes at offset of 22 bytes
|
||||
set NumChannels(uint) {
|
||||
this.view.setUint16(22, uint, this.littleEndian);
|
||||
}
|
||||
|
||||
get NumChannels() {
|
||||
return this.view.getUint16(22, this.littleEndian);
|
||||
}
|
||||
|
||||
// 4 bytes at offset of 24 bytes
|
||||
set SampleRate(uint) {
|
||||
this.view.setUint32(24, uint, this.littleEndian);
|
||||
}
|
||||
|
||||
get SampleRate() {
|
||||
return this.view.getUint32(24, this.littleEndian);
|
||||
}
|
||||
|
||||
// 4 bytes at offset of 28 bytes
|
||||
set ByteRate(uint) {
|
||||
this.view.setUint32(28, uint, this.littleEndian);
|
||||
}
|
||||
|
||||
get ByteRate() {
|
||||
return this.view.getUint32(28, this.littleEndian);
|
||||
}
|
||||
|
||||
// 2 bytes at offset of 32 bytes
|
||||
set BlockAlign(uint) {
|
||||
this.view.setUint16(32, uint, this.littleEndian);
|
||||
}
|
||||
|
||||
get BlockAlign() {
|
||||
return this.view.getUint16(32, this.littleEndian);
|
||||
}
|
||||
|
||||
// 2 bytes at offset of 34 bytes
|
||||
set BitsPerSample(uint) {
|
||||
this.view.setUint16(34, uint, this.littleEndian);
|
||||
}
|
||||
|
||||
get BitsPerSample() {
|
||||
return this.view.getUint16(34, this.littleEndian);
|
||||
}
|
||||
|
||||
// 4 bytes at offset of 36 bytes
|
||||
set SubChunk2ID(str) {
|
||||
this.setString(str, 4, 36);
|
||||
}
|
||||
|
||||
get SubChunk2ID() {
|
||||
return this.getString(4, 36);
|
||||
}
|
||||
|
||||
// 4 bytes at offset of 40 bytes
|
||||
set SubChunk2Size(uint) {
|
||||
this.view.setUint32(40, uint, this.littleEndian);
|
||||
}
|
||||
|
||||
get SubChunk2Size() {
|
||||
return this.view.getUint32(40, this.littleEndian);
|
||||
}
|
||||
|
||||
// internal getter for sound data as
|
||||
// typed array based on header properties
|
||||
get typedData() {
|
||||
var bytesPerSample = this.BitsPerSample >>> 3;
|
||||
var data = this.data;
|
||||
var size = this.SubChunk2Size;
|
||||
var samples = size / bytesPerSample;
|
||||
var buffer = new ArrayBuffer(size);
|
||||
var uint8 = new Uint8Array(buffer);
|
||||
|
||||
// convert signed normalized sound data to typed integer data
|
||||
// i.e. [-1, 1] -> [INT_MIN, INT_MAX]
|
||||
var amplitude = Math.pow(2, (bytesPerSample << 3) - 1) - 1;
|
||||
var i, d;
|
||||
|
||||
switch (bytesPerSample) {
|
||||
case 1:
|
||||
// endianess not relevant for 8-bit encoding
|
||||
for (i = 0; i < samples; i++) {
|
||||
// convert by adding 0x80 instead of 0x100
|
||||
// WAV uses unsigned data for 8-bit encoding
|
||||
|
||||
// [INT8_MIN, INT8_MAX] -> [0, UINT8_MAX]
|
||||
uint8[i] = (data[i] * amplitude + 0x80) & 0xFF;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
// LSB first
|
||||
if (this.littleEndian) {
|
||||
for (i = 0; i < samples; i++) {
|
||||
// [INT16_MIN, INT16_MAX] -> [0, UINT16_MAX]
|
||||
d = (data[i] * amplitude + 0x10000) & 0xFFFF;
|
||||
|
||||
// unwrap inner loop
|
||||
uint8[i * 2 ] = (d ) & 0xFF;
|
||||
uint8[i * 2 + 1] = (d >>> 8);
|
||||
}
|
||||
// MSB first
|
||||
} else {
|
||||
for (i = 0; i < samples; i++) {
|
||||
// [INT16_MIN, INT16_MAX] -> [0, UINT16_MAX]
|
||||
d = (data[i] * amplitude + 0x10000) & 0xFFFF;
|
||||
|
||||
// unwrap inner loop
|
||||
uint8[i * 2 ] = (d >>> 8);
|
||||
uint8[i * 2 + 1] = (d ) & 0xFF;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
// LSB first
|
||||
if (this.littleEndian) {
|
||||
for (i = 0; i < samples; i++) {
|
||||
// [INT24_MIN, INT24_MAX] -> [0, UINT24_MAX]
|
||||
d = (data[i] * amplitude + 0x1000000) & 0xFFFFFF;
|
||||
|
||||
// unwrap inner loop
|
||||
uint8[i * 3 ] = (d ) & 0xFF;
|
||||
uint8[i * 3 + 1] = (d >>> 8) & 0xFF;
|
||||
uint8[i * 3 + 2] = (d >>> 16);
|
||||
}
|
||||
// MSB first
|
||||
} else {
|
||||
for (i = 0; i < samples; i++) {
|
||||
// [INT24_MIN, INT24_MAX] -> [0, UINT24_MAX]
|
||||
d = (data[i] * amplitude + 0x1000000) & 0xFFFFFF;
|
||||
|
||||
// unwrap inner loop
|
||||
uint8[i * 3 ] = (d >>> 16);
|
||||
uint8[i * 3 + 1] = (d >>> 8) & 0xFF;
|
||||
uint8[i * 3 + 2] = (d ) & 0xFF;
|
||||
}
|
||||
}
|
||||
case 4:
|
||||
// LSB first
|
||||
if (this.littleEndian) {
|
||||
for (i = 0; i < samples; i++) {
|
||||
// [INT32_MIN, INT32_MAX] -> [0, UINT32_MAX]
|
||||
d = (data[i] * amplitude + 0x100000000) & 0xFFFFFFFF;
|
||||
|
||||
// unwrap inner loop
|
||||
uint8[i * 4 ] = (d ) & 0xFF;
|
||||
uint8[i * 4 + 1] = (d >>> 8) & 0xFF;
|
||||
uint8[i * 4 + 2] = (d >>> 16) & 0xFF;
|
||||
uint8[i * 4 + 3] = (d >>> 24);
|
||||
}
|
||||
// MSB first
|
||||
} else {
|
||||
for (i = 0; i < samples; i++) {
|
||||
// [INT32_MIN, INT32_MAX] -> [0, UINT32_MAX]
|
||||
d = (data[i] * amplitude + 0x100000000) & 0xFFFFFFFF;
|
||||
|
||||
// unwrap inner loop
|
||||
uint8[i * 4 ] = (d >>> 24);
|
||||
uint8[i * 4 + 1] = (d >>> 16) & 0xFF;
|
||||
uint8[i * 4 + 2] = (d >>> 8) & 0xFF;
|
||||
uint8[i * 4 + 3] = (d ) & 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// binary container outputs
|
||||
|
||||
// browser-specific
|
||||
// generates blob from concatenated typed arrays
|
||||
toBlob() {
|
||||
return new Blob([this.header, this.typedData], {type: 'audio/wav'});
|
||||
}
|
||||
|
||||
// Node.js-specific
|
||||
// generates buffer from concatenated typed arrays
|
||||
toBuffer() {
|
||||
return Buffer.concat([Buffer.from(this.header), Buffer.from(this.typedData)]);
|
||||
}
|
||||
|
||||
// pointer mutators
|
||||
|
||||
// gets time (in seconds) of pointer
|
||||
tell() {
|
||||
return this.pointer / this.NumChannels / this.SampleRate;
|
||||
}
|
||||
|
||||
// sets time (in seconds) of pointer
|
||||
// zero-fills by default
|
||||
seek(time, fill = true) {
|
||||
var data = this.data;
|
||||
var sample = Math.round(this.SampleRate * time);
|
||||
|
||||
this.pointer = this.NumChannels * sample;
|
||||
|
||||
if (fill) {
|
||||
// zero-fill seek
|
||||
while (data.length < this.pointer) {
|
||||
data[data.length] = 0;
|
||||
}
|
||||
} else {
|
||||
this.pointer = data.length;
|
||||
}
|
||||
}
|
||||
|
||||
// sound data mutators
|
||||
|
||||
// writes the specified note to the sound data
|
||||
// for amount of time in seconds
|
||||
// at given normalized amplitude
|
||||
// to channels listed (or all by default)
|
||||
// adds to existing data by default
|
||||
// and does not reset write index after operation by default
|
||||
writeNote({note, time, amplitude = 1}, channels = [], blend = true, reset = false) {
|
||||
// creating local references to properties
|
||||
var data = this.data;
|
||||
var numChannels = this.NumChannels;
|
||||
var sampleRate = this.SampleRate;
|
||||
|
||||
// to prevent sound artifacts
|
||||
const fadeSeconds = 0.001;
|
||||
|
||||
// calculating properties of given note
|
||||
var semitone = WAV.semitone(note);
|
||||
var frequency = WAV.frequency(semitone) * Math.PI * 2 / sampleRate;
|
||||
var period = Math.PI * 2 / frequency;
|
||||
|
||||
// amount of blocks to be written
|
||||
var blocksOut = Math.round(sampleRate * time);
|
||||
// reduces sound artifacts by fading at last fadeSeconds
|
||||
var nonZero = blocksOut - sampleRate * fadeSeconds;
|
||||
// fade interval in samples
|
||||
var fade = blocksOut - nonZero + 1;
|
||||
|
||||
// index of start and stop samples
|
||||
var start = this.pointer;
|
||||
var stop = data.length;
|
||||
|
||||
// determines amount of blocks to be updated
|
||||
var blocksIn = Math.min(Math.floor((stop - start) / numChannels), blocksOut);
|
||||
|
||||
// i = index of each sample block
|
||||
// j = index of each channel in a block
|
||||
// k = cached index of data
|
||||
// d = sample data value
|
||||
var i, j, k, d;
|
||||
|
||||
// by default write to all channels
|
||||
if (channels.length === 0) {
|
||||
// don't overwrite passed array
|
||||
channels = [];
|
||||
|
||||
for (i = 0; i < numChannels; i++) {
|
||||
channels[i] = i;
|
||||
}
|
||||
}
|
||||
|
||||
// inline .indexOf() function calls into array references
|
||||
var skipChannel = [];
|
||||
|
||||
for (i = 0; i < numChannels; i++) {
|
||||
skipChannel[i] = (channels.indexOf(i) === -1);
|
||||
}
|
||||
|
||||
// update existing data
|
||||
for (i = 0; i < blocksIn; i++) {
|
||||
// iterate through specified channels
|
||||
for (j = 0; j < channels.length; j++) {
|
||||
k = start + i * numChannels + channels[j];
|
||||
d = 0;
|
||||
|
||||
if (frequency > 0) {
|
||||
d = amplitude * Math.sin(frequency * i) * ((i < fade) ? i : (i > nonZero) ? blocksOut - i + 1 : fade) / fade;
|
||||
}
|
||||
|
||||
data[k] = d + (blend ? data[k] : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// append data
|
||||
for (i = blocksIn; i < blocksOut; i++) {
|
||||
k = start + i * numChannels;
|
||||
|
||||
// iterate through all channels
|
||||
for (j = 0; j < numChannels; j++) {
|
||||
d = 0;
|
||||
|
||||
// only write non-zero data to specified channels
|
||||
if (frequency > 0 || !skipChannel[j]) {
|
||||
d = amplitude * Math.sin(frequency * i) * ((i < fade) ? i : (i > nonZero) ? blocksOut - i + 1 : fade) / fade;
|
||||
}
|
||||
|
||||
data[k + j] = d;
|
||||
}
|
||||
}
|
||||
|
||||
// update header properties
|
||||
var end = Math.max(start + blocksOut * numChannels, stop) * this.BitsPerSample >>> 3;
|
||||
|
||||
this.ChunkSize = end + this.header.byteLength - 8;
|
||||
this.SubChunk2Size = end;
|
||||
|
||||
if (!reset) {
|
||||
// move write index to end of written data
|
||||
this.pointer = start + blocksOut * numChannels;
|
||||
}
|
||||
}
|
||||
|
||||
// adds specified notes in series
|
||||
// (or asynchronously if offset property is specified in a note)
|
||||
// each playing for time * relativeDuration seconds
|
||||
// followed by a time * (1 - relativeDuration) second rest
|
||||
writeProgression(notes, amplitude = 1, channels = [], blend = true, reset = false, relativeDuration = 1) {
|
||||
var start = this.pointer;
|
||||
|
||||
for (var i = 0, note, time, amp, off, secs, rest; i < notes.length; i++) {
|
||||
({note, time, amplitude: amp, offset: off} = notes[i]);
|
||||
|
||||
// for asynchronous progression
|
||||
if (off !== undefined) {
|
||||
this.seek(off);
|
||||
}
|
||||
|
||||
if (relativeDuration === 1 || note === 'REST') {
|
||||
this.writeNote({note, time, amplitude: amp === undefined ? amplitude : amp * amplitude}, channels, blend, false);
|
||||
} else {
|
||||
secs = time * relativeDuration;
|
||||
rest = time - secs;
|
||||
|
||||
this.writeNote({note: note, time: secs, amplitude: amp === undefined ? amplitude : amp * amplitude}, channels, blend, false);
|
||||
this.writeNote({note: 'REST', time: rest}, channels, blend, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
this.pointer = start;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = WAV;
|
Reference in New Issue
Block a user