nodejs-audio-visualizer 5.5.3 → 5.6.3
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 +84 -83
- package/dist/audio.d.ts +13 -13
- package/dist/audio.js +128 -128
- package/dist/beats.d.ts +39 -39
- package/dist/beats.js +220 -220
- package/dist/bpmEncoder.d.ts +14 -14
- package/dist/bpmEncoder.js +97 -97
- package/dist/config.d.ts +81 -80
- package/dist/config.js +392 -391
- package/dist/config.js.map +1 -1
- package/dist/dsp.d.ts +16 -16
- package/dist/dsp.js +76 -76
- package/dist/highlight.d.ts +91 -91
- package/dist/highlight.js +606 -606
- package/dist/image.d.ts +107 -97
- package/dist/image.js +614 -545
- package/dist/image.js.map +1 -1
- package/dist/index.d.ts +98 -96
- package/dist/index.js +673 -667
- 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 +171 -171
- package/dist/video.js +743 -743
- package/dist/waitForEventLoop.d.ts +1 -1
- package/dist/waitForEventLoop.js +5 -5
- package/package.json +62 -62
- package/src/audio.ts +129 -129
- package/src/beats.ts +279 -279
- package/src/bpmEncoder.ts +97 -97
- package/src/config.ts +504 -501
- package/src/dsp.ts +95 -95
- package/src/highlight.ts +617 -617
- package/src/image.ts +791 -707
- package/src/index.ts +896 -885
- package/src/subtitleConvert.ts +63 -63
- package/src/tests/audio.test.ts +102 -102
- package/src/tests/beats.test.ts +134 -134
- package/src/tests/config.test.ts +568 -553
- package/src/tests/dsp.test.ts +24 -24
- package/src/tests/highlight.test.ts +397 -397
- package/src/tests/image.test.ts +371 -319
- package/src/tests/index.test.ts +180 -180
- package/src/tests/subtitleConvert.test.ts +23 -23
- package/src/tests/video.test.ts +652 -652
- package/src/typings.d.ts +28 -28
- package/src/video.ts +947 -947
- 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,83 +1,84 @@
|
|
|
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. Can also be an array of paths, e.g. ['media/bg1.mp4', 'media/bg2.mp4']: with autoEdit false the videos play one after another (looping the sequence as needed), and with autoHighlight producing several videos each one starts with a different video from the array; with autoEdit true, cuts switch (shuffle) between the given videos instead of auto-detecting scene changes. Videos in an array should share the same resolution and frame rate for best results, especially when outVideo.resolution is not set.
|
|
15
|
-
// hookPath: 'media/hook.mp4', // Optional. Video played once before the 'path' video(s). Plays in full; with audio.autoHighlight, each highlight is chosen so the hook ends on a detected drop (the biggest jump in energy on a beat) and 'path' starts with it and plays for ~15s (the hook is added before the ~15s, not counted in it); the whole hook always plays (when the drop is closer to the track start than the hook's length, silence is added before the track), and if no drop fits, the hook plays before the loudest ~15s window instead. The hook's sound, if it has any, is mixed over the track, which fades in from silence to full volume while the hook plays. With several highlight videos, each one opens with the hook.
|
|
16
|
-
// autoEdit: true, // Optional. Detects scene cuts and advances between them on the detected BPM beat grid about every 2–4 beats so the background edits with the music; omit or false for one continuous play-through from the start. When enabled, a brief camera shake starts 0.1s after each cut and decays over about one-third of a beat (if the video has no detected scene changes, and so no cuts, it shakes every 2 beats instead).
|
|
17
|
-
// cameraShake: false, // Optional. With autoEdit: set to false to disable the camera shake applied on cuts (default true).
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.log(`
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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. Can also be an array of paths, e.g. ['media/bg1.mp4', 'media/bg2.mp4']: with autoEdit false the videos play one after another (looping the sequence as needed), and with autoHighlight producing several videos each one starts with a different video from the array; with autoEdit true, cuts switch (shuffle) between the given videos instead of auto-detecting scene changes. Videos in an array should share the same resolution and frame rate for best results, especially when outVideo.resolution is not set.
|
|
15
|
+
// hookPath: 'media/hook.mp4', // Optional. Video played once before the 'path' video(s). Plays in full; with audio.autoHighlight, each highlight is chosen so the hook ends on a detected drop (the biggest jump in energy on a beat) and 'path' starts with it and plays for ~15s (the hook is added before the ~15s, not counted in it); the whole hook always plays (when the drop is closer to the track start than the hook's length, silence is added before the track), and if no drop fits, the hook plays before the loudest ~15s window instead. The hook's sound, if it has any, is mixed over the track, which fades in from silence to full volume while the hook plays. With several highlight videos, each one opens with the hook.
|
|
16
|
+
// autoEdit: true, // Optional. Detects scene cuts and advances between them on the detected BPM beat grid about every 2–4 beats so the background edits with the music; omit or false for one continuous play-through from the start. When enabled, a brief camera shake starts 0.1s after each cut and decays over about one-third of a beat (if the video has no detected scene changes, and so no cuts, it shakes every 2 beats instead).
|
|
17
|
+
// cameraShake: false, // Optional. With autoEdit: set to false to disable the camera shake applied on cuts (default true).
|
|
18
|
+
// cutZoom: true, // Optional. With autoEdit: starts each cut zoomed in (115%) and quickly zooms out to normal over about one-third of a beat (every 2 beats when there are no cuts). Can be combined with cameraShake (default false).
|
|
19
|
+
// },
|
|
20
|
+
audio: {
|
|
21
|
+
path: 'media/audio.wav', // Supports MP3 and WAV audio
|
|
22
|
+
autoHighlight: true, // Optional. Cuts the most energetic ~15s of the track. Start and end are moved onto beats (by up to 1.5s each), preferring a length of whole 4-beat bars, so the highlight may run slightly shorter or longer than 15s.
|
|
23
|
+
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).
|
|
24
|
+
},
|
|
25
|
+
outVideo: {
|
|
26
|
+
path: 'media/out.mp4',
|
|
27
|
+
subtitles: { // Optional.
|
|
28
|
+
path: 'media/subtitles.lrc', // Optional. .srt or .lrc format.
|
|
29
|
+
rawContent: "1\n00:00:00,000 --> 99:59:59,999\nSome Artist - Some Song", // Only if path not specified.
|
|
30
|
+
alignment: "middle", // Optional. Available values: 'top', 'middle', 'bottom'. Default: 'bottom'.
|
|
31
|
+
},
|
|
32
|
+
fps: 25, // Default value: 30
|
|
33
|
+
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).
|
|
34
|
+
enhanceFilters: true, // Optional. Denoise, sharpen and boost contrast/saturation of the output video (FFmpeg hqdn3d, unsharp, eq; Lanczos scaling). Default: false
|
|
35
|
+
spectrum: { // Audio spectrum configuration (linear bar visualizer). Optional. If neither 'spectrum' nor 'polar' is specified, no visualizer is drawn.
|
|
36
|
+
width: '30%', // 30% of background image width. Default value: 33%. Also you can use absolute values. For example, value 300 for 300 pixels width.
|
|
37
|
+
height: '15%', // 15% of background image height. Default value: 160. Also you can use absolute values. For example, value 300 for 300 pixels height.
|
|
38
|
+
x: 'center', // Available values: 'left', 'center', right'. Also you can use absolute number values. For example, value 300 for 300 pixels x.
|
|
39
|
+
y: 'bottom', // Available values: 'top', 'middle', bottom'. Also you can use absolute number values. For example, value 300 for 300 pixels y.
|
|
40
|
+
rotation: 'mirror', // Available values: 'up', 'down', 'mirror'. Default value: 'mirror'.
|
|
41
|
+
effect: 'volume', // Available values: 'volume', 'smooth', undefined. Default value: undefined.
|
|
42
|
+
color: '#cccc99', // Default value: inverted color of background image
|
|
43
|
+
opacity: '70%' // Default value: '80%'.
|
|
44
|
+
},
|
|
45
|
+
polar: { // Polar/circular audio spectrum configuration. Optional. Use either 'spectrum' or 'polar', not both.
|
|
46
|
+
x: 'center', // Available values: 'left', 'center', 'right'. Also you can use absolute number values. Default value: 'center'.
|
|
47
|
+
y: 'middle', // Available values: 'top', 'middle', 'bottom'. Also you can use absolute number values. Default value: 'middle'.
|
|
48
|
+
innerRadius: 100, // Inner radius of the circular spectrum in pixels. Default value: 100.
|
|
49
|
+
maxBarLength: 160, // Maximum bar length in pixels. Default value: 160.
|
|
50
|
+
barWidth: 15, // Width of each bar in pixels. Default value: 15.
|
|
51
|
+
effect: 'volume', // Available values: 'volume', 'smooth', undefined. Default value: undefined.
|
|
52
|
+
color: '#cccc99', // Default value: inverted color of background image
|
|
53
|
+
opacity: '70%' // Default value: '80%'.
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
tweaks: { // Optional
|
|
57
|
+
ffmpeg_cfr: '30', // Default value: 23
|
|
58
|
+
ffmpeg_preset: 'ultrafast', // Default value: medium
|
|
59
|
+
frame_processing_delay: 1000, // Delay between processing frames in milliseconds
|
|
60
|
+
timeouts: { // Optional. FFmpeg I/O timeouts in milliseconds
|
|
61
|
+
readVideoFrame: 30000, // Max wait for one background video frame from ffmpeg. Default: 30000
|
|
62
|
+
waitDrain: 120000, // Max wait for ffmpeg stdin to accept more frames. Default: 120000
|
|
63
|
+
waitForProcessExit: 480000 // Max wait for ffmpeg to finish after all frames are sent. Default: 480000
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const onProgress = (progressPercent: number) => { // not necessary callback
|
|
69
|
+
console.log(`progress: ${progressPercent} %`);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const shouldStop = () => { // not necessary callback
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
renderAudioVisualizer(config, onProgress, shouldStop)
|
|
77
|
+
.then(({ exitCode, outputVideoFiles }) => {
|
|
78
|
+
console.log(`exited with code: ${exitCode}`);
|
|
79
|
+
console.log(`written:`, outputVideoFiles);
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Output preview
|
|
84
|
+

|
package/dist/audio.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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 pcmU8ToFloatSamples: (buffer: Buffer) => number[];
|
|
9
|
-
export declare const spawnFfmpegAudioReader: (filename: string, format: string) => import("child_process").ChildProcessWithoutNullStreams;
|
|
10
|
-
export declare const createAudioBuffer: (filename: string, format: string) => Promise<{
|
|
11
|
-
audioBuffer: Buffer;
|
|
12
|
-
sampleRate: number;
|
|
13
|
-
}>;
|
|
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 pcmU8ToFloatSamples: (buffer: Buffer) => number[];
|
|
9
|
+
export declare const spawnFfmpegAudioReader: (filename: string, format: string) => import("child_process").ChildProcessWithoutNullStreams;
|
|
10
|
+
export declare const createAudioBuffer: (filename: string, format: string) => Promise<{
|
|
11
|
+
audioBuffer: Buffer;
|
|
12
|
+
sampleRate: number;
|
|
13
|
+
}>;
|
package/dist/audio.js
CHANGED
|
@@ -1,129 +1,129 @@
|
|
|
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.pcmU8ToFloatSamples = function (buffer) {
|
|
94
|
-
var n = buffer.length;
|
|
95
|
-
var samples = new Array(n);
|
|
96
|
-
for (var i = 0; i < n; i++) {
|
|
97
|
-
samples[i] = (buffer[i] - 128) / 128;
|
|
98
|
-
}
|
|
99
|
-
return samples;
|
|
100
|
-
};
|
|
101
|
-
exports.spawnFfmpegAudioReader = function (filename, format) {
|
|
102
|
-
if (!ffmpeg_static_1.default) {
|
|
103
|
-
throw new Error('ffmpeg path not found');
|
|
104
|
-
}
|
|
105
|
-
var ffmpegProcess = child_process_1.spawn(ffmpeg_static_1.default, ['-i', filename, '-f', format, '-ac', '1', '-']);
|
|
106
|
-
return ffmpegProcess;
|
|
107
|
-
};
|
|
108
|
-
exports.createAudioBuffer = function (filename, format) {
|
|
109
|
-
return new Promise(function (resolve, reject) {
|
|
110
|
-
var sampleRate;
|
|
111
|
-
var sampleRateRegExp = /(\d+) Hz/m;
|
|
112
|
-
var audioBuffers = [];
|
|
113
|
-
var ffmpegAudioReader = exports.spawnFfmpegAudioReader(filename, format);
|
|
114
|
-
ffmpegAudioReader.stderr.on('data', function (data) {
|
|
115
|
-
var match = data.toString().match(sampleRateRegExp);
|
|
116
|
-
if (!sampleRate && match) {
|
|
117
|
-
sampleRate = match[1];
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
ffmpegAudioReader.stdout.on('data', function (chunkBuffer) {
|
|
121
|
-
audioBuffers.push(chunkBuffer);
|
|
122
|
-
});
|
|
123
|
-
ffmpegAudioReader.stdout.on('end', function () {
|
|
124
|
-
var audioBuffer = Buffer.concat(audioBuffers);
|
|
125
|
-
resolve({ audioBuffer: audioBuffer, sampleRate: sampleRate });
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
};
|
|
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.pcmU8ToFloatSamples = function (buffer) {
|
|
94
|
+
var n = buffer.length;
|
|
95
|
+
var samples = new Array(n);
|
|
96
|
+
for (var i = 0; i < n; i++) {
|
|
97
|
+
samples[i] = (buffer[i] - 128) / 128;
|
|
98
|
+
}
|
|
99
|
+
return samples;
|
|
100
|
+
};
|
|
101
|
+
exports.spawnFfmpegAudioReader = function (filename, format) {
|
|
102
|
+
if (!ffmpeg_static_1.default) {
|
|
103
|
+
throw new Error('ffmpeg path not found');
|
|
104
|
+
}
|
|
105
|
+
var ffmpegProcess = child_process_1.spawn(ffmpeg_static_1.default, ['-i', filename, '-f', format, '-ac', '1', '-']);
|
|
106
|
+
return ffmpegProcess;
|
|
107
|
+
};
|
|
108
|
+
exports.createAudioBuffer = function (filename, format) {
|
|
109
|
+
return new Promise(function (resolve, reject) {
|
|
110
|
+
var sampleRate;
|
|
111
|
+
var sampleRateRegExp = /(\d+) Hz/m;
|
|
112
|
+
var audioBuffers = [];
|
|
113
|
+
var ffmpegAudioReader = exports.spawnFfmpegAudioReader(filename, format);
|
|
114
|
+
ffmpegAudioReader.stderr.on('data', function (data) {
|
|
115
|
+
var match = data.toString().match(sampleRateRegExp);
|
|
116
|
+
if (!sampleRate && match) {
|
|
117
|
+
sampleRate = match[1];
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
ffmpegAudioReader.stdout.on('data', function (chunkBuffer) {
|
|
121
|
+
audioBuffers.push(chunkBuffer);
|
|
122
|
+
});
|
|
123
|
+
ffmpegAudioReader.stdout.on('end', function () {
|
|
124
|
+
var audioBuffer = Buffer.concat(audioBuffers);
|
|
125
|
+
resolve({ audioBuffer: audioBuffer, sampleRate: sampleRate });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
129
|
//# sourceMappingURL=audio.js.map
|
package/dist/beats.d.ts
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
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 interface TempoEstimate {
|
|
20
|
-
bpm: number;
|
|
21
|
-
periodFrames: number;
|
|
22
|
-
phaseFrame: number;
|
|
23
|
-
/** Beat times in seconds from the start of the analyzed audio. */
|
|
24
|
-
beatsSec?: number[];
|
|
25
|
-
}
|
|
26
|
-
export declare const MIN_TEMPO_BPM = 70;
|
|
27
|
-
export declare const MAX_TEMPO_BPM = 180;
|
|
28
|
-
export declare const PREFERRED_TEMPO_BPM_MIN = 90;
|
|
29
|
-
export declare const PREFERRED_TEMPO_BPM_MAX = 160;
|
|
30
|
-
export declare const DEFAULT_BASS_END_INDEX = 8;
|
|
31
|
-
/** Longest PCM window passed to music-tempo (seconds), taken from the start of the audio. */
|
|
32
|
-
export declare const MAX_TEMPO_ANALYSIS_SECONDS = 90;
|
|
33
|
-
export declare const frameBassEnergy: (spectrum: number[], bassEndIndex?: number) => number;
|
|
34
|
-
export declare const createBeatDetector: (fps: number, options?: BeatDetectorOptions | undefined) => (spectrum: number[]) => BeatInfo;
|
|
35
|
-
export declare const estimateTempo: (samples: ArrayLike<number>, sampleRate: number, fps: number) => TempoEstimate | null;
|
|
36
|
-
export declare const shiftTempoPhase: (tempo: TempoEstimate, startFrame: number) => TempoEstimate;
|
|
37
|
-
/** Beat-grid phase for a later audio window, using in-window beat times when available. */
|
|
38
|
-
export declare const tempoForWindow: (tempo: TempoEstimate, startFrame: number, fps: number, windowFrames?: number | undefined, localOnsetFrames?: number[] | undefined) => TempoEstimate;
|
|
39
|
-
export declare const beatGridFrameIndices: (tempo: Pick<TempoEstimate, "periodFrames" | "phaseFrame">, totalFrames: number, stride?: number) => number[];
|
|
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 interface TempoEstimate {
|
|
20
|
+
bpm: number;
|
|
21
|
+
periodFrames: number;
|
|
22
|
+
phaseFrame: number;
|
|
23
|
+
/** Beat times in seconds from the start of the analyzed audio. */
|
|
24
|
+
beatsSec?: number[];
|
|
25
|
+
}
|
|
26
|
+
export declare const MIN_TEMPO_BPM = 70;
|
|
27
|
+
export declare const MAX_TEMPO_BPM = 180;
|
|
28
|
+
export declare const PREFERRED_TEMPO_BPM_MIN = 90;
|
|
29
|
+
export declare const PREFERRED_TEMPO_BPM_MAX = 160;
|
|
30
|
+
export declare const DEFAULT_BASS_END_INDEX = 8;
|
|
31
|
+
/** Longest PCM window passed to music-tempo (seconds), taken from the start of the audio. */
|
|
32
|
+
export declare const MAX_TEMPO_ANALYSIS_SECONDS = 90;
|
|
33
|
+
export declare const frameBassEnergy: (spectrum: number[], bassEndIndex?: number) => number;
|
|
34
|
+
export declare const createBeatDetector: (fps: number, options?: BeatDetectorOptions | undefined) => (spectrum: number[]) => BeatInfo;
|
|
35
|
+
export declare const estimateTempo: (samples: ArrayLike<number>, sampleRate: number, fps: number) => TempoEstimate | null;
|
|
36
|
+
export declare const shiftTempoPhase: (tempo: TempoEstimate, startFrame: number) => TempoEstimate;
|
|
37
|
+
/** Beat-grid phase for a later audio window, using in-window beat times when available. */
|
|
38
|
+
export declare const tempoForWindow: (tempo: TempoEstimate, startFrame: number, fps: number, windowFrames?: number | undefined, localOnsetFrames?: number[] | undefined) => TempoEstimate;
|
|
39
|
+
export declare const beatGridFrameIndices: (tempo: Pick<TempoEstimate, "periodFrames" | "phaseFrame">, totalFrames: number, stride?: number) => number[];
|