@yeasoft/wav 1.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/CHANGELOG.md +80 -0
- package/LICENSE +22 -0
- package/README.md +169 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/wav-file-writer.d.ts +60 -0
- package/dist/wav-file-writer.js +57 -0
- package/dist/wav-reader.d.ts +94 -0
- package/dist/wav-reader.js +258 -0
- package/dist/wav-writer.d.ts +52 -0
- package/dist/wav-writer.js +155 -0
- package/package.json +52 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.1.0] - 2026-04-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added support for new options that allow an improved control of
|
|
10
|
+
the classes behaviour
|
|
11
|
+
- Added TypeScript declarations with full inline documentation
|
|
12
|
+
- Added example application for `WavFileWriter`
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Made `WavReader` more robust allowing other chunks like "JUNK" to appear
|
|
17
|
+
before "fmt "
|
|
18
|
+
- Refactoring of tests and examples
|
|
19
|
+
- Full reimplementation of `WavReader` based on an internal state machine
|
|
20
|
+
- Removed all external dependencies
|
|
21
|
+
- Refactoring of the original `Reader`, `Writer` and `FileWriter` classes and
|
|
22
|
+
renaming to new unambigous names `WavReader`, `WavWriter` and `WavFileWriter`
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Moved rewriting of final header to `close` handler since under high pressure
|
|
27
|
+
sometimes the file is still not closed and rewriting fails
|
|
28
|
+
|
|
29
|
+
## [1.0.2] - 2018-04-27
|
|
30
|
+
|
|
31
|
+
- Upgrade linting
|
|
32
|
+
- Avoid deprecated Buffer API
|
|
33
|
+
- Writer options and FileWriter example in Readme
|
|
34
|
+
|
|
35
|
+
## [1.0.1] - 2016-07-21
|
|
36
|
+
|
|
37
|
+
- Bump dependencies and cleanup package.json
|
|
38
|
+
- Add semistandard linting
|
|
39
|
+
- Add missing fs.open call
|
|
40
|
+
- Update CI Node.js versions
|
|
41
|
+
- travis, appveyor: test node v0.12 instead of v0.11
|
|
42
|
+
|
|
43
|
+
## [1.0.0] - 2015-05-01
|
|
44
|
+
|
|
45
|
+
- add MIT license file
|
|
46
|
+
- add appveyor.yml file for Windows testing
|
|
47
|
+
- examples: fix comment
|
|
48
|
+
- index: add link to RFC2361
|
|
49
|
+
- reader: add clarifying comment
|
|
50
|
+
- reader: add initial `float` WAV file support
|
|
51
|
+
- reader: add a few more formats defined by the RFC
|
|
52
|
+
- reader: add `formats` map and set `float`, `alaw` and `ulaw` on the "format" object
|
|
53
|
+
- reader: use %o debug v1 formatters
|
|
54
|
+
- reader, writer: always use "readable-stream" copy of Transform
|
|
55
|
+
- package: remove "engines" field
|
|
56
|
+
- package: update all dependency versions
|
|
57
|
+
- README: use svg for Travis badge
|
|
58
|
+
- travis: don't test node v0.7 and v0.9, test v0.11
|
|
59
|
+
|
|
60
|
+
## [0.1.2] - 2014-01-11
|
|
61
|
+
|
|
62
|
+
- package: update `readable-stream` dep to v1.1.10
|
|
63
|
+
- travis: test node v0.10 and v0.11
|
|
64
|
+
- Writer: bypassed `stream-parser` to avoid assertion error (#1, #5)
|
|
65
|
+
|
|
66
|
+
## [0.1.1] - 2013-12-12
|
|
67
|
+
|
|
68
|
+
- Fix package.json repository URL so npm link isn't broken (@cbebry)
|
|
69
|
+
|
|
70
|
+
## [0.1.0] - 2013-03-07
|
|
71
|
+
|
|
72
|
+
- reader: passthrough the audio data chunk until EOF
|
|
73
|
+
- test: begin testing with Travis-ci
|
|
74
|
+
- add experimental RIFX support
|
|
75
|
+
- reader, writer: integrate the "stream-parser" mixin
|
|
76
|
+
- test: add initial Reader tests
|
|
77
|
+
|
|
78
|
+
## [0.0.1] - 2012-02-05
|
|
79
|
+
|
|
80
|
+
- Initial release
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YeaSoft Intl. - Leo Moll
|
|
4
|
+
Copyright (c) 2012 Nathan Rajlich
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
node-wav
|
|
2
|
+
========
|
|
3
|
+
|
|
4
|
+
This module offers streams to help work with Microsoft WAVE files. This module is basically
|
|
5
|
+
a fork of the original module [node-wav](https://github.com/TooTallNate/node-wav), which has
|
|
6
|
+
not been further developed for many years and whose codebase is outdated due to former
|
|
7
|
+
compatibility requirements.
|
|
8
|
+
|
|
9
|
+
The most important thing: **this module has no external dependencies**.
|
|
10
|
+
|
|
11
|
+
Installation
|
|
12
|
+
------------
|
|
13
|
+
|
|
14
|
+
Install through npm:
|
|
15
|
+
|
|
16
|
+
``` bash
|
|
17
|
+
npm install @yeasoft/wav
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Example
|
|
21
|
+
-------
|
|
22
|
+
|
|
23
|
+
Here's how you would play a standard PCM WAVE file out of the speakers using
|
|
24
|
+
`node-wav` and `node-speaker`:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const Speaker = require('speaker');
|
|
29
|
+
const { WavReader } = require('@yeasoft/wav');
|
|
30
|
+
|
|
31
|
+
const file = fs.createReadStream('track01.wav');
|
|
32
|
+
const reader = new WavReader();
|
|
33
|
+
|
|
34
|
+
// the "format" event gets emitted at the end of the WAVE header
|
|
35
|
+
reader.on('format', format => {
|
|
36
|
+
|
|
37
|
+
// the WAVE header is stripped from the output of the reader
|
|
38
|
+
reader.pipe(new Speaker(format));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// pipe the WAVE file to the Reader instance
|
|
42
|
+
file.pipe(reader);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
API
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
The module exports the follwing three classes that can be used to read and write WAVE
|
|
49
|
+
files:
|
|
50
|
+
|
|
51
|
+
- [WavReader(options)](#wavreaderoptions)
|
|
52
|
+
- [WavWriter(options)](#wavwriteroptions)
|
|
53
|
+
- [WavFileWriter()](#wavfilewriterpath-options)
|
|
54
|
+
|
|
55
|
+
Additionally, the following deprecated compatibility aliases are also exported in order
|
|
56
|
+
to provide a drop in replacement of [node-wav](https://github.com/TooTallNate/node-wav):
|
|
57
|
+
|
|
58
|
+
- [Reader(options)](#wavreaderoptions)
|
|
59
|
+
- [Writer(options)](#wavwriteroptions)
|
|
60
|
+
- [FileWriter()](#wavfilewriterpath-options)
|
|
61
|
+
|
|
62
|
+
### WavReader(options)
|
|
63
|
+
|
|
64
|
+
The `WavReader` class accepts a WAV audio file written to it and outputs the raw
|
|
65
|
+
audio data with the WAV header stripped (most of the time, PCM audio data will
|
|
66
|
+
be output, depending on the `audioFormat` property).
|
|
67
|
+
|
|
68
|
+
A `"format"` event gets emitted after the WAV header has been parsed.
|
|
69
|
+
|
|
70
|
+
#### Options
|
|
71
|
+
|
|
72
|
+
Currently there are no specific options of the `WavReader` class but the `options`
|
|
73
|
+
object is also passed verbatim to the base `Transform` class allowing to influence
|
|
74
|
+
the overall behaviour of the class.
|
|
75
|
+
|
|
76
|
+
### WavWriter(options)
|
|
77
|
+
|
|
78
|
+
The `WavWriter` class accepts raw audio data written to it (only PCM audio data is
|
|
79
|
+
currently supported), and outputs a WAV file with a valid WAVE header at the
|
|
80
|
+
beginning specifying the formatting information of the audio stream.
|
|
81
|
+
|
|
82
|
+
Note that there's an interesting problem, because the WAVE header also
|
|
83
|
+
specifies the total byte length of the audio data in the file, and there's no
|
|
84
|
+
way that we can know this ahead of time. Therefore the WAVE header will contain
|
|
85
|
+
a byte-length if `0` initially, which most WAVE decoders will know means to
|
|
86
|
+
just read until `EOF`.
|
|
87
|
+
|
|
88
|
+
Optionally, if you are in a situation where you can seek back to the beginning
|
|
89
|
+
of the destination of the WAVE file (like writing to a regular file, for
|
|
90
|
+
example), then you may listen for the `"header"` event which will be emitted
|
|
91
|
+
_after_ all the data has been written, and you can go back and rewrite the new
|
|
92
|
+
header with proper audio byte length into the beginning of the destination
|
|
93
|
+
(though if your destination _is_ a regular file, you should use the the
|
|
94
|
+
`WavFileWriter` class instead).
|
|
95
|
+
|
|
96
|
+
#### Options
|
|
97
|
+
|
|
98
|
+
The following options can be specified on creation of a `WavWriter` object:
|
|
99
|
+
|
|
100
|
+
- `format`: Format of the audio data. See the [official WAVE format specification][1]
|
|
101
|
+
(default: `0x0001`)
|
|
102
|
+
- `channels`: Number of channels (default: `2`)
|
|
103
|
+
- `sampleRate`: Sample rate (default: `44100`)
|
|
104
|
+
- `bitDepth`: Bits per sample (default: `16`)
|
|
105
|
+
- `bigEndian`: If `true`, a big endian wave file will be generated (default: `false`)
|
|
106
|
+
- `dataLength`: The expected size of the "data" portion of the WAVE file (default: _maximum valid length for a WAVE file_)
|
|
107
|
+
|
|
108
|
+
The `options` object is also passed verbatim to the base `Transform` class allowing
|
|
109
|
+
to influence the overall behaviour of the class.
|
|
110
|
+
|
|
111
|
+
### WavFileWriter(path, options)
|
|
112
|
+
|
|
113
|
+
The `WavFileWriter` class is, essentially, a combination of `fs.createWriteStream()` and
|
|
114
|
+
the above `WavWriter()` class, except it automatically corrects the header after the file
|
|
115
|
+
is written. Options are passed to both `WavWriter()` and `fs.createWriteStream()`.
|
|
116
|
+
|
|
117
|
+
Example usage with `mic`:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
const mic = require('mic'); // requires arecord or sox, see https://www.npmjs.com/package/mic
|
|
121
|
+
const { WavFileWriter } = require('@yeasoft/wav');
|
|
122
|
+
|
|
123
|
+
const micInstance = mic({
|
|
124
|
+
rate: '16000',
|
|
125
|
+
channels: '1',
|
|
126
|
+
debug: true
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const micInputStream = micInstance.getAudioStream();
|
|
130
|
+
|
|
131
|
+
const outputFileStream = new WavFileWriter('./test.wav', {
|
|
132
|
+
sampleRate: 16000,
|
|
133
|
+
channels: 1
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
micInputStream.pipe(outputFileStream);
|
|
137
|
+
|
|
138
|
+
micInstance.start();
|
|
139
|
+
|
|
140
|
+
setTimeout(function() {
|
|
141
|
+
micInstance.stop();
|
|
142
|
+
}, 5000);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Options
|
|
146
|
+
|
|
147
|
+
The following options can be specified on creation of a `WavFileWriter` object:
|
|
148
|
+
|
|
149
|
+
- `fixHeaderOn`: Specifies when to fix the header of the WAVE file after having written the whole audio data:
|
|
150
|
+
- `none`: Do not fix the header. This can be done later using the `fixHeader` method
|
|
151
|
+
- `close`: Fix the header automatically on "_close_" event
|
|
152
|
+
- `header`: Fix the header automatically on "_header_" event (compatibility with `node-wav`)
|
|
153
|
+
- `ignoreFixHeaderError`: If `true`, errors that occur when fixing the headers are silently
|
|
154
|
+
ignored and no "error" event is emitted (default: `false`)
|
|
155
|
+
|
|
156
|
+
The `options` object is also passed verbatim to the base `WavWriter` class allowing
|
|
157
|
+
to influence the overall behaviour of the class.
|
|
158
|
+
|
|
159
|
+
References
|
|
160
|
+
----------
|
|
161
|
+
|
|
162
|
+
- [RFC 2361](http://tools.ietf.org/html/rfc2361)
|
|
163
|
+
- [WAVE Specifications](https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html)
|
|
164
|
+
- [Multimedia Programming Interface and Data Specifications 1.0](https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf)
|
|
165
|
+
- [WAVE File Format](http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/wave.htm)
|
|
166
|
+
- [BEXT Metadata](https://2manyrobots.com/YateResources/InAppHelp/BEXTMetadata.html)
|
|
167
|
+
- [Specification of the Broadcast Wave Format](https://tech.ebu.ch/docs/tech/tech3285.pdf)
|
|
168
|
+
|
|
169
|
+
[1]: https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { WavReader } = require( './wav-reader' );
|
|
4
|
+
const { WavWriter } = require( './wav-writer' );
|
|
5
|
+
const { WavFileWriter } = require( "./wav-file-writer" );
|
|
6
|
+
|
|
7
|
+
module.exports = { WavReader, WavWriter, WavFileWriter, Reader: WavReader, Writer: WavWriter, FileWriter: WavFileWriter };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { WavWriter, WavWriterOptions } from "./wav-writer";
|
|
4
|
+
|
|
5
|
+
/** Options of the {@link WavFileWriter} */
|
|
6
|
+
export interface WavFileWriterOptions extends WavWriterOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Specifies when to fix the header of the WAVE file after having
|
|
9
|
+
* written the whole audio data:
|
|
10
|
+
*
|
|
11
|
+
* - `none`: Do not fix the header. This can be done later using the {@link fixHeader} method
|
|
12
|
+
* - `close`: Fix the header automatically on "close" event
|
|
13
|
+
* - `header`: Fix the header automatically on "header" event (compatibility with `node-wav`)
|
|
14
|
+
*
|
|
15
|
+
* If not specified, the default setting is `close`
|
|
16
|
+
*/
|
|
17
|
+
fixHeaderOn?: 'header' | 'close' | 'none';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* If `true`, errors that occur when fixing the headers are silently
|
|
21
|
+
* ignored and no "error" event is emitted (default: `false`)
|
|
22
|
+
*/
|
|
23
|
+
ignoreFixHeaderError?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The `WavFileWriter` is a subclass of `WavWriter` that can automatically take
|
|
28
|
+
* care of writing the "header" event at the end of the stream to the beginning
|
|
29
|
+
* of the output file.
|
|
30
|
+
*/
|
|
31
|
+
export class WavFileWriter extends WavWriter {
|
|
32
|
+
/**
|
|
33
|
+
* Constructs a {@link WavFileWriter} object.
|
|
34
|
+
*
|
|
35
|
+
* @param path The destination filename for the WAVE file
|
|
36
|
+
* @param options The options of the {@link WavFileWriter}
|
|
37
|
+
*/
|
|
38
|
+
constructor( path: string, options?: WavFileWriterOptions );
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Helper function that can be invoked in order to fix the internal file sizes
|
|
42
|
+
* in the file header after the whole audio data has been written. This method
|
|
43
|
+
* must only be called, if `options.fixHeaderOn = 'none'` was set.
|
|
44
|
+
*
|
|
45
|
+
* @param callback A callback function that gets the result of the operation
|
|
46
|
+
*/
|
|
47
|
+
fixHeader( callback: ( error: Error ) => void ): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The `FileWriter` is a subclass of `WavWriter` that can automatically take
|
|
52
|
+
* care of writing the "header" event at the end of the stream to the beginning
|
|
53
|
+
* of the output file.
|
|
54
|
+
*
|
|
55
|
+
* This old name is only provided for compatibility reasons with the former
|
|
56
|
+
* `node-wav` module. Use {@link WavFileWriter} instead.
|
|
57
|
+
*
|
|
58
|
+
* @deprecated
|
|
59
|
+
*/
|
|
60
|
+
export class FileWriter extends WavFileWriter { }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// activate strict mode
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// load modules
|
|
5
|
+
const fs = require( 'node:fs' );
|
|
6
|
+
const { WavWriter } = require( './wav-writer' );
|
|
7
|
+
|
|
8
|
+
class WavFileWriter extends WavWriter {
|
|
9
|
+
constructor( pathName, options ) {
|
|
10
|
+
super( options );
|
|
11
|
+
|
|
12
|
+
this.path = pathName;
|
|
13
|
+
this.file = fs.createWriteStream( pathName, options );
|
|
14
|
+
|
|
15
|
+
const attachFixHeader = event => {
|
|
16
|
+
this.file.on( event, () => {
|
|
17
|
+
this.fixHeader( error => {
|
|
18
|
+
if ( error && this.options.ignoreFixHeaderError === false ) return this.emit( 'error', error );
|
|
19
|
+
this.emit( 'done' );
|
|
20
|
+
} );
|
|
21
|
+
} );
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
switch ( this.options.fixHeaderOn ) {
|
|
25
|
+
default:
|
|
26
|
+
case 'close':
|
|
27
|
+
attachFixHeader( 'close' );
|
|
28
|
+
break;
|
|
29
|
+
case 'header':
|
|
30
|
+
attachFixHeader( 'header' );
|
|
31
|
+
break;
|
|
32
|
+
case 'none':
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.pipe( this.file );
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fixHeader( callback ) {
|
|
40
|
+
fs.open( this.path, 'r+', ( error, fd ) => {
|
|
41
|
+
if ( error ) return callback( error );
|
|
42
|
+
|
|
43
|
+
fs.write( fd, this.header, 0, this.header.length, 0, ( error, bytesWritten ) => {
|
|
44
|
+
|
|
45
|
+
fs.close( fd, errorClose => {
|
|
46
|
+
if ( error ) return callback( error );
|
|
47
|
+
if ( bytesWritten !== this.header.length ) return callback( new Error( 'problem writing "header" data' ) );
|
|
48
|
+
if ( errorClose ) return callback( errorClose );
|
|
49
|
+
|
|
50
|
+
callback();
|
|
51
|
+
} );
|
|
52
|
+
} );
|
|
53
|
+
} );
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
exports.WavFileWriter = WavFileWriter;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { Transform, TransformOptions } from "node:stream";
|
|
4
|
+
|
|
5
|
+
/** Options of the {@link WavReader} */
|
|
6
|
+
export interface WavReaderOptions extends TransformOptions {
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Format information of the read WAVE stream as emitted by the "format" event */
|
|
10
|
+
export interface WavReaderFormat {
|
|
11
|
+
/** Format of the audio data */
|
|
12
|
+
audioFormat: number;
|
|
13
|
+
/** Endianess of the audio data: LE = little endian, BE = big endian */
|
|
14
|
+
endianness: 'LE' | 'BE';
|
|
15
|
+
/** Number of audio channels */
|
|
16
|
+
channels: number;
|
|
17
|
+
/** Sample rate in samples per second */
|
|
18
|
+
sampleRate: number;
|
|
19
|
+
/** Byte rate in bytes per second */
|
|
20
|
+
byteRate: number;
|
|
21
|
+
/** Block alignment in bytes */
|
|
22
|
+
blockAlign: number;
|
|
23
|
+
/** Bits per sample */
|
|
24
|
+
bitDepth: number;
|
|
25
|
+
/** `true` if data is signed, `false` if data is unsigned */
|
|
26
|
+
signed: boolean;
|
|
27
|
+
/** `true` if `audioFormat` is `WAVE_FORMAT_IEEE_FLOAT` */
|
|
28
|
+
float?: boolean;
|
|
29
|
+
/** `true` if `audioFormat` is `WAVE_FORMAT_ALAW` */
|
|
30
|
+
alaw?: boolean;
|
|
31
|
+
/** `true` if `audioFormat` is `WAVE_FORMAT_ULAW` */
|
|
32
|
+
ulaw?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Unprocessed chunks information of the read WAVE stream as emitted by the "chunk" event */
|
|
36
|
+
export interface WavReaderChunk {
|
|
37
|
+
/** ID of the chunk */
|
|
38
|
+
id: string;
|
|
39
|
+
/** size in bytes of the chunk */
|
|
40
|
+
size: number;
|
|
41
|
+
/** The chunk data */
|
|
42
|
+
data: Buffer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The `WavReader` class accepts a WAVE audio file, emits a "format" event, and
|
|
47
|
+
* outputs the raw "data" from the WAVE file (usually raw PCM data, but if the
|
|
48
|
+
* WAVE file uses compression then the compressed data will be output, you are
|
|
49
|
+
* responsible for uncompressing in that case if necessary).
|
|
50
|
+
*/
|
|
51
|
+
export class WavReader extends Transform {
|
|
52
|
+
/** detected 'RIFF' id of the stream */
|
|
53
|
+
riffId: string;
|
|
54
|
+
/** detected 'WAVE' id of the stream */
|
|
55
|
+
waveId: string;
|
|
56
|
+
/** detected 'fmt ' id of the stream */
|
|
57
|
+
chunkId: string;
|
|
58
|
+
/** the size of the RIFF chunk */
|
|
59
|
+
chunkSize: number;
|
|
60
|
+
/** the sizes of all encoutnered chunks */
|
|
61
|
+
chunkSizes: Record<string, number>;
|
|
62
|
+
/** the size of the 'fmt ' chunk */
|
|
63
|
+
subchunk1Size: number;
|
|
64
|
+
/** the size of the 'data' chunk */
|
|
65
|
+
subchunk2Size: number;
|
|
66
|
+
|
|
67
|
+
/** Format of the audio data */
|
|
68
|
+
audioFormat: number;
|
|
69
|
+
/** Endianess of the audio data: LE = little endian, BE = big endian */
|
|
70
|
+
endianness: 'LE' | 'BE';
|
|
71
|
+
/** Number of audio channels */
|
|
72
|
+
channels: number;
|
|
73
|
+
/** Sample rate in samples per second */
|
|
74
|
+
sampleRate: number;
|
|
75
|
+
/** Byte rate in bytes per second */
|
|
76
|
+
byteRate: number;
|
|
77
|
+
/** Block alignment in bytes */
|
|
78
|
+
blockAlign: number;
|
|
79
|
+
/** Bits per sample */
|
|
80
|
+
bitDepth: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The `Reader` class accepts a WAVE audio file, emits a "format" event, and
|
|
85
|
+
* outputs the raw "data" from the WAVE file (usually raw PCM data, but if the
|
|
86
|
+
* WAVE file uses compression then the compressed data will be output, you are
|
|
87
|
+
* responsible for uncompressing in that case if necessary).
|
|
88
|
+
*
|
|
89
|
+
* This old name is only provided for compatibility reasons with the former
|
|
90
|
+
* `node-wav` module. Use {@link WavReader} instead.
|
|
91
|
+
*
|
|
92
|
+
* @deprecated
|
|
93
|
+
*/
|
|
94
|
+
export class Reader extends WavReader { }
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
|
|
2
|
+
// activate strict mode
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
// load modules
|
|
6
|
+
const { Transform } = require( 'node:stream' );
|
|
7
|
+
|
|
8
|
+
// Values for the `audioFormat` byte.
|
|
9
|
+
const formats = {
|
|
10
|
+
WAVE_FORMAT_UNKNOWN: 0x0000, // Microsoft Unknown Wave Format
|
|
11
|
+
WAVE_FORMAT_PCM: 0x0001, // Microsoft PCM Format
|
|
12
|
+
WAVE_FORMAT_ADPCM: 0x0002, // Microsoft ADPCM Format
|
|
13
|
+
WAVE_FORMAT_IEEE_FLOAT: 0x0003, // IEEE float
|
|
14
|
+
WAVE_FORMAT_VSELP: 0x0004, // Compaq Computer's VSELP
|
|
15
|
+
WAVE_FORMAT_IBM_CVSD: 0x0005, // IBM CVSD
|
|
16
|
+
WAVE_FORMAT_ALAW: 0x0006, // 8-bit ITU-T G.711 A-law
|
|
17
|
+
WAVE_FORMAT_MULAW: 0x0007, // 8-bit ITU-T G.711 µ-law
|
|
18
|
+
WAVE_FORMAT_EXTENSIBLE: 0xFFFE // Determined by SubFormat
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// States of the internal state machine
|
|
22
|
+
const states = {
|
|
23
|
+
PROCESS_HEADER: 0,
|
|
24
|
+
PROCESS_DETECT_CHUNK: 1,
|
|
25
|
+
PROCESS_FORM: 2,
|
|
26
|
+
PROCESS_FACT: 3,
|
|
27
|
+
PROCESS_DATA: 50,
|
|
28
|
+
PROCESS_UNKNOWN: 99,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WavReader extends Transform {
|
|
33
|
+
constructor( options ) {
|
|
34
|
+
super( options );
|
|
35
|
+
|
|
36
|
+
this.options = options instanceof Object ? options : {};
|
|
37
|
+
this.endianness = 'LE';
|
|
38
|
+
this.chunkSizes = {};
|
|
39
|
+
|
|
40
|
+
this._initState( states.PROCESS_HEADER, 12 );
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_transform( chunk, encoding, callback ) {
|
|
44
|
+
switch ( this.state ) {
|
|
45
|
+
case states.PROCESS_HEADER:
|
|
46
|
+
this._processHeader( chunk, encoding, callback );
|
|
47
|
+
break;
|
|
48
|
+
case states.PROCESS_DETECT_CHUNK:
|
|
49
|
+
this._detectChunk( chunk, encoding, callback );
|
|
50
|
+
break;
|
|
51
|
+
case states.PROCESS_FORM:
|
|
52
|
+
this._processForm( chunk, encoding, callback );
|
|
53
|
+
break;
|
|
54
|
+
case states.PROCESS_FACT:
|
|
55
|
+
this._processFact( chunk, encoding, callback );
|
|
56
|
+
break;
|
|
57
|
+
case states.PROCESS_DATA:
|
|
58
|
+
this._processData( chunk, encoding, callback );
|
|
59
|
+
break;
|
|
60
|
+
case states.PROCESS_UNKNOWN:
|
|
61
|
+
this._processUnknown( chunk, encoding, callback );
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
// should never happen
|
|
65
|
+
callback();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_initState( state, bytesExpected ) {
|
|
71
|
+
this.state = state;
|
|
72
|
+
this.bytesExpected = bytesExpected;
|
|
73
|
+
this.bytesRead = 0;
|
|
74
|
+
this.bytes = Buffer.alloc( 0 );
|
|
75
|
+
this.bytes.readUInt16 = this.endianness === 'LE' ? this.bytes.readUInt16LE : this.bytes.readUInt16BE;
|
|
76
|
+
this.bytes.readUInt32 = this.endianness === 'LE' ? this.bytes.readUInt32LE : this.bytes.readUInt32BE;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_accumulate( chunk ) {
|
|
80
|
+
this.bytes = Buffer.concat( [ this.bytes, chunk ] );
|
|
81
|
+
this.bytes.readUInt16 = this.endianness === 'LE' ? this.bytes.readUInt16LE : this.bytes.readUInt16BE;
|
|
82
|
+
this.bytes.readUInt32 = this.endianness === 'LE' ? this.bytes.readUInt32LE : this.bytes.readUInt32BE;
|
|
83
|
+
return ( this.bytes.length < this.bytesExpected );
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_increaseChunkSize( bytesCount ) {
|
|
87
|
+
this.bytesExpected += bytesCount;
|
|
88
|
+
return ( this.bytes.length < this.bytesExpected );
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_finishChunk( encoding, callback ) {
|
|
92
|
+
const chunkData = this.bytes.subarray( this.bytesExpected );
|
|
93
|
+
this._initState( states.PROCESS_DETECT_CHUNK, 8 );
|
|
94
|
+
this._transform( chunkData, encoding, callback );
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_detectChunk( chunk, encoding, callback ) {
|
|
98
|
+
if ( this._accumulate( chunk ) ) return callback();
|
|
99
|
+
|
|
100
|
+
this.currentChunkId = this.bytes.subarray( 0, 4 ).toString( 'ascii' );
|
|
101
|
+
const chunkSize = this.bytes.readUInt32( 4 );
|
|
102
|
+
const chunkData = this.bytes.subarray( 8 );
|
|
103
|
+
this.chunkSizes[ this.currentChunkId ] = chunkSize;
|
|
104
|
+
|
|
105
|
+
switch ( this.currentChunkId ) {
|
|
106
|
+
case 'fmt ':
|
|
107
|
+
this.chunkId = this.currentChunkId;
|
|
108
|
+
this.subchunk1Size = chunkSize;
|
|
109
|
+
this._initState( states.PROCESS_FORM, chunkSize );
|
|
110
|
+
this._processForm( chunkData, encoding, callback );
|
|
111
|
+
break;
|
|
112
|
+
case 'fact':
|
|
113
|
+
this._initState( states.PROCESS_FACT, chunkSize );
|
|
114
|
+
this._processFact( chunkData, encoding, callback );
|
|
115
|
+
break;
|
|
116
|
+
case 'data':
|
|
117
|
+
// Some encoders write `0` for the byte length here in the case of a WAV file
|
|
118
|
+
// being generated on-the-fly. In that case, we're just gonna passthrough the
|
|
119
|
+
// remaining bytes assuming they're going to be audio data.
|
|
120
|
+
this.subchunk2Size = chunkSize;
|
|
121
|
+
this._initState( states.PROCESS_DATA, chunkSize === 0 ? Infinity : chunkSize );
|
|
122
|
+
this._processData( chunkData, encoding, callback );
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
this._initState( states.PROCESS_UNKNOWN, chunkSize );
|
|
126
|
+
this._processUnknown( chunkData, encoding, callback );
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_processHeader( chunk, encoding, callback ) {
|
|
132
|
+
if ( this._accumulate( chunk ) ) return callback();
|
|
133
|
+
|
|
134
|
+
this.riffId = this.bytes.subarray( 0, 4 ).toString( 'ascii' );
|
|
135
|
+
if ( this.riffId === 'RIFF' ) {
|
|
136
|
+
this.endianness = 'LE';
|
|
137
|
+
this.bytes.readUInt16 = this.bytes.readUInt16LE;
|
|
138
|
+
this.bytes.readUInt32 = this.bytes.readUInt32LE;
|
|
139
|
+
} else if ( this.riffId === 'RIFX' ) {
|
|
140
|
+
this.endianness = 'BE';
|
|
141
|
+
this.bytes.readUInt16 = this.bytes.readUInt16BE;
|
|
142
|
+
this.bytes.readUInt32 = this.bytes.readUInt32BE;
|
|
143
|
+
} else {
|
|
144
|
+
return callback( new Error( `bad "chunk id": expected "RIFF" or "RIFX", got ${this.riffId}` ) );
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.chunkSize = this.bytes.readUInt32( 4 );
|
|
148
|
+
this.waveId = this.bytes.subarray( 8, 12 ).toString( 'ascii' );
|
|
149
|
+
if ( this.waveId !== 'WAVE' ) {
|
|
150
|
+
return callback( new Error( `bad "format": expected "WAVE", got ${this.waveId}` ) );
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this._finishChunk( encoding, callback );
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_processForm( chunk, encoding, callback ) {
|
|
157
|
+
if ( this._accumulate( chunk ) ) return callback();
|
|
158
|
+
|
|
159
|
+
this.audioFormat = this.bytes.readUInt16( 0 );
|
|
160
|
+
this.channels = this.bytes.readUInt16( 2 );
|
|
161
|
+
this.sampleRate = this.bytes.readUInt32( 4 );
|
|
162
|
+
this.byteRate = this.bytes.readUInt32( 8 ); // useless...
|
|
163
|
+
this.blockAlign = this.bytes.readUInt16( 12 ); // useless...
|
|
164
|
+
this.bitDepth = this.bytes.readUInt16( 14 );
|
|
165
|
+
this.signed = this.bitDepth !== 8;
|
|
166
|
+
|
|
167
|
+
const format = {
|
|
168
|
+
audioFormat: this.audioFormat,
|
|
169
|
+
endianness: this.endianness,
|
|
170
|
+
channels: this.channels,
|
|
171
|
+
sampleRate: this.sampleRate,
|
|
172
|
+
byteRate: this.byteRate,
|
|
173
|
+
blockAlign: this.blockAlign,
|
|
174
|
+
bitDepth: this.bitDepth,
|
|
175
|
+
signed: this.signed
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
switch ( format.audioFormat ) {
|
|
179
|
+
case formats.WAVE_FORMAT_PCM:
|
|
180
|
+
// default, common case. don't need to do anything.
|
|
181
|
+
break;
|
|
182
|
+
case formats.WAVE_FORMAT_IEEE_FLOAT:
|
|
183
|
+
format.float = true;
|
|
184
|
+
break;
|
|
185
|
+
case formats.WAVE_FORMAT_ALAW:
|
|
186
|
+
format.alaw = true;
|
|
187
|
+
break;
|
|
188
|
+
case formats.WAVE_FORMAT_MULAW:
|
|
189
|
+
format.ulaw = true;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.emit( 'format', format );
|
|
194
|
+
|
|
195
|
+
this._finishChunk( encoding, callback );
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_processData( chunk, encoding, callback ) {
|
|
199
|
+
if ( !( 'fmt ' in this.chunkSizes ) ) return callback( new Error( `bad "fmt id": expected "fmt ", got ${this.currentChunkId}` ) );
|
|
200
|
+
|
|
201
|
+
if ( this.bytesExpected === Infinity ) {
|
|
202
|
+
// pass through and go on forever
|
|
203
|
+
this.bytesRead += chunk.length;
|
|
204
|
+
this.push( chunk );
|
|
205
|
+
return callback();
|
|
206
|
+
}
|
|
207
|
+
else if ( this.bytesRead + chunk.length < this.bytesExpected ) {
|
|
208
|
+
// pass through
|
|
209
|
+
this.bytesRead += chunk.length;
|
|
210
|
+
this.push( chunk );
|
|
211
|
+
return callback();
|
|
212
|
+
}
|
|
213
|
+
else if ( this.bytesRead + chunk.length === this.bytesExpected ) {
|
|
214
|
+
// pass through and switch to chunk detection
|
|
215
|
+
this.bytesRead += chunk.length;
|
|
216
|
+
this.push( chunk );
|
|
217
|
+
this._initState( states.PROCESS_DETECT_CHUNK, 8 );
|
|
218
|
+
return callback();
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// pass through remaining bytes, switch to chunk detection of process first bytes of new chunk
|
|
222
|
+
const bytesRemaining = this.bytesExpected - this.bytesRead;
|
|
223
|
+
this.push( chunk.subarray( 0, bytesRemaining ) );
|
|
224
|
+
this._initState( states.PROCESS_DETECT_CHUNK, 8 );
|
|
225
|
+
this._transform( chunk.subarray( bytesRemaining ), encoding, callback );
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_processFact( chunk, encoding, callback ) {
|
|
230
|
+
if ( this._accumulate( chunk ) ) return callback();
|
|
231
|
+
|
|
232
|
+
// There is currently only one field defined for the format dependant data.
|
|
233
|
+
// It is a single 4-byte value that specifies the number of samples in the
|
|
234
|
+
// waveform data chunk.
|
|
235
|
+
//
|
|
236
|
+
// The number of samples field is redundant for sampled data, since the Data
|
|
237
|
+
// chunk indicates the length of the data. The number of samples can be
|
|
238
|
+
// determined from the length of the data and the container size as determined
|
|
239
|
+
// from the Format chunk.
|
|
240
|
+
this.numSamples = this.bytes.readUInt32( 0 );
|
|
241
|
+
|
|
242
|
+
this._finishChunk( encoding, callback );
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_processUnknown( chunk, encoding, callback ) {
|
|
246
|
+
if ( this._accumulate( chunk ) ) return callback();
|
|
247
|
+
|
|
248
|
+
this.emit( 'chunk', {
|
|
249
|
+
id: this.currentChunkId,
|
|
250
|
+
size: this.bytesExpected,
|
|
251
|
+
data: this.bytes.subarray( 0, this.bytesExpected )
|
|
252
|
+
} );
|
|
253
|
+
|
|
254
|
+
this._finishChunk( encoding, callback );
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
exports.WavReader = WavReader;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { Transform, TransformOptions } from "node:stream";
|
|
4
|
+
|
|
5
|
+
/** Options of the {@link WavWriter} */
|
|
6
|
+
export interface WavWriterOptions extends TransformOptions {
|
|
7
|
+
/** Format of the audio data (default: 0x0001) */
|
|
8
|
+
format?: number;
|
|
9
|
+
/** Number of channels (default: 2) */
|
|
10
|
+
channels?: number;
|
|
11
|
+
/** Sample rate (default: 44100) */
|
|
12
|
+
sampleRate?: number;
|
|
13
|
+
/** Bits per sample (default: 16) */
|
|
14
|
+
bitDepth?: number;
|
|
15
|
+
/** If `true`, a big endian wave file will be generated (default: `false`) */
|
|
16
|
+
bigEndian?: boolean;
|
|
17
|
+
/** The expected size of the "data" portion of the WAVE file (default: <maximum valid length for a WAVE file>) */
|
|
18
|
+
dataLength?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The `WavWriter` class outputs a valid WAVE file from the audio data written to
|
|
23
|
+
* it. You may set any of the "channels", "sampleRate" or "bitDepth"
|
|
24
|
+
* properties before writing the first chunk. You may also set the "dataLength" to
|
|
25
|
+
* the number of bytes expected in the "data" portion of the WAVE file. If
|
|
26
|
+
* "dataLength" is not set, then the maximum valid length for a WAVE file is
|
|
27
|
+
* written.
|
|
28
|
+
*/
|
|
29
|
+
export class WavWriter extends Transform {
|
|
30
|
+
/**
|
|
31
|
+
* Constructs a {@link WavWriter} object.
|
|
32
|
+
*
|
|
33
|
+
* @param options The options of the {@link WavWriter}
|
|
34
|
+
*/
|
|
35
|
+
constructor( options: WavWriterOptions );
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The `Writer` class outputs a valid WAVE file from the audio data written to
|
|
41
|
+
* it. You may set any of the "channels", "sampleRate" or "bitDepth"
|
|
42
|
+
* properties before writing the first chunk. You may also set the "dataLength" to
|
|
43
|
+
* the number of bytes expected in the "data" portion of the WAVE file. If
|
|
44
|
+
* "dataLength" is not set, then the maximum valid length for a WAVE file is
|
|
45
|
+
* written.
|
|
46
|
+
*
|
|
47
|
+
* This old name is only provided for compatibility reasons with the former
|
|
48
|
+
* `node-wav` module. Use {@link WavWriter} instead.
|
|
49
|
+
*
|
|
50
|
+
* @deprecated
|
|
51
|
+
*/
|
|
52
|
+
export class Writer extends WavWriter { }
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// activate strict mode
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// load modules
|
|
5
|
+
const process = require( 'node:process' );
|
|
6
|
+
const { Transform } = require( 'node:stream' );
|
|
7
|
+
|
|
8
|
+
// RIFF Chunk IDs in Buffers.
|
|
9
|
+
const RIFF = Buffer.from( 'RIFF' );
|
|
10
|
+
const WAVE = Buffer.from( 'WAVE' );
|
|
11
|
+
const fmt = Buffer.from( 'fmt ' );
|
|
12
|
+
const data = Buffer.from( 'data' );
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The max size of the "data" chunk of a WAVE file. This is the max unsigned
|
|
16
|
+
* 32-bit int value, minus 100 bytes (overkill, 44 would be safe) for the header.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_WAV = 4294967295 - 100;
|
|
19
|
+
|
|
20
|
+
class WavWriter extends Transform {
|
|
21
|
+
constructor( options ) {
|
|
22
|
+
super( options );
|
|
23
|
+
|
|
24
|
+
this.options = options instanceof Object ? options : {};
|
|
25
|
+
this.endianness = this.options.bigEndian === true ? 'BE' : 'LE';
|
|
26
|
+
this.format = 1; // raw PCM
|
|
27
|
+
this.channels = 2;
|
|
28
|
+
this.sampleRate = 44100;
|
|
29
|
+
this.bitDepth = 16;
|
|
30
|
+
this.bytesProcessed = 0;
|
|
31
|
+
|
|
32
|
+
if ( options instanceof Object ) {
|
|
33
|
+
if ( !isNaN( options.format ) ) this.format = parseInt( options.format );
|
|
34
|
+
if ( !isNaN( options.channels ) ) this.channels = parseInt( options.channels );
|
|
35
|
+
if ( !isNaN( options.sampleRate ) ) this.sampleRate = parseInt( options.sampleRate );
|
|
36
|
+
if ( !isNaN( options.bitDepth ) ) this.bitDepth = parseInt( options.bitDepth );
|
|
37
|
+
if ( !isNaN( options.dataLength ) ) this.dataLength = parseInt( options.dataLength );
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this._writeHeader();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_transform( chunk, encoding, callback ) {
|
|
44
|
+
this.push( chunk );
|
|
45
|
+
this.bytesProcessed += chunk.length;
|
|
46
|
+
callback();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_flush( callback ) {
|
|
50
|
+
this.dataLength = this.bytesProcessed;
|
|
51
|
+
|
|
52
|
+
// write the file length at the beginning of the header
|
|
53
|
+
this.header.writeUInt32( this.dataLength + this.headerLength - 8, 4 );
|
|
54
|
+
|
|
55
|
+
// write the data length at the end of the header
|
|
56
|
+
this.header.writeUInt32( this.dataLength, this.headerLength - 4 );
|
|
57
|
+
|
|
58
|
+
callback();
|
|
59
|
+
|
|
60
|
+
process.nextTick( () => {
|
|
61
|
+
this.emit( 'header', this.header );
|
|
62
|
+
} );
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_writeHeader() {
|
|
66
|
+
// TODO: 44 is only for format 1 (PCM), any other
|
|
67
|
+
// format will have a variable size...
|
|
68
|
+
const headerLength = 44;
|
|
69
|
+
|
|
70
|
+
let dataLength = this.dataLength;
|
|
71
|
+
if ( isNaN( dataLength ) ) {
|
|
72
|
+
dataLength = MAX_WAV;
|
|
73
|
+
}
|
|
74
|
+
const fileSize = dataLength + headerLength;
|
|
75
|
+
const header = Buffer.allocUnsafe( headerLength );
|
|
76
|
+
// fastify
|
|
77
|
+
header.writeUInt16 = this.endianness === 'LE' ? header.writeUInt16LE : header.writeUInt16BE;
|
|
78
|
+
header.writeUInt32 = this.endianness === 'LE' ? header.writeUInt32LE : header.writeUInt32BE;
|
|
79
|
+
|
|
80
|
+
let offset = 0;
|
|
81
|
+
|
|
82
|
+
// write the "RIFF" identifier
|
|
83
|
+
RIFF.copy( header, offset );
|
|
84
|
+
offset += RIFF.length;
|
|
85
|
+
|
|
86
|
+
// write the file size minus the identifier and this 32-bit int
|
|
87
|
+
header.writeUInt32( fileSize - 8, offset );
|
|
88
|
+
offset += 4;
|
|
89
|
+
|
|
90
|
+
// write the "WAVE" identifier
|
|
91
|
+
WAVE.copy( header, offset );
|
|
92
|
+
offset += WAVE.length;
|
|
93
|
+
|
|
94
|
+
// write the "fmt " sub-chunk identifier
|
|
95
|
+
fmt.copy( header, offset );
|
|
96
|
+
offset += fmt.length;
|
|
97
|
+
|
|
98
|
+
// write the size of the "fmt " chunk
|
|
99
|
+
// XXX: value of 16 is hard-coded for raw PCM format. other formats have
|
|
100
|
+
// different size.
|
|
101
|
+
header.writeUInt32( 16, offset );
|
|
102
|
+
offset += 4;
|
|
103
|
+
|
|
104
|
+
// write the audio format code
|
|
105
|
+
header.writeUInt16( this.format, offset );
|
|
106
|
+
offset += 2;
|
|
107
|
+
|
|
108
|
+
// write the number of channels
|
|
109
|
+
header.writeUInt16( this.channels, offset );
|
|
110
|
+
offset += 2;
|
|
111
|
+
|
|
112
|
+
// write the sample rate
|
|
113
|
+
header.writeUInt32( this.sampleRate, offset );
|
|
114
|
+
offset += 4;
|
|
115
|
+
|
|
116
|
+
// write the byte rate
|
|
117
|
+
let byteRate = this.byteRate;
|
|
118
|
+
if ( isNaN( byteRate ) ) {
|
|
119
|
+
byteRate = this.sampleRate * this.channels * this.bitDepth / 8;
|
|
120
|
+
}
|
|
121
|
+
header.writeUInt32( byteRate, offset );
|
|
122
|
+
offset += 4;
|
|
123
|
+
|
|
124
|
+
// write the block align
|
|
125
|
+
let blockAlign = this.blockAlign;
|
|
126
|
+
if ( blockAlign == null ) {
|
|
127
|
+
blockAlign = this.channels * this.bitDepth / 8;
|
|
128
|
+
}
|
|
129
|
+
header.writeUInt16( blockAlign, offset );
|
|
130
|
+
offset += 2;
|
|
131
|
+
|
|
132
|
+
// write the bits per sample
|
|
133
|
+
header.writeUInt16( this.bitDepth, offset );
|
|
134
|
+
offset += 2;
|
|
135
|
+
|
|
136
|
+
// write the "data" sub-chunk ID
|
|
137
|
+
data.copy( header, offset );
|
|
138
|
+
offset += data.length;
|
|
139
|
+
|
|
140
|
+
// write the remaining length of the rest of the data
|
|
141
|
+
header.writeUInt32( dataLength, offset );
|
|
142
|
+
// offset += 4;
|
|
143
|
+
|
|
144
|
+
// save the "header" Buffer for the end, we emit the "header" event at the end
|
|
145
|
+
// with the "size" values properly filled out. if this stream is being piped to
|
|
146
|
+
// a file (or anything else seekable), then this correct header should be placed
|
|
147
|
+
// at the very beginning of the file.
|
|
148
|
+
this.header = header;
|
|
149
|
+
this.headerLength = headerLength;
|
|
150
|
+
|
|
151
|
+
this.push( header );
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
exports.WavWriter = WavWriter;
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yeasoft/wav",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Stream classes for Microsoft WAVE audio files",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Leo Moll",
|
|
7
|
+
"email": "leo.moll@yeasoft.com"
|
|
8
|
+
},
|
|
9
|
+
"contributors": [ {
|
|
10
|
+
"name": "Nathan Friedly",
|
|
11
|
+
"email": "nathan@nfriedly.com"
|
|
12
|
+
}, {
|
|
13
|
+
"name": "Linus Unnebäck",
|
|
14
|
+
"email": "linus@folkdatorn.se"
|
|
15
|
+
}, {
|
|
16
|
+
"name": "Matt McKegg",
|
|
17
|
+
"email": "matt@wetsand.co.nz"
|
|
18
|
+
}, {
|
|
19
|
+
"name": "Christopher Bebry",
|
|
20
|
+
"email": "invalidsyntax@gmail.com"
|
|
21
|
+
}, {
|
|
22
|
+
"name": "Nathan Rajlich",
|
|
23
|
+
"email": "nathan@tootallnate.net"
|
|
24
|
+
} ],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"bugs": "https://github.com/YeaSoft/node-wav/issues",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"clean": "rm -rf node_modules",
|
|
31
|
+
"test": "eslint && mocha"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/YeaSoft/node-wav.git"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=16"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^10.0.1",
|
|
45
|
+
"@types/node": "^22.9.3",
|
|
46
|
+
"eslint": "^10.0.3",
|
|
47
|
+
"globals": "^17.3.0",
|
|
48
|
+
"mic": "^2.1.2",
|
|
49
|
+
"mocha": "^11.7.5",
|
|
50
|
+
"speaker": "^0.5.5"
|
|
51
|
+
}
|
|
52
|
+
}
|