@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 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
@@ -0,0 +1,5 @@
1
+ /// <reference types="node" />
2
+
3
+ export * from "./wav-reader";
4
+ export * from "./wav-writer";
5
+ export * from "./wav-file-writer";
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
+ }