nodejs-audio-visualizer 1.2.0 → 2.1.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/src/config.ts CHANGED
@@ -1,14 +1,38 @@
1
1
  import path from 'path';
2
- import { Config } from './index';
2
+ import { Config, SpectrumSizeValue, PositionAliasName, RotationAliasName } from './index';
3
3
 
4
4
  export const defaults = {
5
5
  fps: 60,
6
- spectrumWidth: 0.4,
7
- spectrumHeight: 0.1
6
+ spectrumWidth: '40%',
7
+ spectrumHeight: '10%',
8
+ spectrumX: 'center',
9
+ spectrumY: 'top',
10
+ spectrumRotation: 'down',
11
+ spectrumBusesCount: 64,
12
+ spectrumBusMargin: 4,
13
+ spectrumOpacity: '100%',
8
14
  };
9
15
 
10
- const checkIsInt = (num: number) =>
11
- num % 1 === 0;
16
+ type RelativePositionValue = 0 | 0.5 | 1;
17
+
18
+ interface PositionAlias {
19
+ name: PositionAliasName;
20
+ value: RelativePositionValue;
21
+ }
22
+
23
+ const postitionAliases: PositionAlias[] = [
24
+ { name: 'left', value: 0 },
25
+ { name: 'center', value: 0.5 },
26
+ { name: 'right', value: 1 },
27
+ { name: 'top', value: 0 },
28
+ { name: 'middle', value: 0.5 },
29
+ { name: 'bottom', value: 1 },
30
+ ];
31
+
32
+ export const rotationAliasValues = ['up', 'down'] as const;
33
+
34
+ export const checkIsPercentValue = (value: string) =>
35
+ RegExp(/\d+%/).test(value)
12
36
 
13
37
  export const getAudioFilePath = (config: Config) =>
14
38
  path.resolve(config.audio.path);
@@ -22,19 +46,46 @@ export const getOutVideoPath = (config: Config) =>
22
46
  export const getFPS = (config: Config) =>
23
47
  config.outVideo.fps || defaults.fps;
24
48
 
49
+ export const getSpectrumBusesCount = () => defaults.spectrumBusesCount;
50
+
51
+ export const getSpectrumBusMargin = () => defaults.spectrumBusMargin;
52
+
25
53
  const getSpectrumWidth = (config: Config) =>
26
54
  (config.outVideo.spectrum && config.outVideo.spectrum.width) ||
27
55
  defaults.spectrumWidth;
28
56
 
