nodejs-audio-visualizer 5.0.0 → 5.2.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.
Files changed (53) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -2
  3. package/dist/audio.d.ts +13 -12
  4. package/dist/audio.js +128 -120
  5. package/dist/audio.js.map +1 -1
  6. package/dist/beats.d.ts +39 -19
  7. package/dist/beats.js +220 -43
  8. package/dist/beats.js.map +1 -1
  9. package/dist/bpmEncoder.d.ts +14 -14
  10. package/dist/bpmEncoder.js +97 -97
  11. package/dist/config.d.ts +78 -77
  12. package/dist/config.js +377 -369
  13. package/dist/config.js.map +1 -1
  14. package/dist/dsp.d.ts +16 -16
  15. package/dist/dsp.js +76 -76
  16. package/dist/highlight.d.ts +34 -32
  17. package/dist/highlight.js +334 -325
  18. package/dist/highlight.js.map +1 -1
  19. package/dist/image.d.ts +97 -79
  20. package/dist/image.js +545 -458
  21. package/dist/image.js.map +1 -1
  22. package/dist/index.d.ts +87 -79
  23. package/dist/index.js +597 -534
  24. package/dist/index.js.map +1 -1
  25. package/dist/subtitleConvert.d.ts +3 -3
  26. package/dist/subtitleConvert.js +61 -61
  27. package/dist/video.d.ts +150 -80
  28. package/dist/video.js +677 -434
  29. package/dist/video.js.map +1 -1
  30. package/dist/waitForEventLoop.d.ts +1 -1
  31. package/dist/waitForEventLoop.js +5 -5
  32. package/package.json +62 -61
  33. package/src/audio.ts +129 -120
  34. package/src/beats.ts +279 -68
  35. package/src/bpmEncoder.ts +97 -97
  36. package/src/config.ts +487 -476
  37. package/src/dsp.ts +95 -95
  38. package/src/highlight.ts +291 -284
  39. package/src/image.ts +707 -602
  40. package/src/index.ts +124 -31
  41. package/src/subtitleConvert.ts +63 -63
  42. package/src/tests/audio.test.ts +102 -96
  43. package/src/tests/beats.test.ts +134 -0
  44. package/src/tests/config.test.ts +534 -496
  45. package/src/tests/dsp.test.ts +24 -24
  46. package/src/tests/highlight.test.ts +178 -178
  47. package/src/tests/image.test.ts +319 -222
  48. package/src/tests/index.test.ts +180 -180
  49. package/src/tests/subtitleConvert.test.ts +23 -23
  50. package/src/tests/video.test.ts +535 -285
  51. package/src/typings.d.ts +28 -6
  52. package/src/video.ts +856 -550
  53. package/src/waitForEventLoop.ts +2 -2
