nodejs-audio-visualizer 4.5.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +80 -80
- package/dist/audio.d.ts +12 -12
- package/dist/audio.js +120 -120
- package/dist/beats.d.ts +19 -19
- package/dist/beats.js +43 -43
- package/dist/bpmEncoder.d.ts +14 -14
- package/dist/bpmEncoder.js +97 -97
- package/dist/config.d.ts +77 -77
- package/dist/config.js +369 -369
- package/dist/dsp.d.ts +16 -16
- package/dist/dsp.js +76 -76
- package/dist/highlight.d.ts +32 -32
- package/dist/highlight.js +325 -325
- package/dist/image.d.ts +79 -79
- package/dist/image.js +458 -458
- package/dist/index.d.ts +79 -79
- package/dist/index.js +534 -531
- package/dist/index.js.map +1 -1
- package/dist/subtitleConvert.d.ts +3 -3
- package/dist/subtitleConvert.js +61 -61
- package/dist/video.d.ts +80 -80
- package/dist/video.js +434 -426
- package/dist/video.js.map +1 -1
- package/dist/waitForEventLoop.d.ts +1 -1
- package/dist/waitForEventLoop.js +5 -5
- package/package.json +61 -61
- package/src/audio.ts +120 -120
- package/src/beats.ts +68 -68
- package/src/bpmEncoder.ts +97 -97
- package/src/config.ts +476 -476
- package/src/dsp.ts +95 -95
- package/src/highlight.ts +284 -284
- package/src/image.ts +602 -602
- package/src/index.ts +718 -713
- package/src/subtitleConvert.ts +63 -63
- package/src/tests/audio.test.ts +96 -96
- package/src/tests/config.test.ts +496 -496
- package/src/tests/dsp.test.ts +24 -24
- package/src/tests/highlight.test.ts +178 -178
- package/src/tests/image.test.ts +222 -222
- package/src/tests/index.test.ts +180 -112
- package/src/tests/subtitleConvert.test.ts +23 -23
- package/src/tests/video.test.ts +285 -216
- package/src/typings.d.ts +6 -6
- package/src/video.ts +550 -542
- package/src/waitForEventLoop.ts +2 -2
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2019 VladislavPetyukevich
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 VladislavPetyukevich
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
# nodejs-audio-visualizer
|
|
2
|
-
Audio visualizer generator for Node.js with simple configuration.
|
|
3
|
-
Supports PNG, JPG for images, MP4/MOV/AVI/MKV for background videos, and MP3, WAV for audio.
|
|
4
|
-
|
|
5
|
-
## Example
|
|
6
|
-
```javascript
|
|
7
|
-
const renderAudioVisualizer = require('nodejs-audio-visualizer').renderAudioVisualizer;
|
|
8
|
-
const config = {
|
|
9
|
-
image: { // Optional.
|
|
10
|
-
path: 'media/background.png' // Supports PNG and JPG images. Use either 'image' or 'video', not both.
|
|
11
|
-
},
|
|
12
|
-
// OR use a video as background (optional):
|
|
13
|
-
// video: {
|
|
14
|
-
// path: 'media/background.mp4', // Supports MP4, MOV, AVI, MKV. Loops automatically if shorter than audio.
|
|
15
|
-
// autoEdit: true, // Optional. Detects scene cuts and advances between them on each beat so the background edits with the music; omit or false for one continuous play-through from the start.
|
|
16
|
-
// },
|
|
17
|
-
audio: {
|
|
18
|
-
path: 'media/audio.wav', // Supports MP3 and WAV audio
|
|
19
|
-
autoHighlight: true,
|
|
20
|
-
autoHighlightCount: 3, // Optional. With autoHighlight: how many non-overlapping 15s windows with the highest summed spectral energy to aim for (default 1; must be a whole number ≥ 1). You may get fewer videos if the track is too short or does not have that many valid windows. More than one window produces separate videos next to outVideo.path (e.g. out-1.mp4, out-2.mp4).
|
|
21
|
-
},
|
|
22
|
-
outVideo: {
|
|
23
|
-
path: 'media/out.mp4',
|
|
24
|
-
subtitles: { // Optional.
|
|
25
|
-
path: 'media/subtitles.lrc', // Optional. .srt or .lrc format.
|
|
26
|
-
rawContent: "1\n00:00:00,000 --> 99:59:59,999\nSome Artist - Some Song", // Only if path not specified.
|
|
27
|
-
alignment: "middle", // Optional. Available values: 'top', 'middle', 'bottom'. Default: 'bottom'.
|
|
28
|
-
},
|
|
29
|
-
fps: 25, // Default value: 30
|
|
30
|
-
resolution: { width: 1920, height: 1080 }, // Optional. Output frame size in pixels. Omit to keep the background image or video at its native resolution; when set, video frames are scaled to this size and images are resized (center-cropped first if landscape/portrait does not match the target aspect).
|
|
31
|
-
spectrum: { // Audio spectrum configuration (linear bar visualizer). Optional. If neither 'spectrum' nor 'polar' is specified,
|
|
32
|
-
width: '30%', // 30% of background image width. Default value: 33%. Also you can use absolute values. For example, value 300 for 300 pixels width.
|
|
33
|
-
height: '15%', // 15% of background image height. Default value: 160. Also you can use absolute values. For example, value 300 for 300 pixels height.
|
|
34
|
-
x: 'center', // Available values: 'left', 'center', right'. Also you can use absolute number values. For example, value 300 for 300 pixels x.
|
|
35
|
-
y: 'bottom', // Available values: 'top', 'middle', bottom'. Also you can use absolute number values. For example, value 300 for 300 pixels y.
|
|
36
|
-
rotation: 'mirror', // Available values: 'up', 'down', 'mirror'. Default value: 'mirror'.
|
|
37
|
-
effect: 'volume', // Available values: 'volume', 'smooth', undefined. Default value: undefined.
|
|
38
|
-
color: '#cccc99', // Default value: inverted color of background image
|
|
39
|
-
opacity: '70%' // Default value: '80%'.
|
|
40
|
-
},
|
|
41
|
-
polar: { // Polar/circular audio spectrum configuration
|
|
42
|
-
x: 'center', // Available values: 'left', 'center', 'right'. Also you can use absolute number values. Default value: 'center'.
|
|
43
|
-
y: 'middle', // Available values: 'top', 'middle', 'bottom'. Also you can use absolute number values. Default value: 'middle'.
|
|
44
|
-
innerRadius: 100, // Inner radius of the circular spectrum in pixels. Default value: 100.
|
|
45
|
-
maxBarLength: 160, // Maximum bar length in pixels. Default value: 160.
|
|
46
|
-
barWidth: 15, // Width of each bar in pixels. Default value: 15.
|
|
47
|
-
effect: 'volume', // Available values: 'volume', 'smooth', undefined. Default value: undefined.
|
|
48
|
-
color: '#cccc99', // Default value: inverted color of background image
|
|
49
|
-
opacity: '70%' // Default value: '80%'.
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
tweaks: { // Optional
|
|
53
|
-
ffmpeg_cfr: '30', // Default value: 23
|
|
54
|
-
ffmpeg_preset: 'ultrafast', // Default value: medium
|
|
55
|
-
frame_processing_delay: 1000, // Delay between processing frames in milliseconds
|
|
56
|
-
timeouts: { // Optional. FFmpeg I/O timeouts in milliseconds
|
|
57
|
-
readVideoFrame: 30000, // Max wait for one background video frame from ffmpeg. Default: 30000
|
|
58
|
-
waitDrain: 120000, // Max wait for ffmpeg stdin to accept more frames. Default: 120000
|
|
59
|
-
waitForProcessExit: 480000 // Max wait for ffmpeg to finish after all frames are sent. Default: 480000
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const onProgress = (progressPercent: number) => { // not necessary callback
|
|
65
|
-
console.log(`progress: ${progressPercent} %`);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const shouldStop = () => { // not necessary callback
|
|
69
|
-
return false;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
renderAudioVisualizer(config, onProgress, shouldStop)
|
|
73
|
-
.then(({ exitCode, outputVideoFiles }) => {
|
|
74
|
-
console.log(`exited with code: ${exitCode}`);
|
|
75
|
-
console.log(`written:`, outputVideoFiles);
|
|
76
|
-
});
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Output preview
|
|
80
|
-

|
|
1
|
+
# nodejs-audio-visualizer
|
|
2
|
+
Audio visualizer generator for Node.js with simple configuration.
|
|
3
|
+
Supports PNG, JPG for images, MP4/MOV/AVI/MKV for background videos, and MP3, WAV for audio.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
```javascript
|
|
7
|
+
const renderAudioVisualizer = require('nodejs-audio-visualizer').renderAudioVisualizer;
|
|
8
|
+
const config = {
|
|
9
|
+
image: { // Optional.
|
|
10
|
+
path: 'media/background.png' // Supports PNG and JPG images. Use either 'image' or 'video', not both.
|
|
11
|
+
},
|
|
12
|
+
// OR use a video as background (optional):
|
|
13
|
+
// video: {
|
|
14
|
+
// path: 'media/background.mp4', // Supports MP4, MOV, AVI, MKV. Loops automatically if shorter than audio.
|
|
15
|
+
// autoEdit: true, // Optional. Detects scene cuts and advances between them on each beat so the background edits with the music; omit or false for one continuous play-through from the start.
|
|
16
|
+
// },
|
|
17
|
+
audio: {
|
|
18
|
+
path: 'media/audio.wav', // Supports MP3 and WAV audio
|
|
19
|
+
autoHighlight: true,
|
|
20
|
+
autoHighlightCount: 3, // Optional. With autoHighlight: how many non-overlapping 15s windows with the highest summed spectral energy to aim for (default 1; must be a whole number ≥ 1). You may get fewer videos if the track is too short or does not have that many valid windows. More than one window produces separate videos next to outVideo.path (e.g. out-1.mp4, out-2.mp4).
|
|
21
|
+
},
|
|
22
|
+
outVideo: {
|
|
23
|
+
path: 'media/out.mp4',
|
|
24
|
+
subtitles: { // Optional.
|
|
25
|
+
path: 'media/subtitles.lrc', // Optional. .srt or .lrc format.
|
|
26
|
+
rawContent: "1\n00:00:00,000 --> 99:59:59,999\nSome Artist - Some Song", // Only if path not specified.
|
|
27
|
+
alignment: "middle", // Optional. Available values: 'top', 'middle', 'bottom'. Default: 'bottom'.
|
|
28
|
+
},
|
|
29
|
+
fps: 25, // Default value: 30
|
|
30
|
+
resolution: { width: 1920, height: 1080 }, // Optional. Output frame size in pixels. Omit to keep the background image or video at its native resolution; when set, video frames are scaled to this size and images are resized (center-cropped first if landscape/portrait does not match the target aspect).
|
|
31
|
+
spectrum: { // Audio spectrum configuration (linear bar visualizer). Optional. If neither 'spectrum' nor 'polar' is specified, no visualizer is drawn.
|
|
32
|
+
width: '30%', // 30% of background image width. Default value: 33%. Also you can use absolute values. For example, value 300 for 300 pixels width.
|
|
33
|
+
height: '15%', // 15% of background image height. Default value: 160. Also you can use absolute values. For example, value 300 for 300 pixels height.
|
|
34
|
+
x: 'center', // Available values: 'left', 'center', right'. Also you can use absolute number values. For example, value 300 for 300 pixels x.
|
|
35
|
+
y: 'bottom', // Available values: 'top', 'middle', bottom'. Also you can use absolute number values. For example, value 300 for 300 pixels y.
|
|
36
|
+
rotation: 'mirror', // Available values: 'up', 'down', 'mirror'. Default value: 'mirror'.
|
|
37
|
+
effect: 'volume', // Available values: 'volume', 'smooth', undefined. Default value: undefined.
|
|
38
|
+
color: '#cccc99', // Default value: inverted color of background image
|
|
39
|
+
opacity: '70%' // Default value: '80%'.
|
|
40
|
+
},
|
|
41
|
+
polar: { // Polar/circular audio spectrum configuration. Optional. Use either 'spectrum' or 'polar', not both.
|
|
42
|
+
x: 'center', // Available values: 'left', 'center', 'right'. Also you can use absolute number values. Default value: 'center'.
|
|
43
|
+
y: 'middle', // Available values: 'top', 'middle', 'bottom'. Also you can use absolute number values. Default value: 'middle'.
|
|
44
|
+
innerRadius: 100, // Inner radius of the circular spectrum in pixels. Default value: 100.
|
|
45
|
+
maxBarLength: 160, // Maximum bar length in pixels. Default value: 160.
|
|
46
|
+
barWidth: 15, // Width of each bar in pixels. Default value: 15.
|
|
47
|
+
effect: 'volume', // Available values: 'volume', 'smooth', undefined. Default value: undefined.
|
|
48
|
+
color: '#cccc99', // Default value: inverted color of background image
|
|
49
|
+
opacity: '70%' // Default value: '80%'.
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
tweaks: { // Optional
|
|
53
|
+
ffmpeg_cfr: '30', // Default value: 23
|
|
54
|
+
ffmpeg_preset: 'ultrafast', // Default value: medium
|
|
55
|
+
frame_processing_delay: 1000, // Delay between processing frames in milliseconds
|
|
56
|
+
timeouts: { // Optional. FFmpeg I/O timeouts in milliseconds
|
|
57
|
+
readVideoFrame: 30000, // Max wait for one background video frame from ffmpeg. Default: 30000
|
|
58
|
+
waitDrain: 120000, // Max wait for ffmpeg stdin to accept more frames. Default: 120000
|
|
59
|
+
waitForProcessExit: 480000 // Max wait for ffmpeg to finish after all frames are sent. Default: 480000
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onProgress = (progressPercent: number) => { // not necessary callback
|
|
65
|
+
console.log(`progress: ${progressPercent} %`);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const shouldStop = () => { // not necessary callback
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
renderAudioVisualizer(config, onProgress, shouldStop)
|
|
73
|
+
.then(({ exitCode, outputVideoFiles }) => {
|
|
74
|
+
console.log(`exited with code: ${exitCode}`);
|
|
75
|
+
console.log(`written:`, outputVideoFiles);
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Output preview
|
|
80
|
+

|
package/dist/audio.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
export declare const skipEvery: <T>(skipIndex: number) => (element: T, index: number) => boolean;
|
|
3
|
-
export declare const getPeaks: (spectrums: number[], prevPeaks?: number[] | undefined) => number[];
|
|
4
|
-
export declare const correctPeaks: (spectrums: number[], peaks: number[]) => number[];
|
|
5
|
-
export declare const createSpectrumsProcessor: (sampleRate: number, skipFramesCount: number) => (parseAudioData: () => number[]) => number[];
|
|
6
|
-
export declare const bufferToUInt8: (buffer: Buffer, start: number, end: number) => number[];
|
|
7
|
-
export declare const normalizeAudioData: (PCMData: number[]) => number[];
|
|
8
|
-
export declare const spawnFfmpegAudioReader: (filename: string, format: string) => import("child_process").ChildProcessWithoutNullStreams;
|
|
9
|
-
export declare const createAudioBuffer: (filename: string, format: string) => Promise<{
|
|
10
|
-
audioBuffer: Buffer;
|
|
11
|
-
sampleRate: number;
|
|
12
|
-
}>;
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
export declare const skipEvery: <T>(skipIndex: number) => (element: T, index: number) => boolean;
|
|
3
|
+
export declare const getPeaks: (spectrums: number[], prevPeaks?: number[] | undefined) => number[];
|
|
4
|
+
export declare const correctPeaks: (spectrums: number[], peaks: number[]) => number[];
|
|
5
|
+
export declare const createSpectrumsProcessor: (sampleRate: number, skipFramesCount: number) => (parseAudioData: () => number[]) => number[];
|
|
6
|
+
export declare const bufferToUInt8: (buffer: Buffer, start: number, end: number) => number[];
|
|
7
|
+
export declare const normalizeAudioData: (PCMData: number[]) => number[];
|
|
8
|
+
export declare const spawnFfmpegAudioReader: (filename: string, format: string) => import("child_process").ChildProcessWithoutNullStreams;
|
|
9
|
+
export declare const createAudioBuffer: (filename: string, format: string) => Promise<{
|
|
10
|
+
audioBuffer: Buffer;
|
|
11
|
+
sampleRate: number;
|
|
12
|
+
}>;
|
package/dist/audio.js
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __spreadArrays = (this && this.__spreadArrays) || function () {
|
|
3
|
-
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
|
|
4
|
-
for (var r = Array(s), k = 0, i = 0; i < il; i++)
|
|
5
|
-
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
|
|
6
|
-
r[k] = a[j];
|
|
7
|
-
return r;
|
|
8
|
-
};
|
|
9
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
-
};
|
|
12
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
var child_process_1 = require("child_process");
|
|
14
|
-
var ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
|
|
15
|
-
var dsp_1 = require("./dsp");
|
|
16
|
-
exports.skipEvery = function (skipIndex) { return function (element, index) {
|
|
17
|
-
return index % skipIndex === 0;
|
|
18
|
-
}; };
|
|
19
|
-
exports.getPeaks = function (spectrums, prevPeaks) {
|
|
20
|
-
if (!prevPeaks) {
|
|
21
|
-
return spectrums;
|
|
22
|
-
}
|
|
23
|
-
var resultPeaks = [];
|
|
24
|
-
for (var i = 0; i < spectrums.length; i++) {
|
|
25
|
-
var currValue = spectrums[i];
|
|
26
|
-
var currPrevPeak = prevPeaks[i] || 0;
|
|
27
|
-
resultPeaks.push((currValue > currPrevPeak) ? currValue : currPrevPeak);
|
|
28
|
-
}
|
|
29
|
-
return resultPeaks;
|
|
30
|
-
};
|
|
31
|
-
exports.correctPeaks = function (spectrums, peaks) {
|
|
32
|
-
var resultSpectrum = [];
|
|
33
|
-
for (var i = 0; i < spectrums.length; i++) {
|
|
34
|
-
var value = spectrums[i];
|
|
35
|
-
var peakValue = peaks[i] || 0;
|
|
36
|
-
if (value < 3) {
|
|
37
|
-
resultSpectrum.push(value / 3);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
resultSpectrum.push(value / peakValue);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return resultSpectrum;
|
|
44
|
-
};
|
|
45
|
-
var oldMax = 1;
|
|
46
|
-
var newMin = 0.6;
|
|
47
|
-
var getInterpolationFactor = function (skippedFramesCount, skipFramesCount) {
|
|
48
|
-
var interpolationFactor = 1 - (skippedFramesCount / skipFramesCount);
|
|
49
|
-
var interpolationFactorInRange = (interpolationFactor * (oldMax - newMin)) + newMin;
|
|
50
|
-
return interpolationFactorInRange;
|
|
51
|
-
};
|
|
52
|
-
exports.createSpectrumsProcessor = function (sampleRate, skipFramesCount) {
|
|
53
|
-
var prevAudioDataNormalized = [];
|
|
54
|
-
var prevPeaks = [];
|
|
55
|
-
var skippedFramesCount = 0;
|
|
56
|
-
var targetSpectrum = [];
|
|
57
|
-
var previousSpectrum = [];
|
|
58
|
-
return function (parseAudioData) {
|
|
59
|
-
if (skippedFramesCount === 0) {
|
|
60
|
-
skippedFramesCount = skipFramesCount;
|
|
61
|
-
var audioDataNomrmalized = exports.normalizeAudioData(parseAudioData());
|
|
62
|
-
prevAudioDataNormalized = audioDataNomrmalized;
|
|
63
|
-
var spectrum = dsp_1.getSpectrum(audioDataNomrmalized, sampleRate);
|
|
64
|
-
var peaks = exports.getPeaks(spectrum, prevPeaks);
|
|
65
|
-
var correctedSpectrum = prevPeaks.length === 0 ?
|
|
66
|
-
Array(dsp_1.FREQUENCY_BANDS.length).fill(0) :
|
|
67
|
-
exports.correctPeaks(spectrum, peaks);
|
|
68
|
-
prevPeaks = peaks;
|
|
69
|
-
previousSpectrum = __spreadArrays(targetSpectrum);
|
|
70
|
-
targetSpectrum = correctedSpectrum;
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
skippedFramesCount--;
|
|
74
|
-
}
|
|
75
|
-
var interpolationFactor = getInterpolationFactor(skippedFramesCount, skipFramesCount);
|
|
76
|
-
var interpolatedSpectrum = targetSpectrum.map(function (target, i) {
|
|
77
|
-
var prev = previousSpectrum[i] || target;
|
|
78
|
-
return prev + (target - prev) * interpolationFactor;
|
|
79
|
-
});
|
|
80
|
-
return interpolatedSpectrum;
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
exports.bufferToUInt8 = function (buffer, start, end) {
|
|
84
|
-
var numbers = [];
|
|
85
|
-
for (var i = start; i < end; i += 1) {
|
|
86
|
-
numbers.push(buffer.readUInt8(i));
|
|
87
|
-
}
|
|
88
|
-
return numbers;
|
|
89
|
-
};
|
|
90
|
-
exports.normalizeAudioData = function (PCMData) {
|
|
91
|
-
return PCMData.map(function (num) { return (num - 128) / 128; });
|
|
92
|
-
};
|
|
93
|
-
exports.spawnFfmpegAudioReader = function (filename, format) {
|
|
94
|
-
if (!ffmpeg_static_1.default) {
|
|
95
|
-
throw new Error('ffmpeg path not found');
|
|
96
|
-
}
|
|
97
|
-
var ffmpegProcess = child_process_1.spawn(ffmpeg_static_1.default, ['-i', filename, '-f', format, '-ac', '1', '-']);
|
|
98
|
-
return ffmpegProcess;
|
|
99
|
-
};
|
|
100
|
-
exports.createAudioBuffer = function (filename, format) {
|
|
101
|
-
return new Promise(function (resolve, reject) {
|
|
102
|
-
var sampleRate;
|
|
103
|
-
var sampleRateRegExp = /(\d+) Hz/m;
|
|
104
|
-
var audioBuffers = [];
|
|
105
|
-
var ffmpegAudioReader = exports.spawnFfmpegAudioReader(filename, format);
|
|
106
|
-
ffmpegAudioReader.stderr.on('data', function (data) {
|
|
107
|
-
var match = data.toString().match(sampleRateRegExp);
|
|
108
|
-
if (!sampleRate && match) {
|
|
109
|
-
sampleRate = match[1];
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
ffmpegAudioReader.stdout.on('data', function (chunkBuffer) {
|
|
113
|
-
audioBuffers.push(chunkBuffer);
|
|
114
|
-
});
|
|
115
|
-
ffmpegAudioReader.stdout.on('end', function () {
|
|
116
|
-
var audioBuffer = Buffer.concat(audioBuffers);
|
|
117
|
-
resolve({ audioBuffer: audioBuffer, sampleRate: sampleRate });
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
var __spreadArrays = (this && this.__spreadArrays) || function () {
|
|
3
|
+
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
|
|
4
|
+
for (var r = Array(s), k = 0, i = 0; i < il; i++)
|
|
5
|
+
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
|
|
6
|
+
r[k] = a[j];
|
|
7
|
+
return r;
|
|
8
|
+
};
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
var child_process_1 = require("child_process");
|
|
14
|
+
var ffmpeg_static_1 = __importDefault(require("ffmpeg-static"));
|
|
15
|
+
var dsp_1 = require("./dsp");
|
|
16
|
+
exports.skipEvery = function (skipIndex) { return function (element, index) {
|
|
17
|
+
return index % skipIndex === 0;
|
|
18
|
+
}; };
|
|
19
|
+
exports.getPeaks = function (spectrums, prevPeaks) {
|
|
20
|
+
if (!prevPeaks) {
|
|
21
|
+
return spectrums;
|
|
22
|
+
}
|
|
23
|
+
var resultPeaks = [];
|
|
24
|
+
for (var i = 0; i < spectrums.length; i++) {
|
|
25
|
+
var currValue = spectrums[i];
|
|
26
|
+
var currPrevPeak = prevPeaks[i] || 0;
|
|
27
|
+
resultPeaks.push((currValue > currPrevPeak) ? currValue : currPrevPeak);
|
|
28
|
+
}
|
|
29
|
+
return resultPeaks;
|
|
30
|
+
};
|
|
31
|
+
exports.correctPeaks = function (spectrums, peaks) {
|
|
32
|
+
var resultSpectrum = [];
|
|
33
|
+
for (var i = 0; i < spectrums.length; i++) {
|
|
34
|
+
var value = spectrums[i];
|
|
35
|
+
var peakValue = peaks[i] || 0;
|
|
36
|
+
if (value < 3) {
|
|
37
|
+
resultSpectrum.push(value / 3);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
resultSpectrum.push(value / peakValue);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return resultSpectrum;
|
|
44
|
+
};
|
|
45
|
+
var oldMax = 1;
|
|
46
|
+
var newMin = 0.6;
|
|
47
|
+
var getInterpolationFactor = function (skippedFramesCount, skipFramesCount) {
|
|
48
|
+
var interpolationFactor = 1 - (skippedFramesCount / skipFramesCount);
|
|
49
|
+
var interpolationFactorInRange = (interpolationFactor * (oldMax - newMin)) + newMin;
|
|
50
|
+
return interpolationFactorInRange;
|
|
51
|
+
};
|
|
52
|
+
exports.createSpectrumsProcessor = function (sampleRate, skipFramesCount) {
|
|
53
|
+
var prevAudioDataNormalized = [];
|
|
54
|
+
var prevPeaks = [];
|
|
55
|
+
var skippedFramesCount = 0;
|
|
56
|
+
var targetSpectrum = [];
|
|
57
|
+
var previousSpectrum = [];
|
|
58
|
+
return function (parseAudioData) {
|
|
59
|
+
if (skippedFramesCount === 0) {
|
|
60
|
+
skippedFramesCount = skipFramesCount;
|
|
61
|
+
var audioDataNomrmalized = exports.normalizeAudioData(parseAudioData());
|
|
62
|
+
prevAudioDataNormalized = audioDataNomrmalized;
|
|
63
|
+
var spectrum = dsp_1.getSpectrum(audioDataNomrmalized, sampleRate);
|
|
64
|
+
var peaks = exports.getPeaks(spectrum, prevPeaks);
|
|
65
|
+
var correctedSpectrum = prevPeaks.length === 0 ?
|
|
66
|
+
Array(dsp_1.FREQUENCY_BANDS.length).fill(0) :
|
|
67
|
+
exports.correctPeaks(spectrum, peaks);
|
|
68
|
+
prevPeaks = peaks;
|
|
69
|
+
previousSpectrum = __spreadArrays(targetSpectrum);
|
|
70
|
+
targetSpectrum = correctedSpectrum;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
skippedFramesCount--;
|
|
74
|
+
}
|
|
75
|
+
var interpolationFactor = getInterpolationFactor(skippedFramesCount, skipFramesCount);
|
|
76
|
+
var interpolatedSpectrum = targetSpectrum.map(function (target, i) {
|
|
77
|
+
var prev = previousSpectrum[i] || target;
|
|
78
|
+
return prev + (target - prev) * interpolationFactor;
|
|
79
|
+
});
|
|
80
|
+
return interpolatedSpectrum;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
exports.bufferToUInt8 = function (buffer, start, end) {
|
|
84
|
+
var numbers = [];
|
|
85
|
+
for (var i = start; i < end; i += 1) {
|
|
86
|
+
numbers.push(buffer.readUInt8(i));
|
|
87
|
+
}
|
|
88
|
+
return numbers;
|
|
89
|
+
};
|
|
90
|
+
exports.normalizeAudioData = function (PCMData) {
|
|
91
|
+
return PCMData.map(function (num) { return (num - 128) / 128; });
|
|
92
|
+
};
|
|
93
|
+
exports.spawnFfmpegAudioReader = function (filename, format) {
|
|
94
|
+
if (!ffmpeg_static_1.default) {
|
|
95
|
+
throw new Error('ffmpeg path not found');
|
|
96
|
+
}
|
|
97
|
+
var ffmpegProcess = child_process_1.spawn(ffmpeg_static_1.default, ['-i', filename, '-f', format, '-ac', '1', '-']);
|
|
98
|
+
return ffmpegProcess;
|
|
99
|
+
};
|
|
100
|
+
exports.createAudioBuffer = function (filename, format) {
|
|
101
|
+
return new Promise(function (resolve, reject) {
|
|
102
|
+
var sampleRate;
|
|
103
|
+
var sampleRateRegExp = /(\d+) Hz/m;
|
|
104
|
+
var audioBuffers = [];
|
|
105
|
+
var ffmpegAudioReader = exports.spawnFfmpegAudioReader(filename, format);
|
|
106
|
+
ffmpegAudioReader.stderr.on('data', function (data) {
|
|
107
|
+
var match = data.toString().match(sampleRateRegExp);
|
|
108
|
+
if (!sampleRate && match) {
|
|
109
|
+
sampleRate = match[1];
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
ffmpegAudioReader.stdout.on('data', function (chunkBuffer) {
|
|
113
|
+
audioBuffers.push(chunkBuffer);
|
|
114
|
+
});
|
|
115
|
+
ffmpegAudioReader.stdout.on('end', function () {
|
|
116
|
+
var audioBuffer = Buffer.concat(audioBuffers);
|
|
117
|
+
resolve({ audioBuffer: audioBuffer, sampleRate: sampleRate });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
121
|
//# sourceMappingURL=audio.js.map
|
package/dist/beats.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
export interface BeatInfo {
|
|
2
|
-
isBeat: boolean;
|
|
3
|
-
intensity: number;
|
|
4
|
-
energy: number;
|
|
5
|
-
threshold: number;
|
|
6
|
-
}
|
|
7
|
-
export interface BeatDetectorOptions {
|
|
8
|
-
/** Number of past frames to average for adaptive threshold (~1.5s by default) */
|
|
9
|
-
historySize?: number;
|
|
10
|
-
/** Energy must exceed average * this multiplier to trigger a beat */
|
|
11
|
-
thresholdMultiplier?: number;
|
|
12
|
-
/** How fast the beat intensity fades between beats (0-1, lower = faster fade) */
|
|
13
|
-
decayRate?: number;
|
|
14
|
-
/** Minimum frames between consecutive beats (~150ms by default) */
|
|
15
|
-
cooldownFrames?: number;
|
|
16
|
-
/** How many low-frequency spectrum bands contribute to energy (sub-bass + bass) */
|
|
17
|
-
bassEndIndex?: number;
|
|
18
|
-
}
|
|
19
|
-
export declare const createBeatDetector: (fps: number, options?: BeatDetectorOptions | undefined) => (spectrum: number[]) => BeatInfo;
|
|
1
|
+
export interface BeatInfo {
|
|
2
|
+
isBeat: boolean;
|
|
3
|
+
intensity: number;
|
|
4
|
+
energy: number;
|
|
5
|
+
threshold: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BeatDetectorOptions {
|
|
8
|
+
/** Number of past frames to average for adaptive threshold (~1.5s by default) */
|
|
9
|
+
historySize?: number;
|
|
10
|
+
/** Energy must exceed average * this multiplier to trigger a beat */
|
|
11
|
+
thresholdMultiplier?: number;
|
|
12
|
+
/** How fast the beat intensity fades between beats (0-1, lower = faster fade) */
|
|
13
|
+
decayRate?: number;
|
|
14
|
+
/** Minimum frames between consecutive beats (~150ms by default) */
|
|
15
|
+
cooldownFrames?: number;
|
|
16
|
+
/** How many low-frequency spectrum bands contribute to energy (sub-bass + bass) */
|
|
17
|
+
bassEndIndex?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const createBeatDetector: (fps: number, options?: BeatDetectorOptions | undefined) => (spectrum: number[]) => BeatInfo;
|
package/dist/beats.js
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createBeatDetector = function (fps, options) {
|
|
4
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
5
|
-
var historySize = (_b = (_a = options) === null || _a === void 0 ? void 0 : _a.historySize, (_b !== null && _b !== void 0 ? _b : Math.round(fps * 1.5)));
|
|
6
|
-
var thresholdMultiplier = (_d = (_c = options) === null || _c === void 0 ? void 0 : _c.thresholdMultiplier, (_d !== null && _d !== void 0 ? _d : 1.4));
|
|
7
|
-
var decayRate = (_f = (_e = options) === null || _e === void 0 ? void 0 : _e.decayRate, (_f !== null && _f !== void 0 ? _f : 0.85));
|
|
8
|
-
var cooldownFrames = (_h = (_g = options) === null || _g === void 0 ? void 0 : _g.cooldownFrames, (_h !== null && _h !== void 0 ? _h : Math.round(fps * 0.15)));
|
|
9
|
-
var bassEndIndex = (_k = (_j = options) === null || _j === void 0 ? void 0 : _j.bassEndIndex, (_k !== null && _k !== void 0 ? _k : 8));
|
|
10
|
-
var energyHistory = [];
|
|
11
|
-
var framesSinceLastBeat = cooldownFrames;
|
|
12
|
-
var decayingIntensity = 0;
|
|
13
|
-
return function (spectrum) {
|
|
14
|
-
var bassSpectrum = spectrum.slice(0, Math.min(bassEndIndex, spectrum.length));
|
|
15
|
-
var energy = bassSpectrum.reduce(function (sum, val) { return sum + val * val; }, 0) / bassSpectrum.length;
|
|
16
|
-
energyHistory.push(energy);
|
|
17
|
-
if (energyHistory.length > historySize) {
|
|
18
|
-
energyHistory.shift();
|
|
19
|
-
}
|
|
20
|
-
var avgEnergy = energyHistory.reduce(function (sum, e) { return sum + e; }, 0) / energyHistory.length;
|
|
21
|
-
var variance = energyHistory.reduce(function (sum, e) { return sum + Math.pow((e - avgEnergy), 2); }, 0) / energyHistory.length;
|
|
22
|
-
var stdDev = Math.sqrt(variance);
|
|
23
|
-
var threshold = avgEnergy + stdDev * thresholdMultiplier;
|
|
24
|
-
framesSinceLastBeat++;
|
|
25
|
-
var hasEnoughHistory = energyHistory.length >= Math.round(historySize / 3);
|
|
26
|
-
var isAboveThreshold = energy > threshold && hasEnoughHistory;
|
|
27
|
-
var isCooldownOver = framesSinceLastBeat >= cooldownFrames;
|
|
28
|
-
var isBeat = isAboveThreshold && isCooldownOver;
|
|
29
|
-
if (isBeat) {
|
|
30
|
-
framesSinceLastBeat = 0;
|
|
31
|
-
decayingIntensity = Math.min(1, (energy - threshold) / (stdDev + 1e-6));
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
decayingIntensity *= decayRate;
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
isBeat: isBeat,
|
|
38
|
-
intensity: decayingIntensity,
|
|
39
|
-
energy: energy,
|
|
40
|
-
threshold: threshold,
|
|
41
|
-
};
|
|
42
|
-
};
|
|
43
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createBeatDetector = function (fps, options) {
|
|
4
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
5
|
+
var historySize = (_b = (_a = options) === null || _a === void 0 ? void 0 : _a.historySize, (_b !== null && _b !== void 0 ? _b : Math.round(fps * 1.5)));
|
|
6
|
+
var thresholdMultiplier = (_d = (_c = options) === null || _c === void 0 ? void 0 : _c.thresholdMultiplier, (_d !== null && _d !== void 0 ? _d : 1.4));
|
|
7
|
+
var decayRate = (_f = (_e = options) === null || _e === void 0 ? void 0 : _e.decayRate, (_f !== null && _f !== void 0 ? _f : 0.85));
|
|
8
|
+
var cooldownFrames = (_h = (_g = options) === null || _g === void 0 ? void 0 : _g.cooldownFrames, (_h !== null && _h !== void 0 ? _h : Math.round(fps * 0.15)));
|
|
9
|
+
var bassEndIndex = (_k = (_j = options) === null || _j === void 0 ? void 0 : _j.bassEndIndex, (_k !== null && _k !== void 0 ? _k : 8));
|
|
10
|
+
var energyHistory = [];
|
|
11
|
+
var framesSinceLastBeat = cooldownFrames;
|
|
12
|
+
var decayingIntensity = 0;
|
|
13
|
+
return function (spectrum) {
|
|
14
|
+
var bassSpectrum = spectrum.slice(0, Math.min(bassEndIndex, spectrum.length));
|
|
15
|
+
var energy = bassSpectrum.reduce(function (sum, val) { return sum + val * val; }, 0) / bassSpectrum.length;
|
|
16
|
+
energyHistory.push(energy);
|
|
17
|
+
if (energyHistory.length > historySize) {
|
|
18
|
+
energyHistory.shift();
|
|
19
|
+
}
|
|
20
|
+
var avgEnergy = energyHistory.reduce(function (sum, e) { return sum + e; }, 0) / energyHistory.length;
|
|
21
|
+
var variance = energyHistory.reduce(function (sum, e) { return sum + Math.pow((e - avgEnergy), 2); }, 0) / energyHistory.length;
|
|
22
|
+
var stdDev = Math.sqrt(variance);
|
|
23
|
+
var threshold = avgEnergy + stdDev * thresholdMultiplier;
|
|
24
|
+
framesSinceLastBeat++;
|
|
25
|
+
var hasEnoughHistory = energyHistory.length >= Math.round(historySize / 3);
|
|
26
|
+
var isAboveThreshold = energy > threshold && hasEnoughHistory;
|
|
27
|
+
var isCooldownOver = framesSinceLastBeat >= cooldownFrames;
|
|
28
|
+
var isBeat = isAboveThreshold && isCooldownOver;
|
|
29
|
+
if (isBeat) {
|
|
30
|
+
framesSinceLastBeat = 0;
|
|
31
|
+
decayingIntensity = Math.min(1, (energy - threshold) / (stdDev + 1e-6));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
decayingIntensity *= decayRate;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
isBeat: isBeat,
|
|
38
|
+
intensity: decayingIntensity,
|
|
39
|
+
energy: energy,
|
|
40
|
+
threshold: threshold,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
44
|
//# sourceMappingURL=beats.js.map
|
package/dist/bpmEncoder.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
export interface EncodedBmp {
|
|
3
|
-
shiftPos: number;
|
|
4
|
-
rowBytes: number;
|
|
5
|
-
data: Buffer;
|
|
6
|
-
}
|
|
7
|
-
export declare const createBpmEncoder: (imgData: {
|
|
8
|
-
width: number;
|
|
9
|
-
height: number;
|
|
10
|
-
}) => (buffer: Buffer) => EncodedBmp;
|
|
11
|
-
export declare const createBgrFrameEncoder: (imgData: {
|
|
12
|
-
width: number;
|
|
13
|
-
height: number;
|
|
14
|
-
}) => (bgrBuffer: Buffer) => EncodedBmp;
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
export interface EncodedBmp {
|
|
3
|
+
shiftPos: number;
|
|
4
|
+
rowBytes: number;
|
|
5
|
+
data: Buffer;
|
|
6
|
+
}
|
|
7
|
+
export declare const createBpmEncoder: (imgData: {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}) => (buffer: Buffer) => EncodedBmp;
|
|
11
|
+
export declare const createBgrFrameEncoder: (imgData: {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}) => (bgrBuffer: Buffer) => EncodedBmp;
|