57
+ const getSpectrumSizeAbsolute = (
58
+ spectrumSize: SpectrumSizeValue,
59
+ backgroundImageSize: number,
60
+ fieldName: string
61
+ ) => {
62
+ if (typeof spectrumSize === 'number') {
63
+ return spectrumSize;
64
+ }
65
+ const isPercentValue = checkIsPercentValue(spectrumSize);
66
+ if (!isPercentValue) {
67
+ throw new Error(getSpectrumSizeValueErrorMessage(fieldName, spectrumSize));
68
+ }
69
+ const percentValue = parseInt(spectrumSize) / 100;
70
+ return backgroundImageSize * percentValue;
71
+ };
72
+
73
+ const getSpectrumSizeValueErrorMessage = (
74
+ fieldName: string, value: SpectrumSizeValue
75
+ ) =>
76
+ `Invalid spectrum ${fieldName} value: '${value}'. Use number value or percent value in string, for example: '30%'.`
77
+
29
78
  export const getSpectrumWidthAbsolute = (
30
79
  config: Config, backgroundImageWidth: number
31
80
  ) => {
32
81
  const spectrumWidth = getSpectrumWidth(config);
33
- const isAbsoluteValue = checkIsInt(spectrumWidth);
34
- if (isAbsoluteValue) {
35
- return spectrumWidth;
82
+ const spectrumWidthAbsolute = getSpectrumSizeAbsolute(spectrumWidth, backgroundImageWidth, 'width');
83
+ const spectrumWidthWithoutMargin =
84
+ Math.trunc(spectrumWidthAbsolute / defaults.spectrumBusesCount) - defaults.spectrumBusMargin / 2;
85
+ if (spectrumWidthWithoutMargin <= 0) {
86
+ throw new Error(`Spectrum width '${spectrumWidth}' is too small.`);
36
87
  }
37
- return backgroundImageWidth * spectrumWidth;
88
+ return spectrumWidthAbsolute;
38
89
  };
39
90
 
40
91
  const getSpectrumHeight = (config: Config) =>
@@ -45,15 +96,140 @@ export const getSpectrumHeightAbsolute = (
45
96
  config: Config, backgroundImageHeight: number
46
97
  ) => {
47
98
  const spectrumHeight = getSpectrumHeight(config);
48
- const isAbsoluteValue = checkIsInt(spectrumHeight);
49
- if (isAbsoluteValue) {
50
- return spectrumHeight;
99
+ return getSpectrumSizeAbsolute(spectrumHeight, backgroundImageHeight, 'height');
100
+ };
101
+
102
+ const getSpectrumX = (config: Config) =>
103
+ parseNumberIfPossible(
104
+ (config.outVideo.spectrum && config.outVideo.spectrum.x) ||
105
+ defaults.spectrumX
106
+ );
107
+
108
+ const getSpectrumY = (config: Config) =>
109
+ parseNumberIfPossible(
110
+ (config.outVideo.spectrum && config.outVideo.spectrum.y) ||
111
+ defaults.spectrumY
112
+ );
113
+
114
+ const parseNumberIfPossible = (value: string | number) => {
115
+ const parsed = Number(value);
116
+ if (isNaN(parsed)) {
117
+ return value;
51
118
  }
52
- return backgroundImageHeight * spectrumHeight;
119
+ return parsed;
120
+ };
121
+
122
+ const parsePositionAlias = (aliasName: string): PositionAlias => {
123
+ const nameNormalized = aliasName.trim().toLowerCase();
124
+ const alias = postitionAliases.find(
125
+ als => als.name === nameNormalized
126
+ );
127
+ if (!alias) {
128
+ throw new Error();
129
+ }
130
+ return alias;
131
+ };
132
+
133
+ const getValidPositionAliasValues = () =>
134
+ postitionAliases.map(aliasEl => aliasEl.name).join(', ');
135
+
136
+ const getSpectrumAbsolutePosition = (
137
+ relativePosition: RelativePositionValue,
138
+ spectrumSize: number,
139
+ backgroundImageSize: number
140
+ ) => {
141
+ switch (relativePosition) {
142
+ case 0:
143
+ return 0;
144
+ case 0.5:
145
+ return relativePosition * backgroundImageSize - spectrumSize / 2;
146
+ case 1:
147
+ return relativePosition * backgroundImageSize - spectrumSize;
148
+ default:
149
+ throw new Error();
150
+ }
151
+ };
152
+
153
+ export const getSpectrumXAbsolute = (
154
+ config: Config,
155
+ spectrumWidth: number,
156
+ backgroundImageWidth: number
157
+ ) => {
158
+ const spectrumX = getSpectrumX(config);
159
+ if (typeof spectrumX !== 'string') {
160
+ return spectrumX;
161
+ }
162
+ try {
163
+ const spectrumXParsed = parsePositionAlias(spectrumX);
164
+ return getSpectrumAbsolutePosition(
165
+ spectrumXParsed.value,
166
+ spectrumWidth,
167
+ backgroundImageWidth
168
+ );
169
+ } catch {
170
+ throw new Error(`Invalid spectrum x value: ${spectrumX}. Valid values: ${getValidPositionAliasValues()}.`);
171
+ }
172
+ };
173
+
174
+ export const getSpectrumYAbsolute = (
175
+ config: Config,
176
+ spectrumHeight: number,
177
+ backgroundImageHeight: number
178
+ ) => {
179
+ const spectrumY = getSpectrumY(config);
180
+ if (typeof spectrumY !== 'string') {
181
+ return spectrumY;
182
+ }
183
+ try {
184
+ const spectrumXParsed = parsePositionAlias(spectrumY);
185
+ return getSpectrumAbsolutePosition(
186
+ spectrumXParsed.value,
187
+ spectrumHeight,
188
+ backgroundImageHeight
189
+ );
190
+ } catch {
191
+ throw new Error(`Invalid spectrum y value: ${spectrumY}. Valid values: ${getValidPositionAliasValues()}.`);
192
+ }
193
+ };
194
+
195
+ const getValidRotationAliasValues = () =>
196
+ rotationAliasValues.join(', ');
197
+
198
+ export const checkIsValidRotationAlias = (aliasName: string) => {
199
+ return !!rotationAliasValues.find(al => al === aliasName);
200
+ };
201
+
202
+ export const getSpectrumRotation = (config: Config): RotationAliasName => {
203
+ const rotationValue =
204
+ (config.outVideo.spectrum && config.outVideo.spectrum.rotation) ||
205
+ defaults.spectrumRotation;
206
+ if (!checkIsValidRotationAlias(rotationValue)) {
207
+ throw new Error(`Invalid spectrum rotation value: ${rotationValue}. Valid values: ${getValidRotationAliasValues}.`);
208
+ }
209
+ return rotationValue as RotationAliasName;
53
210
  };
54
211
 
55
212
  export const getSpectrumColor = (config: Config) =>
56
- (config.outVideo.spectrum && config.outVideo.spectrum.color);
213
+ config.outVideo.spectrum && config.outVideo.spectrum.color;
214
+
215
+ export const getSpectrumOpacity = (config: Config) =>
216
+ (config.outVideo.spectrum && config.outVideo.spectrum.opacity) ||
217
+ defaults.spectrumOpacity;
218
+
219
+ export const getSpectrumOpacityParsed = (config: Config) => {
220
+ const spectrumOpacity = getSpectrumOpacity(config);
221
+ if (!checkIsPercentValue(spectrumOpacity)) {
222
+ throw new Error(`Invalid spectrum opacity value: '${spectrumOpacity}'. Use string percent value, for example '80%'.`);
223
+ }
224
+ const percentValue = parseInt(spectrumOpacity);
225
+ if (
226
+ (percentValue < 0) ||
227
+ (percentValue > 100)
228
+ ) {
229
+ throw new Error(`Invalid spectrum opacity value: '${spectrumOpacity}'. Percent values must be in range from '0%' to '100%'.`);
230
+ }
231
+ return percentValue / 100;
232
+ };
57
233
 
58
234
  export const getFfmpeg_cfr = (config: Config) =>
59
235
  config.tweaks && config.tweaks.ffmpeg_cfr;
package/src/image.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { RotationAliasName } from './index';
1
2
  import { decode, encode, BmpDecoder } from 'bmp-js';
2
3
  import Jimp from 'jimp';
3
4
 
@@ -17,22 +18,94 @@ interface Size {
17
18
  height: number;
18
19
  }
19
20
 
20
- export const drawRect = (imageDstBuffer: BmpDecoder, position: Position, size: Size, color: Color) => {
21
+ interface MixColorProps {
22
+ color: Color;
23
+ colorOpacity: number;
24
+ backgroundColor: Color;
25
+ }
26
+
27
+ export const mixValues =(value1: number, value1Opacity: number, value2: number) =>
28
+ Math.round(value2 * (1 - value1Opacity) + value1 * value1Opacity);
29
+
30
+ export const mixColors = ({
31
+ color,
32
+ colorOpacity,
33
+ backgroundColor,
34
+ }: MixColorProps): Color => {
35
+ return {
36
+ red: mixValues(color.red, colorOpacity, backgroundColor.red),
37
+ green: mixValues(color.green, colorOpacity, backgroundColor.green),
38
+ blue: mixValues(color.blue, colorOpacity, backgroundColor.blue),
39
+ };
40
+ };
41
+
42
+ interface DrawRectPorps {
43
+ imageDstBuffer: BmpDecoder;
44
+ position: Position;
45
+ size: Size;
46
+ color: Color;
47
+ opacity: number;
48
+ }
49
+
50
+ export const drawRect = ({
51
+ imageDstBuffer,
52
+ position,
53
+ size,
54
+ color,
55
+ opacity,
56
+ }: DrawRectPorps) => {
21
57
  for (var currY = position.y; currY < position.y + size.height; currY++) {
22
58
  for (var currX = position.x; currX < position.x + size.width; currX++) {
23
59
  var idx = (imageDstBuffer.width * currY + currX) << 2;
24
-
25
- imageDstBuffer.data[idx + 1] = color.blue;
26
- imageDstBuffer.data[idx + 2] = color.green;
27
- imageDstBuffer.data[idx + 3] = color.red;
60
+ if (opacity === 1) {
61
+ imageDstBuffer.data[idx + 1] = color.blue;
62
+ imageDstBuffer.data[idx + 2] = color.green;
63
+ imageDstBuffer.data[idx + 3] = color.red;
64
+ continue;
65
+ }
66
+
67
+ const backgroundPixelColor: Color = {
68
+ red: imageDstBuffer.data[idx + 3],
69
+ green: imageDstBuffer.data[idx + 2],
70
+ blue: imageDstBuffer.data[idx + 1],
71
+ };
72
+ const resultColor = mixColors({
73
+ color,
74
+ colorOpacity: opacity,
75
+ backgroundColor: backgroundPixelColor,
76
+ });
77
+ imageDstBuffer.data[idx + 1] = resultColor.blue;
78
+ imageDstBuffer.data[idx + 2] = resultColor.green;
79
+ imageDstBuffer.data[idx + 3] = resultColor.red;
28
80
  }
29
81
  }
30
82
  };
31
83
 
32
- const drawSpectrum = (imageDstBuffer: BmpDecoder, spectrum: number[], size: Size, color: Color) => {
33
- const paddingLeft = Math.trunc(imageDstBuffer.width / 2 - size.width / 2);
84
+ interface DrawSpectrumProps {
85
+ imageDstBuffer: BmpDecoder;
86
+ spectrum: number[];
87
+ size: Size;
88
+ position: Position;
89
+ rotation: RotationAliasName;
90
+ margin: number;
91
+ color: Color;
92
+ opacity: number;
93
+ }
94
+
95
+ const drawSpectrum = ({
96
+ imageDstBuffer,
97
+ spectrum,
98
+ size,
99
+ position,
100
+ rotation,
101
+ margin,
102
+ color,
103
+ opacity,
104
+ }: DrawSpectrumProps) => {
34
105
  const busWidth = Math.trunc(size.width / spectrum.length);
35
- const margin = 4;
106
+ const left = Math.trunc(position.x);
107
+ const top = Math.trunc(position.y);
108
+ const height = Math.trunc(size.height);
36
109
 
37
110
  for (let spectrumX = 0; spectrumX < spectrum.length; spectrumX++) {
38
111
  const spectrumValue = spectrum[spectrumX];
@@ -40,26 +113,69 @@ const drawSpectrum = (imageDstBuffer: BmpDecoder, spectrum: number[], size: Size
40
113
  throw new Error('Spectrum values must be in range from 0 to 1');
41
114
  }
42
115
 
43
- const rectX = paddingLeft + busWidth * spectrumX;
44
- const rectHeight = size.height * spectrumValue;
45
- drawRect(imageDstBuffer, { x: rectX + margin, y: 0 }, { width: busWidth - margin / 2, height: rectHeight }, color);
116
+ const rectX = left + busWidth * spectrumX;
117
+ const rectHeight = Math.trunc(height * spectrumValue);
118
+ const rectY = (rotation === 'up') ? top + height - rectHeight : top;
119
+ drawRect({
120
+ imageDstBuffer,
121
+ position: { x: rectX + margin, y: rectY },
122
+ size: { width: busWidth - margin / 2, height: rectHeight },
123
+ color,
124
+ opacity,
125
+ });
46
126
  }
47
127
  };
48
128
 
49
- export const createVisualizerFrame = (backgroundImageBuffer: BmpDecoder, spectrum: number[], size: Size, color: Color | string) => {
50
- const image = Object.assign({}, backgroundImageBuffer);
51
- image.data = Buffer.from(image.data);
129
+ interface CreateVisualizerFrameProps {
130
+ backgroundImageBuffer: BmpDecoder;
131
+ spectrum: number[];
132
+ size: Size;
133
+ position: Position;
134
+ rotation: RotationAliasName;
135
+ margin: number;
136
+ color: Color | string;
137
+ opacity: number;
138
+ }
139
+
140
+ export const createVisualizerFrame = ({
141
+ backgroundImageBuffer,
142
+ spectrum,
143
+ size,
144
+ position,
145
+ rotation,
146
+ margin,
147
+ color,
148
+ opacity,
149
+ }: CreateVisualizerFrameProps) => {
150
+ const imageDstBuffer = Object.assign({}, backgroundImageBuffer);
151
+ imageDstBuffer.data = Buffer.from(imageDstBuffer.data);
52
152
  const rgbSpectrumColor = (typeof color === 'string') ? hexToRgb(color) : color;
53
- drawSpectrum(image, spectrum, size, rgbSpectrumColor);
54
- return image;
153
+ drawSpectrum({
154
+ imageDstBuffer,
155
+ spectrum,
156
+ size,
157
+ position,
158
+ rotation,
159
+ margin,
160
+ color: rgbSpectrumColor,
161
+ opacity,
162
+ });
163
+ return imageDstBuffer;
55
164
  };
56
165
 
57
166
  export const convertToBmp = async (filePath: string) =>
58
- new Promise<Buffer>(async resolve => {
59
- const image = await Jimp.read(filePath);
60
- image.getBuffer("image/bmp", (err, value) => {
61
- resolve(value);
62
- });
167
+ new Promise<Buffer>(async (resolve, reject) => {
168
+ try {
169
+ const image = await Jimp.read(filePath);
170
+ image.getBuffer("image/bmp", (err, value) => {
171
+ if (err) {
172
+ reject(err);
173
+ }
174
+ resolve(value);
175
+ });
176
+ } catch {
177
+ throw new Error(`Cannot procces image file from path: ${filePath}`);
178
+ }
63
179
  });
64
180
 
65
181
  export const parseImage = (buffer: Buffer) => decode(buffer);
package/src/index.ts CHANGED
@@ -3,12 +3,19 @@ import {
3
3
  getBackgroundImagePath,
4
4
  getOutVideoPath,
5
5
  getFPS,
6
+ getSpectrumBusesCount,
7
+ getSpectrumBusMargin,
6
8
  getSpectrumWidthAbsolute,
7
9
  getSpectrumHeightAbsolute,
10
+ getSpectrumXAbsolute,
11
+ getSpectrumYAbsolute,
8
12
  getSpectrumColor,
13
+ getSpectrumOpacityParsed,
9
14
  getFfmpeg_cfr,
10
15
  getFfmpeg_preset,
11
- getFrame_processing_delay
16
+ getFrame_processing_delay,
17
+ rotationAliasValues,
18
+ getSpectrumRotation
12
19
  } from './config';
13
20
  import { createAudioBuffer, bufferToUInt8, createSpectrumsProcessor } from './audio';
14
21
  import { parseImage, createVisualizerFrame, createImageBuffer, getImageColor, invertColor, Color, convertToBmp } from './image';
@@ -32,9 +39,13 @@ export interface Config {
32
39
  path: string;
33
40
  fps?: number;
34
41
  spectrum?: {
35
- width?: number;
36
- height?: number;
42
+ width?: SpectrumSizeValue;
43
+ height?: SpectrumSizeValue;
44
+ x?: number | PositionAliasName;
45
+ y?: number | PositionAliasName;
46
+ rotation?: RotationAliasName;
37
47
  color?: Color | string;
48
+ opacity?: string;
38
49
  }
39
50
  };
40
51
  tweaks?: {
@@ -44,6 +55,18 @@ export interface Config {
44
55
  };
45
56
  }
46
57
 
58
+ export type SpectrumSizeValue = number | string;
59
+
60
+ export type PositionAliasName =
61
+ 'left' |
62
+ 'center' |
63
+ 'right' |
64
+ 'top' |
65
+ 'middle' |
66
+ 'bottom';
67
+
68
+ export type RotationAliasName = typeof rotationAliasValues[number];
69
+
47
70
  const sleep = (timeout: number) =>
48
71
  new Promise(resolve => setTimeout(resolve, timeout));
49
72
 
@@ -62,18 +85,26 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
62
85
  throw new Error('ffmpeg didn\'t show audio sample rate');
63
86
  }
64
87
 
88
+ const spectrumBusesCount = getSpectrumBusesCount();
89
+ const spectrumBusMargin = getSpectrumBusMargin();
65
90
  const FPS = getFPS(config);
66
91
  const spectrumWidth =
67
92
  getSpectrumWidthAbsolute(config, backgroundImage.width);
68
93
  const spectrumHeight =
69
94
  getSpectrumHeightAbsolute(config, backgroundImage.height);
95
+ const spectrumX =
96
+ getSpectrumXAbsolute(config, spectrumWidth, backgroundImage.width);
97
+ const spectrumY =
98
+ getSpectrumYAbsolute(config, spectrumHeight, backgroundImage.height);
99
+ const spectrumRotation =
100
+ getSpectrumRotation(config);
70
101
  const spectrumColor =
71
102
  getSpectrumColor(config) ||
72
103
  invertColor(getImageColor(backgroundImage));
104
+ const spectrumOpacity = getSpectrumOpacityParsed(config);
73
105
  const ffmpeg_cfr = getFfmpeg_cfr(config);
74
106
  const ffmpeg_preset = getFfmpeg_preset(config);
75
107
  const frame_processing_delay = getFrame_processing_delay(config);
76
- const spectrumBusesCount = 64;
77
108
 
78
109
  const audioDuration = audioBuffer.length / sampleRate;
79
110
  const framesCount = Math.trunc(audioDuration * FPS);
@@ -94,12 +125,16 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
94
125
  const audioDataParser = () =>
95
126
  PCM_FORMAT.parseFunction(audioBuffer, i * audioDataStep, i * audioDataStep + audioDataStep);
96
127
  const spectrum = processSpectrum(i, audioDataParser);
97
- const frameImage = createVisualizerFrame(
98
- backgroundImage,
128
+ const frameImage = createVisualizerFrame({
129
+ backgroundImageBuffer: backgroundImage,
99
130
  spectrum,
100
- { width: spectrumWidth, height: spectrumHeight },
101
- spectrumColor
102
- );
131
+ size: { width: spectrumWidth, height: spectrumHeight },
132
+ position: { x: spectrumX, y: spectrumY },
133
+ rotation: spectrumRotation,
134
+ margin: spectrumBusMargin,
135
+ color: spectrumColor,
136
+ opacity: spectrumOpacity,
137
+ });
103
138
  const frameImageBuffer = createImageBuffer(frameImage);
104
139
  const isFrameProcessed = ffmpegVideoWriter.stdin.write(frameImageBuffer);
105
140
  if (!isFrameProcessed) {