package/src/beats.ts CHANGED
@@ -1,68 +1,279 @@
1
- export interface BeatInfo {
2
- isBeat: boolean;
3
- intensity: number;
4
- energy: number;
5
- threshold: number;
6
- }
7
-
8
- export interface BeatDetectorOptions {
9
- /** Number of past frames to average for adaptive threshold (~1.5s by default) */
10
- historySize?: number;
11
- /** Energy must exceed average * this multiplier to trigger a beat */
12
- thresholdMultiplier?: number;
13
- /** How fast the beat intensity fades between beats (0-1, lower = faster fade) */
14
- decayRate?: number;
15
- /** Minimum frames between consecutive beats (~150ms by default) */
16
- cooldownFrames?: number;
17
- /** How many low-frequency spectrum bands contribute to energy (sub-bass + bass) */
18
- bassEndIndex?: number;
19
- }
20
-
21
- export const createBeatDetector = (fps: number, options?: BeatDetectorOptions) => {
22
- const historySize = options?.historySize ?? Math.round(fps * 1.5);
23
- const thresholdMultiplier = options?.thresholdMultiplier ?? 1.4;
24
- const decayRate = options?.decayRate ?? 0.85;
25
- const cooldownFrames = options?.cooldownFrames ?? Math.round(fps * 0.15);
26
- const bassEndIndex = options?.bassEndIndex ?? 8;
27
-
28
- const energyHistory: number[] = [];
29
- let framesSinceLastBeat = cooldownFrames;
30
- let decayingIntensity = 0;
31
-
32
- return (spectrum: number[]): BeatInfo => {
33
- const bassSpectrum = spectrum.slice(0, Math.min(bassEndIndex, spectrum.length));
34
- const energy = bassSpectrum.reduce((sum, val) => sum + val * val, 0) / bassSpectrum.length;
35
-
36
- energyHistory.push(energy);
37
- if (energyHistory.length > historySize) {
38
- energyHistory.shift();
39
- }
40
-
41
- const avgEnergy = energyHistory.reduce((sum, e) => sum + e, 0) / energyHistory.length;
42
- const variance = energyHistory.reduce((sum, e) => sum + (e - avgEnergy) ** 2, 0) / energyHistory.length;
43
- const stdDev = Math.sqrt(variance);
44
-
45
- const threshold = avgEnergy + stdDev * thresholdMultiplier;
46
-
47
- framesSinceLastBeat++;
48
-
49
- const hasEnoughHistory = energyHistory.length >= Math.round(historySize / 3);
50
- const isAboveThreshold = energy > threshold && hasEnoughHistory;
51
- const isCooldownOver = framesSinceLastBeat >= cooldownFrames;
52
- const isBeat = isAboveThreshold && isCooldownOver;
53
-
54
- if (isBeat) {
55
- framesSinceLastBeat = 0;
56
- decayingIntensity = Math.min(1, (energy - threshold) / (stdDev + 1e-6));
57
- } else {
58
- decayingIntensity *= decayRate;
59
- }
60
-
61
- return {
62
- isBeat,
63
- intensity: decayingIntensity,
64
- energy,
65
- threshold,
66
- };
67
- };
68
- };
1
+ import MusicTempo from 'music-tempo';
2
+
3
+ export interface BeatInfo {
4
+ isBeat: boolean;
5
+ intensity: number;
6
+ energy: number;
7
+ threshold: number;
8
+ }
9
+
10
+ export interface BeatDetectorOptions {
11
+ /** Number of past frames to average for adaptive threshold (~1.5s by default) */
12
+ historySize?: number;
13
+ /** Energy must exceed average * this multiplier to trigger a beat */
14
+ thresholdMultiplier?: number;
15
+ /** How fast the beat intensity fades between beats (0-1, lower = faster fade) */
16
+ decayRate?: number;
17
+ /** Minimum frames between consecutive beats (~150ms by default) */
18
+ cooldownFrames?: number;
19
+ /** How many low-frequency spectrum bands contribute to energy (sub-bass + bass) */
20
+ bassEndIndex?: number;
21
+ }
22
+
23
+ export interface TempoEstimate {
24
+ bpm: number;
25
+ periodFrames: number;
26
+ phaseFrame: number;
27
+ /** Beat times in seconds from the start of the analyzed audio. */
28
+ beatsSec?: number[];
29
+ }
30
+
31
+ export const MIN_TEMPO_BPM = 70;
32
+ export const MAX_TEMPO_BPM = 180;
33
+ export const PREFERRED_TEMPO_BPM_MIN = 90;
34
+ export const PREFERRED_TEMPO_BPM_MAX = 160;
35
+ export const DEFAULT_BASS_END_INDEX = 8;
36
+ /** Longest PCM window passed to music-tempo (seconds), taken from the start of the audio. */
37
+ export const MAX_TEMPO_ANALYSIS_SECONDS = 90;
38
+ const TEMPO_TIME_STEP = 0.01;
39
+
40
+ export const frameBassEnergy = (spectrum: number[], bassEndIndex = DEFAULT_BASS_END_INDEX): number => {
41
+ const end = Math.min(bassEndIndex, spectrum.length);
42
+ if (end <= 0) {
43
+ return 0;
44
+ }
45
+ let sum = 0;
46
+ for (let i = 0; i < end; i++) {
47
+ const v = spectrum[i];
48
+ sum += v * v;
49
+ }
50
+ return sum / end;
51
+ };
52
+
53
+ export const createBeatDetector = (fps: number, options?: BeatDetectorOptions) => {
54
+ const historySize = options?.historySize ?? Math.round(fps * 1.5);
55
+ const thresholdMultiplier = options?.thresholdMultiplier ?? 1.4;
56
+ const decayRate = options?.decayRate ?? 0.85;
57
+ const cooldownFrames = options?.cooldownFrames ?? Math.round(fps * 0.15);
58
+ const bassEndIndex = options?.bassEndIndex ?? DEFAULT_BASS_END_INDEX;
59
+
60
+ const energyHistory: number[] = [];
61
+ let framesSinceLastBeat = cooldownFrames;
62
+ let decayingIntensity = 0;
63
+
64
+ return (spectrum: number[]): BeatInfo => {
65
+ const energy = frameBassEnergy(spectrum, bassEndIndex);
66
+
67
+ energyHistory.push(energy);
68
+ if (energyHistory.length > historySize) {
69
+ energyHistory.shift();
70
+ }
71
+
72
+ const avgEnergy = energyHistory.reduce((sum, e) => sum + e, 0) / energyHistory.length;
73
+ const variance = energyHistory.reduce((sum, e) => sum + (e - avgEnergy) ** 2, 0) / energyHistory.length;
74
+ const stdDev = Math.sqrt(variance);
75
+
76
+ const threshold = avgEnergy + stdDev * thresholdMultiplier;
77
+
78
+ framesSinceLastBeat++;
79
+
80
+ const hasEnoughHistory = energyHistory.length >= Math.round(historySize / 3);
81
+ const isAboveThreshold = energy > threshold && hasEnoughHistory;
82
+ const isCooldownOver = framesSinceLastBeat >= cooldownFrames;
83
+ const isBeat = isAboveThreshold && isCooldownOver;
84
+
85
+ if (isBeat) {
86
+ framesSinceLastBeat = 0;
87
+ decayingIntensity = Math.min(1, (energy - threshold) / (stdDev + 1e-6));
88
+ } else {
89
+ decayingIntensity *= decayRate;
90
+ }
91
+
92
+ return {
93
+ isBeat,
94
+ intensity: decayingIntensity,
95
+ energy,
96
+ threshold,
97
+ };
98
+ };
99
+ };
100
+
101
+ const foldTempoBpm = (bpm: number): number => {
102
+ let folded = bpm;
103
+ while (folded > MAX_TEMPO_BPM && folded / 2 >= MIN_TEMPO_BPM) {
104
+ folded /= 2;
105
+ }
106
+ while (folded < MIN_TEMPO_BPM && folded * 2 <= MAX_TEMPO_BPM) {
107
+ folded *= 2;
108
+ }
109
+ return folded;
110
+ };
111
+
112
+ const samplesToArray = (samples: ArrayLike<number>, maxLength: number): number[] => {
113
+ const n = Math.min(samples.length, maxLength);
114
+ if (Array.isArray(samples) && n === samples.length) {
115
+ return samples;
116
+ }
117
+ const audioData = new Array<number>(n);
118
+ for (let i = 0; i < n; i++) {
119
+ audioData[i] = samples[i];
120
+ }
121
+ return audioData;
122
+ };
123
+
124
+ const wrapIntoPeriod = (value: number, period: number): number => {
125
+ if (!(period > 0) || !isFinite(value)) {
126
+ return 0;
127
+ }
128
+ let wrapped = value - period * Math.floor(value / period);
129
+ if (wrapped < 0) {
130
+ wrapped += period;
131
+ }
132
+ if (wrapped >= period) {
133
+ wrapped = 0;
134
+ }
135
+ return wrapped;
136
+ };
137
+
138
+ const circularMeanPhase = (frames: number[], period: number): number => {
139
+ if (!(period > 0) || frames.length === 0) {
140
+ return 0;
141
+ }
142
+ let sinSum = 0;
143
+ let cosSum = 0;
144
+ for (const frame of frames) {
145
+ const angle = (2 * Math.PI * frame) / period;
146
+ sinSum += Math.sin(angle);
147
+ cosSum += Math.cos(angle);
148
+ }
149
+ const phase = Math.atan2(sinSum, cosSum) * period / (2 * Math.PI);
150
+ return wrapIntoPeriod(phase, period);
151
+ };
152
+
153
+ export const estimateTempo = (
154
+ samples: ArrayLike<number>,
155
+ sampleRate: number,
156
+ fps: number,
157
+ ): TempoEstimate | null => {
158
+ if (!samples || samples.length === 0 || !(sampleRate > 0) || !(fps > 0)) {
159
+ return null;
160
+ }
161
+
162
+ const maxSamples = Math.max(1, Math.floor(sampleRate * MAX_TEMPO_ANALYSIS_SECONDS));
163
+ const audioData = samplesToArray(samples, maxSamples);
164
+ const hopSize = Math.max(1, Math.round(sampleRate * TEMPO_TIME_STEP));
165
+
166
+ try {
167
+ const mt = new MusicTempo(audioData, {
168
+ hopSize,
169
+ timeStep: TEMPO_TIME_STEP,
170
+ minBeatInterval: 60 / MAX_TEMPO_BPM,
171
+ maxBeatInterval: 60 / MIN_TEMPO_BPM,
172
+ });
173
+ const beatInterval = Number(mt.beatInterval);
174
+ let bpm = Number(mt.tempo);
175
+ if (!(bpm > 0) && beatInterval > 0) {
176
+ bpm = 60 / beatInterval;
177
+ }
178
+ if (!(bpm > 0)) {
179
+ return null;
180
+ }
181
+
182
+ bpm = foldTempoBpm(bpm);
183
+ if (bpm < MIN_TEMPO_BPM * 0.9 || bpm > MAX_TEMPO_BPM * 1.1) {
184
+ return null;
185
+ }
186
+
187
+ const periodFrames = fps * 60 / bpm;
188
+ const beatsSec = (mt.beats ?? [])
189
+ .map(t => Number(t))
190
+ .filter(t => t >= 0 && isFinite(t));
191
+ const beatFrames = beatsSec.map(t => t * fps);
192
+ const phaseFrame = beatFrames.length > 0
193
+ ? wrapIntoPeriod(circularMeanPhase(beatFrames, periodFrames), periodFrames)
194
+ : 0;
195
+
196
+ return {
197
+ bpm: Math.round(bpm),
198
+ periodFrames,
199
+ phaseFrame,
200
+ ...(beatsSec.length > 0 ? { beatsSec } : {}),
201
+ };
202
+ } catch {
203
+ return null;
204
+ }
205
+ };
206
+
207
+ export const shiftTempoPhase = (
208
+ tempo: TempoEstimate,
209
+ startFrame: number,
210
+ ): TempoEstimate => {
211
+ const period = tempo.periodFrames;
212
+ if (!(period > 0) || startFrame === 0) {
213
+ return tempo;
214
+ }
215
+ return {
216
+ ...tempo,
217
+ phaseFrame: wrapIntoPeriod(tempo.phaseFrame - startFrame, period),
218
+ };
219
+ };
220
+
221
+ /** Beat-grid phase for a later audio window, using in-window beat times when available. */
222
+ export const tempoForWindow = (
223
+ tempo: TempoEstimate,
224
+ startFrame: number,
225
+ fps: number,
226
+ windowFrames?: number,
227
+ localOnsetFrames?: number[],
228
+ ): TempoEstimate => {
229
+ const period = tempo.periodFrames;
230
+ if (!(period > 0) || !(fps > 0)) {
231
+ return tempo;
232
+ }
233
+ const startSec = startFrame / fps;
234
+ const endSec = windowFrames != null && windowFrames >= 0
235
+ ? (startFrame + windowFrames) / fps
236
+ : Number.POSITIVE_INFINITY;
237
+ const inWindow = (tempo.beatsSec ?? []).filter(t => t >= startSec && t < endSec);
238
+ if (inWindow.length > 0) {
239
+ const localFrames = inWindow.map(t => (t - startSec) * fps);
240
+ return {
241
+ ...tempo,
242
+ phaseFrame: wrapIntoPeriod(circularMeanPhase(localFrames, period), period),
243
+ };
244
+ }
245
+ const onsets = (localOnsetFrames ?? []).filter(frame =>
246
+ frame > 0 && (windowFrames == null || frame < windowFrames),
247
+ );
248
+ if (onsets.length > 0) {
249
+ return {
250
+ ...tempo,
251
+ phaseFrame: wrapIntoPeriod(circularMeanPhase(onsets, period), period),
252
+ };
253
+ }
254
+ return shiftTempoPhase(tempo, startFrame);
255
+ };
256
+
257
+ export const beatGridFrameIndices = (
258
+ tempo: Pick<TempoEstimate, 'periodFrames' | 'phaseFrame'>,
259
+ totalFrames: number,
260
+ stride = 1,
261
+ ): number[] => {
262
+ const period = tempo.periodFrames;
263
+ const step = Math.max(1, Math.round(stride));
264
+ if (!(period > 0) || totalFrames <= 1) {
265
+ return [];
266
+ }
267
+ const frames: number[] = [];
268
+ const nStart = tempo.phaseFrame > 0 ? 0 : 1;
269
+ for (let n = nStart; ; n += step) {
270
+ const frame = Math.round(tempo.phaseFrame + n * period);
271
+ if (frame >= totalFrames) {
272
+ break;
273
+ }
274
+ if (frame > 0) {
275
+ frames.push(frame);
276
+ }
277
+ }
278
+ return frames;
279
+ };
package/src/bpmEncoder.ts CHANGED
@@ -1,97 +1,97 @@
1
- export interface EncodedBmp {
2
- shiftPos: number;
3
- rowBytes: number;
4
- data: Buffer;
5
- }
6
-
7
- const writeBmpHeader = (buffer: Buffer, width: number, height: number, rgbSize: number) => {
8
- const headerInfoSize = 40;
9
- const offset = 54;
10
- const fileSize = rgbSize + offset;
11
-
12
- let pos = 0;
13
- buffer.write('BM', pos, 2); pos += 2;
14
- buffer.writeUInt32LE(fileSize, pos); pos += 4;
15
- buffer.writeUInt32LE(0, pos); pos += 4;
16
- buffer.writeUInt32LE(offset, pos); pos += 4;
17
-
18
- buffer.writeUInt32LE(headerInfoSize, pos); pos += 4;
19
- buffer.writeUInt32LE(width, pos); pos += 4;
20
- buffer.writeInt32LE(-height, pos); pos += 4;
21
- buffer.writeUInt16LE(1, pos); pos += 2;
22
- buffer.writeUInt16LE(24, pos); pos += 2;
23
- buffer.writeUInt32LE(0, pos); pos += 4;
24
- buffer.writeUInt32LE(rgbSize, pos); pos += 4;
25
- buffer.writeUInt32LE(0, pos); pos += 4;
26
- buffer.writeUInt32LE(0, pos); pos += 4;
27
- buffer.writeUInt32LE(0, pos); pos += 4;
28
- buffer.writeUInt32LE(0, pos); pos += 4;
29
-
30
- return offset;
31
- };
32
-
33
- export const createBpmEncoder = (imgData: { width: number; height: number; }) => {
34
- const width = imgData.width;
35
- const height = imgData.height;
36
- const extraBytes = width % 4;
37
- const rgbSize = height * (3 * width + extraBytes);
38
- const offset = 54;
39
-
40
- return (buffer: Buffer): EncodedBmp => {
41
- const tempBuffer = Buffer.alloc(offset + rgbSize);
42
- const pos = writeBmpHeader(tempBuffer, width, height, rgbSize);
43
-
44
- let i = 0;
45
- const rowBytes = 3 * width + extraBytes;
46
-
47
- for (let y = 0; y < height; y++) {
48
- for (let x = 0; x < width; x++) {
49
- const p = pos + y * rowBytes + x * 3;
50
- i++; // a
51
- tempBuffer[p] = buffer[i++]; // b
52
- tempBuffer[p + 1] = buffer[i++]; // g
53
- tempBuffer[p + 2] = buffer[i++]; // r
54
- }
55
- if (extraBytes > 0) {
56
- const fillOffset = pos + y * rowBytes + width * 3;
57
- tempBuffer.fill(0, fillOffset, fillOffset + extraBytes);
58
- }
59
- }
60
-
61
- return {
62
- shiftPos: pos,
63
- rowBytes,
64
- data: tempBuffer
65
- };
66
- };
67
- };
68
-
69
- export const createBgrFrameEncoder = (imgData: { width: number; height: number }) => {
70
- const width = imgData.width;
71
- const height = imgData.height;
72
- const extraBytes = width % 4;
73
- const rowBytes = 3 * width + extraBytes;
74
- const rgbSize = height * rowBytes;
75
- const offset = 54;
76
-
77
- const tempBuffer = Buffer.alloc(offset + rgbSize);
78
- const pos = writeBmpHeader(tempBuffer, width, height, rgbSize);
79
-
80
- return (bgrBuffer: Buffer): EncodedBmp => {
81
- if (extraBytes === 0) {
82
- bgrBuffer.copy(tempBuffer, pos, 0, width * height * 3);
83
- } else {
84
- for (let y = 0; y < height; y++) {
85
- const srcOffset = y * width * 3;
86
- const dstOffset = pos + y * rowBytes;
87
- bgrBuffer.copy(tempBuffer, dstOffset, srcOffset, srcOffset + width * 3);
88
- }
89
- }
90
-
91
- return {
92
- shiftPos: pos,
93
- rowBytes,
94
- data: tempBuffer
95
- };
96
- };
97
- };
1
+ export interface EncodedBmp {
2
+ shiftPos: number;
3
+ rowBytes: number;
4
+ data: Buffer;
5
+ }
6
+
7
+ const writeBmpHeader = (buffer: Buffer, width: number, height: number, rgbSize: number) => {
8
+ const headerInfoSize = 40;
9
+ const offset = 54;
10
+ const fileSize = rgbSize + offset;
11
+
12
+ let pos = 0;
13
+ buffer.write('BM', pos, 2); pos += 2;
14
+ buffer.writeUInt32LE(fileSize, pos); pos += 4;
15
+ buffer.writeUInt32LE(0, pos); pos += 4;
16
+ buffer.writeUInt32LE(offset, pos); pos += 4;
17
+
18
+ buffer.writeUInt32LE(headerInfoSize, pos); pos += 4;
19
+ buffer.writeUInt32LE(width, pos); pos += 4;
20
+ buffer.writeInt32LE(-height, pos); pos += 4;
21
+ buffer.writeUInt16LE(1, pos); pos += 2;
22
+ buffer.writeUInt16LE(24, pos); pos += 2;
23
+ buffer.writeUInt32LE(0, pos); pos += 4;
24
+ buffer.writeUInt32LE(rgbSize, pos); pos += 4;
25
+ buffer.writeUInt32LE(0, pos); pos += 4;
26
+ buffer.writeUInt32LE(0, pos); pos += 4;
27
+ buffer.writeUInt32LE(0, pos); pos += 4;
28
+ buffer.writeUInt32LE(0, pos); pos += 4;
29
+
30
+ return offset;
31
+ };
32
+
33
+ export const createBpmEncoder = (imgData: { width: number; height: number; }) => {
34
+ const width = imgData.width;
35
+ const height = imgData.height;
36
+ const extraBytes = width % 4;
37
+ const rgbSize = height * (3 * width + extraBytes);
38
+ const offset = 54;
39
+
40
+ return (buffer: Buffer): EncodedBmp => {
41
+ const tempBuffer = Buffer.alloc(offset + rgbSize);
42
+ const pos = writeBmpHeader(tempBuffer, width, height, rgbSize);
43
+
44
+ let i = 0;
45
+ const rowBytes = 3 * width + extraBytes;
46
+
47
+ for (let y = 0; y < height; y++) {
48
+ for (let x = 0; x < width; x++) {
49
+ const p = pos + y * rowBytes + x * 3;
50
+ i++; // a
51
+ tempBuffer[p] = buffer[i++]; // b
52
+ tempBuffer[p + 1] = buffer[i++]; // g
53
+ tempBuffer[p + 2] = buffer[i++]; // r
54
+ }
55
+ if (extraBytes > 0) {
56
+ const fillOffset = pos + y * rowBytes + width * 3;
57
+ tempBuffer.fill(0, fillOffset, fillOffset + extraBytes);
58
+ }
59
+ }
60
+
61
+ return {
62
+ shiftPos: pos,
63
+ rowBytes,
64
+ data: tempBuffer
65
+ };
66
+ };
67
+ };
68
+
69
+ export const createBgrFrameEncoder = (imgData: { width: number; height: number }) => {
70
+ const width = imgData.width;
71
+ const height = imgData.height;
72
+ const extraBytes = width % 4;
73
+ const rowBytes = 3 * width + extraBytes;
74
+ const rgbSize = height * rowBytes;
75
+ const offset = 54;
76
+
77
+ const tempBuffer = Buffer.alloc(offset + rgbSize);
78
+ const pos = writeBmpHeader(tempBuffer, width, height, rgbSize);
79
+
80
+ return (bgrBuffer: Buffer): EncodedBmp => {
81
+ if (extraBytes === 0) {
82
+ bgrBuffer.copy(tempBuffer, pos, 0, width * height * 3);
83
+ } else {
84
+ for (let y = 0; y < height; y++) {
85
+ const srcOffset = y * width * 3;
86
+ const dstOffset = pos + y * rowBytes;
87
+ bgrBuffer.copy(tempBuffer, dstOffset, srcOffset, srcOffset + width * 3);
88
+ }
89
+ }
90
+
91
+ return {
92
+ shiftPos: pos,
93
+ rowBytes,
94
+ data: tempBuffer
95
+ };
96
+ };
97
+ };