music-metadata 12.1.1 → 12.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.
package/README.md CHANGED
@@ -51,6 +51,7 @@ If you find this project useful and would like to support its development, consi
51
51
  | [APE](https://wikipedia.org/wiki/Monkey's_Audio) | Monkey's Audio | <img src="./image/logo-APE-AI.svg" width="40" alt="Monkey's Audio logo"> |
52
52
  | [ASF](https://wikipedia.org/wiki/Advanced_Systems_Format) | Advanced Systems Format | |
53
53
  | [BWF](https://en.wikipedia.org/wiki/Broadcast_Wave_Format) | Extended WAV format for broadcast and archiving | |
54
+ | [CAF](https://en.wikipedia.org/wiki/Core_Audio_File_Format) | Core Audio File Format | <img src="./image/logo-Apple_Computer_Logo_rainbow.svg" width="40" alt="Apple rainbow logo"> |
54
55
  | [DSDIFF](https://wikipedia.org/wiki/Direct_Stream_Digital) | Philips DSDIFF | <img src="./image/logo-DSD.svg" width="80" alt="DSD logo"> |
55
56
  | [DSF](https://wikipedia.org/wiki/Direct_Stream_Digital) | Sony's DSD Stream File | <img src="./image/logo-DSD.svg" width="80" alt="DSD logo"> |
56
57
  | [FLAC](https://wikipedia.org/wiki/FLAC) | Free Lossless Audio Codec | <img src="./image/logo-FLAC.svg" width="80" alt="FLAC logo"> |
@@ -80,6 +81,7 @@ Following tag header formats are supported:
80
81
  - [RIFF](https://wikipedia.org/wiki/Resource_Interchange_File_Format)/INFO
81
82
  - [Vorbis comment](https://wikipedia.org/wiki/Vorbis_comment)
82
83
  - [AIFF](https://wikipedia.org/wiki/Audio_Interchange_File_Format)
84
+ - [CAF](https://developer.apple.com/library/archive/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html)
83
85
 
84
86
  Following lyric formats are supported:
85
87
  - [LRC](https://en.wikipedia.org/wiki/LRC_(file_format))
@@ -666,8 +668,10 @@ Audio format information. Defined in the TypeScript `IFormat` interface:
666
668
  - `format.codec?` Name of the codec (algorithm used for the audio compression)
667
669
  - `format.codecProfile?: string` Codec profile / settings
668
670
  - `format.tagTypes?: TagType[]` List of tagging formats found in parsed audio file
669
- - `format.duration?: number` Duration in seconds
670
- - `format.bitrate?: number` Number bits per second of encoded audio file
671
+ - `format.duration?: number` Duration in seconds (existing audio/container summary; complete presentation duration for video-only files)
672
+ - `format.bitrate?: number` Existing audio/container summary bitrate in bits per second
673
+ - `format.containerDuration?: number` Complete media presentation duration in seconds, including video
674
+ - `format.overallBitrate?: number` Average bitrate of the entire file in bits per second, including container overhead; requires a known file size and presentation duration
671
675
  - `format.sampleRate?: number` Sampling rate in Samples per second (S/s)
672
676
  - `format.bitsPerSample?: number` Audio bit depth
673
677
  - `format.lossless?: boolean` True if lossless, false for lossy encoding
@@ -686,9 +690,14 @@ metadata. Import `TrackType` from `music-metadata` to select tracks without know
686
690
  ```js
687
691
  import { parseFile, TrackType } from 'music-metadata';
688
692
 
689
- const { format } = await parseFile('movie.mp4');
693
+ const { format } = await parseFile('movie.mp4', { duration: true });
690
694
  const audioTracks = format.trackInfo.filter(track => track.type === TrackType.audio);
691
695
  const videoTracks = format.trackInfo.filter(track => track.type === TrackType.video);
696
+ for (const track of videoTracks) {
697
+ console.log(track.duration, track.bitrate, track.video?.pixelWidth, track.video?.pixelHeight);
698
+ console.log(track.video?.frameRate);
699
+ }
700
+ console.log(format.containerDuration, format.overallBitrate);
692
701
  ```
693
702
 
694
703
  Each entry describes one discovered track. The order follows container discovery and does not identify
@@ -698,12 +707,27 @@ logical media streams; Skeleton headers do not create media tracks. Single-strea
698
707
  their format properties to one audio track after parsing.
699
708
 
700
709
  Properties are optional when unavailable and are not copied between tracks. In particular, a container
701
- or summary bitrate is not a per-track bitrate. MPEG-4 track timing comes from media headers (or parsed
702
- audio fragments), and track bitrates from sample sizes and duration. ASF supplies audio/video stream
703
- properties and stream bitrates from both standalone and nested Stream Properties Objects. Codec List
704
- entries supply names for matching streams; unused codecs do not create tracks. Matroska applies
705
- specified defaults for flags, language, channels, and sample rate, and prefers IETF language tags when present. Video duration/bitrate coverage and overall
706
- video statistics remain incomplete. The existing `format` fields retain their summary semantics.
710
+ or summary bitrate is not a per-track bitrate. The existing `format.duration` and `format.bitrate`
711
+ fields retain their summary semantics. Use `containerDuration` for the complete presentation and
712
+ `overallBitrate` for the actual file size multiplied by eight and divided by that duration. These
713
+ statistics remain unset when timing is unavailable; `overallBitrate` also requires a known input size.
714
+
715
+ MPEG-4 derives audio and video timing from media headers, sample tables, and fragments, including
716
+ fragment defaults and decode timestamps. Track bitrates use sample sizes and duration; video properties
717
+ include encoded dimensions, display dimensions, and average frame rate. ASF supplies stream dimensions,
718
+ stream bitrates, and extended stream timing and frame rate when present. Standalone and nested Stream
719
+ Properties Objects describe actual streams; Codec List entries supply names without creating unused tracks.
720
+
721
+ Matroska applies specified defaults for flags, language, channels, and sample rate, and prefers IETF
722
+ language tags when present. Track statistics tags provide duration, bitrate, and frame count; without
723
+ these tags, per-track timing may be unavailable even when segment duration is known. Media clusters
724
+ are not scanned to calculate missing track statistics.
725
+
726
+ Ogg/Theora duration comes from granule positions. Set `duration: true` to allow scanning beyond the
727
+ initial metadata pages. Once a video stream has been fully read, its average bitrate uses that logical
728
+ stream's payload bytes, including codec headers and excluding Ogg framing and other streams. Resolution
729
+ and frame rate are available from the identification header. Audio streams retain their own codec
730
+ bitrate semantics.
707
731
 
708
732
  For Ogg/Opus, the per-track average bitrate uses the logical stream's payload bytes, including codec
709
733
  headers and excluding Ogg framing and other streams. It requires the logical stream's end-of-stream
@@ -5,9 +5,10 @@ import { parse as mimeTypeParse } from 'media-typer';
5
5
  import { aiffParserLoader } from './aiff/AiffLoader.js';
6
6
  import { apeParserLoader } from './apev2/Apev2Loader.js';
7
7
  import { asfParserLoader } from './asf/AsfLoader.js';
8
+ import { cafParserLoader } from './caf/CafLoader.js';
8
9
  import { getMaxMemoryAllocation } from './common/Allocation.js';
9
10
  import { MetadataCollector } from './common/MetadataCollector.js';
10
- import { createAudioTrackInfo } from './common/TrackInfo.js';
11
+ import { createAudioTrackInfo, finalizeContainerInfo } from './common/TrackInfo.js';
11
12
  import { scanAppendingHeaders } from './core.js';
12
13
  import { dsdiffParserLoader } from './dsdiff/DsdiffLoader.js';
13
14
  import { dsfParserLoader } from './dsf/DsfLoader.js';
@@ -48,7 +49,8 @@ export class ParserFactory {
48
49
  wavpackParserLoader,
49
50
  musepackParserLoader,
50
51
  dsfParserLoader,
51
- dsdiffParserLoader
52
+ dsdiffParserLoader,
53
+ cafParserLoader
52
54
  ].forEach(parser => {
53
55
  this.registerParser(parser);
54
56
  });
@@ -108,6 +110,7 @@ export class ParserFactory {
108
110
  metadata.setFormat('hasVideo', !!metadata.format.trackInfo.find(track => track.type === TrackType.video));
109
111
  }
110
112
  }
113
+ finalizeContainerInfo(metadata, tokenizer.fileInfo.size);
111
114
  return metadata.toCommonMetadata();
112
115
  }
113
116
  /**
@@ -12,6 +12,7 @@ import { BasicParser } from '../common/BasicParser.js';
12
12
  export declare class AsfParser extends BasicParser {
13
13
  private streams;
14
14
  private codecs;
15
+ private extendedStreams;
15
16
  private streamBitrates;
16
17
  parse(): Promise<void>;
17
18
  private parseObjectHeaders;
@@ -22,6 +22,7 @@ const headerType = 'asf';
22
22
  export class AsfParser extends BasicParser {
23
23
  streams = new Map();
24
24
  codecs = [];
25
+ extendedStreams = new Map();
25
26
  streamBitrates = new Map();
26
27
  async parse() {
27
28
  const header = await this.readToken(AsfObject.TopLevelHeaderObjectToken);
@@ -52,6 +53,7 @@ export class AsfParser extends BasicParser {
52
53
  stream.codecId === new Token.StringType(4, 'ascii').get(codec.information, 0));
53
54
  });
54
55
  const track = { id: stream.streamNumber, type };
56
+ const extended = this.extendedStreams.get(stream.streamNumber);
55
57
  if (stream.codecId !== undefined) {
56
58
  track.codecId = stream.codecId;
57
59
  }
@@ -64,7 +66,20 @@ export class AsfParser extends BasicParser {
64
66
  if (stream.video) {
65
67
  track.video = stream.video;
66
68
  }
67
- const bitrate = this.streamBitrates.get(stream.streamNumber) ?? stream.bitrate;
69
+ if (extended) {
70
+ if (extended.endTime > extended.startTime) {
71
+ track.duration = Number(extended.endTime - extended.startTime) / 1000;
72
+ }
73
+ const name = extended.streamNames[0]?.streamName;
74
+ if (name) {
75
+ track.name = name;
76
+ }
77
+ if (track.video && extended.averageTimePerFrame > 0) {
78
+ track.video.frameRate = 1e7 / extended.averageTimePerFrame;
79
+ }
80
+ }
81
+ const bitrate = this.streamBitrates.get(stream.streamNumber) ??
82
+ (extended && extended.dataBitrate > 0 ? extended.dataBitrate : stream.bitrate);
68
83
  if (bitrate !== undefined) {
69
84
  track.bitrate = bitrate;
70
85
  }
@@ -90,7 +105,13 @@ export class AsfParser extends BasicParser {
90
105
  // 3.2
91
106
  this.validateAllocationSize(payloadSize, 'File Properties Object');
92
107
  const fpo = await this.readToken(new AsfObject.FilePropertiesObject(header));
93
- this.metadata.setFormat('duration', Number(fpo.playDuration / BigInt(1000)) / 10000 - Number(fpo.preroll) / 1000);
108
+ if (!fpo.flags.broadcast) {
109
+ const duration = Number(fpo.playDuration) / 1e7 - Number(fpo.preroll) / 1000;
110
+ if (Number.isFinite(duration) && duration > 0) {
111
+ this.metadata.setFormat('duration', duration);
112
+ this.metadata.setFormat('containerDuration', duration);
113
+ }
114
+ }
94
115
  this.metadata.setFormat('bitrate', fpo.maximumBitrate);
95
116
  break;
96
117
  }
@@ -235,6 +256,7 @@ export class AsfParser extends BasicParser {
235
256
  // 4.1
236
257
  this.validateAllocationSize(payloadSize, 'Extended Stream Properties Object');
237
258
  const extended = await this.readToken(new AsfObject.ExtendedStreamPropertiesObjectState(header));
259
+ this.extendedStreams.set(extended.streamNumber, extended);
238
260
  if (extended.streamPropertiesObject) {
239
261
  const stream = extended.streamPropertiesObject;
240
262
  this.streams.set(stream.streamNumber, stream);
@@ -0,0 +1,2 @@
1
+ import type { IParserLoader } from '../ParserFactory.js';
2
+ export declare const cafParserLoader: IParserLoader;
@@ -0,0 +1,8 @@
1
+ export const cafParserLoader = {
2
+ parserType: 'caf',
3
+ extensions: ['.caf', '.caff'],
4
+ mimeTypes: ['audio/caf', 'application/caf'],
5
+ async load() {
6
+ return (await import('./CafParser.js')).CafParser;
7
+ }
8
+ };
@@ -0,0 +1,37 @@
1
+ import { BasicParser } from '../common/BasicParser.js';
2
+ /**
3
+ * CAF - Core Audio File Format
4
+ *
5
+ * Ref: https://developer.apple.com/library/archive/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html
6
+ */
7
+ export declare class CafParser extends BasicParser {
8
+ private desc;
9
+ private packetTable;
10
+ private dataBytes;
11
+ private expectedFirstChunk;
12
+ private reachedEndOfFile;
13
+ parse(): Promise<void>;
14
+ private endOfFileReached;
15
+ private readChunk;
16
+ private readAudioDescription;
17
+ private applyAudioDescription;
18
+ private readPacketTable;
19
+ private readAudioData;
20
+ private readInfo;
21
+ private readChannelLayout;
22
+ private addChannelLayout;
23
+ /**
24
+ * Rejects a whole chunk whose declared size exceeds the remaining input.
25
+ * Only applicable when the total input size is known.
26
+ */
27
+ private validateChunkAvailability;
28
+ /**
29
+ * Reads a chunk body without trusting its declared size.
30
+ * @param chunkSize Declared body size
31
+ * @param chunkType Chunk name for error messages
32
+ */
33
+ private readChunkBody;
34
+ private skipUnknownChunk;
35
+ private applyDerivedFormat;
36
+ private deriveFrameCount;
37
+ }
@@ -0,0 +1,253 @@
1
+ import initDebug from 'debug';
2
+ import * as strtok3 from 'strtok3';
3
+ import * as Token from 'token-types';
4
+ import { BasicParser } from '../common/BasicParser.js';
5
+ import * as CafToken from './CafToken.js';
6
+ import { CafContentError } from './CafToken.js';
7
+ const debug = initDebug('music-metadata:parser:caf');
8
+ // Unrecognised mFormatID values are reported as-is, without claiming losslessness.
9
+ const codecByFormatId = {
10
+ lpcm: { codec: 'PCM', lossless: true },
11
+ ima4: { codec: 'IMA4 ADPCM', lossless: false },
12
+ alac: { codec: 'ALAC', lossless: true },
13
+ flac: { codec: 'FLAC', lossless: true },
14
+ ulaw: { codec: 'ITU-T G.711 mu-law', lossless: false },
15
+ alaw: { codec: 'ITU-T G.711 A-law', lossless: false },
16
+ '.mp1': { codec: 'MPEG-1 Layer I', lossless: false },
17
+ '.mp2': { codec: 'MPEG-1 Layer II', lossless: false },
18
+ '.mp3': { codec: 'MPEG-1 Layer III', lossless: false },
19
+ 'aac ': { codec: 'AAC', lossless: false },
20
+ aace: { codec: 'AAC', lossless: false },
21
+ aacf: { codec: 'AAC', lossless: false },
22
+ aacg: { codec: 'AAC', lossless: false },
23
+ aach: { codec: 'AAC', lossless: false },
24
+ aacl: { codec: 'AAC', lossless: false },
25
+ aacp: { codec: 'AAC', lossless: false }
26
+ };
27
+ const editCountLen = 4;
28
+ const numEntriesLen = 4;
29
+ /**
30
+ * CAF - Core Audio File Format
31
+ *
32
+ * Ref: https://developer.apple.com/library/archive/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html
33
+ */
34
+ export class CafParser extends BasicParser {
35
+ desc;
36
+ packetTable;
37
+ dataBytes;
38
+ expectedFirstChunk = true;
39
+ reachedEndOfFile = false;
40
+ async parse() {
41
+ const header = await this.tokenizer.readToken(CafToken.FileHeader);
42
+ if (header.fileType !== 'caff') {
43
+ throw new CafContentError(`Invalid file-type, expected 'caff', found '${header.fileType}'`);
44
+ }
45
+ this.metadata.setFormat('container', 'CAF');
46
+ this.metadata.setAudioOnly();
47
+ while (!this.reachedEndOfFile && !this.endOfFileReached()) {
48
+ let chunkHeader;
49
+ try {
50
+ chunkHeader = await this.tokenizer.readToken(CafToken.ChunkHeader);
51
+ }
52
+ catch (error) {
53
+ if (error instanceof strtok3.EndOfStreamError) {
54
+ debug('End-of-stream');
55
+ break;
56
+ }
57
+ throw error;
58
+ }
59
+ debug(`Reading CAF chunk type=${chunkHeader.chunkType} size=${chunkHeader.chunkSize}`);
60
+ if (this.expectedFirstChunk) {
61
+ this.expectedFirstChunk = false;
62
+ if (chunkHeader.chunkType !== 'desc') {
63
+ throw new CafContentError(`Expected an Audio Description chunk, found '${chunkHeader.chunkType}'`);
64
+ }
65
+ }
66
+ await this.readChunk(chunkHeader);
67
+ }
68
+ if (!this.desc) {
69
+ throw new CafContentError('Missing Audio Description chunk');
70
+ }
71
+ this.applyDerivedFormat();
72
+ }
73
+ endOfFileReached() {
74
+ const { size } = this.tokenizer.fileInfo;
75
+ return size !== undefined && size - this.tokenizer.position < CafToken.ChunkHeader.len;
76
+ }
77
+ async readChunk(header) {
78
+ switch (header.chunkType) {
79
+ case 'desc':
80
+ return this.readAudioDescription(header.chunkSize);
81
+ case 'pakt':
82
+ return this.readPacketTable(header.chunkSize);
83
+ case 'data':
84
+ return this.readAudioData(header.chunkSize);
85
+ case 'info':
86
+ return this.readInfo(header.chunkSize);
87
+ case 'chan':
88
+ return this.readChannelLayout(header.chunkSize);
89
+ default:
90
+ debug(`Ignore chunk type=${header.chunkType} size=${header.chunkSize}`);
91
+ return this.skipUnknownChunk(header.chunkSize);
92
+ }
93
+ }
94
+ async readAudioDescription(chunkSize) {
95
+ if (chunkSize < CafToken.AudioDescription.len) {
96
+ throw new CafContentError(`Audio Description chunk size ${chunkSize} is smaller than ${CafToken.AudioDescription.len}`);
97
+ }
98
+ this.desc = await this.tokenizer.readToken(CafToken.AudioDescription);
99
+ await this.tokenizer.ignore(chunkSize - CafToken.AudioDescription.len);
100
+ this.applyAudioDescription(this.desc);
101
+ }
102
+ applyAudioDescription(desc) {
103
+ this.metadata.setFormat('sampleRate', desc.sampleRate);
104
+ this.metadata.setFormat('numberOfChannels', desc.channelsPerFrame);
105
+ if (desc.bitsPerChannel > 0) {
106
+ this.metadata.setFormat('bitsPerSample', desc.bitsPerChannel);
107
+ }
108
+ const codec = codecByFormatId[desc.formatId];
109
+ this.metadata.setFormat('codec', codec ? codec.codec : desc.formatId);
110
+ if (codec) {
111
+ this.metadata.setFormat('lossless', codec.lossless);
112
+ }
113
+ }
114
+ async readPacketTable(chunkSize) {
115
+ const desc = this.desc;
116
+ if (!desc) {
117
+ throw new CafContentError('Packet Table chunk found before the Audio Description chunk');
118
+ }
119
+ if (chunkSize < CafToken.PacketTableHeader.len) {
120
+ throw new CafContentError(`Packet Table chunk size ${chunkSize} is smaller than ${CafToken.PacketTableHeader.len}`);
121
+ }
122
+ this.validateChunkAvailability(chunkSize, 'Packet Table');
123
+ const packetTable = await this.tokenizer.readToken(CafToken.PacketTableHeader);
124
+ const body = await this.readChunkBody(chunkSize - CafToken.PacketTableHeader.len, 'Packet Table');
125
+ CafToken.decodePacketTableEntries(body, desc, packetTable.numberPackets);
126
+ this.packetTable = packetTable;
127
+ }
128
+ async readAudioData(chunkSize) {
129
+ if (chunkSize >= 0 && chunkSize < editCountLen) {
130
+ throw new CafContentError(`Audio Data chunk size ${chunkSize} is too small to hold mEditCount`);
131
+ }
132
+ if (chunkSize >= 0) {
133
+ this.validateChunkAvailability(chunkSize, 'Audio Data');
134
+ }
135
+ if ((await this.tokenizer.ignore(editCountLen)) !== editCountLen) {
136
+ throw new strtok3.EndOfStreamError();
137
+ }
138
+ if (chunkSize === -1) {
139
+ this.reachedEndOfFile = true; // Runs to the end of the file, nothing can follow
140
+ const { size } = this.tokenizer.fileInfo;
141
+ if (size === undefined) {
142
+ debug('Audio Data chunk has an unknown size, cannot derive the bitrate');
143
+ return;
144
+ }
145
+ this.dataBytes = size - this.tokenizer.position;
146
+ await this.tokenizer.ignore(this.dataBytes);
147
+ return;
148
+ }
149
+ const dataBytes = chunkSize - editCountLen;
150
+ if ((await this.tokenizer.ignore(dataBytes)) !== dataBytes) {
151
+ throw new strtok3.EndOfStreamError();
152
+ }
153
+ this.dataBytes = dataBytes;
154
+ }
155
+ async readInfo(chunkSize) {
156
+ if (chunkSize < numEntriesLen) {
157
+ throw new CafContentError(`Information chunk size ${chunkSize} is too small to hold mNumEntries`);
158
+ }
159
+ const body = await this.readChunkBody(chunkSize, 'Information');
160
+ for (const { key, value } of CafToken.parseInfoEntries(body)) {
161
+ debug(`info key=${key}`);
162
+ await this.metadata.addTag('CAF', key, value);
163
+ }
164
+ }
165
+ async readChannelLayout(chunkSize) {
166
+ if (chunkSize < CafToken.ChannelLayoutHeader.len) {
167
+ throw new CafContentError(`Channel Layout chunk size ${chunkSize} is smaller than ${CafToken.ChannelLayoutHeader.len}`);
168
+ }
169
+ this.validateChunkAvailability(chunkSize, 'Channel Layout');
170
+ const header = await this.tokenizer.readToken(CafToken.ChannelLayoutHeader);
171
+ const descriptionLen = header.numberChannelDescriptions * CafToken.channelDescriptionLen;
172
+ const bodyLen = CafToken.ChannelLayoutHeader.len + descriptionLen;
173
+ if (bodyLen > chunkSize) {
174
+ throw new CafContentError(`Channel Layout chunk declares ${header.numberChannelDescriptions} description(s) which exceed its size of ${chunkSize}`);
175
+ }
176
+ const descriptions = descriptionLen > 0
177
+ ? CafToken.parseChannelDescriptions(await this.readChunkBody(descriptionLen, 'Channel Layout'), header.numberChannelDescriptions)
178
+ : [];
179
+ await this.addChannelLayout(header, descriptions);
180
+ await this.tokenizer.ignore(chunkSize - bodyLen);
181
+ }
182
+ async addChannelLayout(header, descriptions) {
183
+ await this.metadata.addTag('CAF', 'channelLayoutTag', header.channelLayoutTag);
184
+ await this.metadata.addTag('CAF', 'channelLayoutBitmap', header.channelBitmap);
185
+ if (descriptions.length > 0) {
186
+ // No common tag describes channel order so keep the complete descriptions
187
+ await this.metadata.addTag('CAF', 'channelLayoutDescriptions', descriptions);
188
+ }
189
+ }
190
+ /**
191
+ * Rejects a whole chunk whose declared size exceeds the remaining input.
192
+ * Only applicable when the total input size is known.
193
+ */
194
+ validateChunkAvailability(chunkSize, chunkType) {
195
+ const { size } = this.tokenizer.fileInfo;
196
+ if (size !== undefined) {
197
+ const available = size - this.tokenizer.position;
198
+ if (chunkSize > available) {
199
+ throw new CafContentError(`${chunkType} chunk size ${chunkSize} exceeds available input size ${available}`);
200
+ }
201
+ }
202
+ }
203
+ /**
204
+ * Reads a chunk body without trusting its declared size.
205
+ * @param chunkSize Declared body size
206
+ * @param chunkType Chunk name for error messages
207
+ */
208
+ async readChunkBody(chunkSize, chunkType) {
209
+ this.validateChunkAvailability(chunkSize, chunkType);
210
+ return this.readToken(new Token.Uint8ArrayType(chunkSize));
211
+ }
212
+ async skipUnknownChunk(chunkSize) {
213
+ if (chunkSize < 0) {
214
+ throw new CafContentError(`Chunk size ${chunkSize} may only be -1 for an Audio Data chunk`);
215
+ }
216
+ await this.tokenizer.ignore(chunkSize);
217
+ }
218
+ // Deferred: the packet table and audio payload may arrive after the description
219
+ applyDerivedFormat() {
220
+ const desc = this.desc;
221
+ if (!desc || desc.sampleRate <= 0) {
222
+ return;
223
+ }
224
+ const frames = this.deriveFrameCount(desc);
225
+ if (frames === undefined) {
226
+ debug('Unable to derive the number of sample frames');
227
+ return;
228
+ }
229
+ const duration = frames / desc.sampleRate;
230
+ this.metadata.setFormat('numberOfSamples', frames);
231
+ this.metadata.setFormat('duration', duration);
232
+ if (this.dataBytes !== undefined) {
233
+ this.metadata.setFormat('bitrate', (8 * this.dataBytes) / duration);
234
+ }
235
+ }
236
+ deriveFrameCount(desc) {
237
+ const packetTable = this.packetTable;
238
+ if (packetTable) {
239
+ // Signed field, writers emit negative values. Already excludes priming and remainder frames.
240
+ if (packetTable.numberValidFrames > 0) {
241
+ return packetTable.numberValidFrames;
242
+ }
243
+ debug(`Ignoring packet table with mNumberValidFrames=${packetTable.numberValidFrames}`);
244
+ }
245
+ if (desc.bytesPerPacket > 0 && desc.framesPerPacket > 0 && this.dataBytes !== undefined) {
246
+ const frames = (this.dataBytes * desc.framesPerPacket) / desc.bytesPerPacket;
247
+ if (frames > 0) {
248
+ return frames;
249
+ }
250
+ }
251
+ return undefined;
252
+ }
253
+ }
@@ -0,0 +1,8 @@
1
+ import { CommonTagMapper } from '../common/GenericTagMapper.js';
2
+ import type { IGenericTag } from '../common/GenericTagTypes.js';
3
+ import type { IWarningCollector } from '../common/MetadataCollector.js';
4
+ import type { ITag } from '../type.js';
5
+ export declare class CafTagMapper extends CommonTagMapper {
6
+ constructor();
7
+ mapGenericTag(tag: ITag, warnings: IWarningCollector): IGenericTag | null;
8
+ }
@@ -0,0 +1,38 @@
1
+ import { CommonTagMapper } from '../common/GenericTagMapper.js';
2
+ // "time signature", "nominal bit rate" and "channel layout" have no common
3
+ // equivalent and stay native. Undocumented keys are always kept natively too.
4
+ const infoTagMap = {
5
+ tempo: 'bpm',
6
+ 'key signature': 'key',
7
+ artist: 'artist',
8
+ album: 'album',
9
+ 'track number': 'track',
10
+ year: 'year',
11
+ composer: 'composer',
12
+ lyricist: 'lyricist',
13
+ genre: 'genre',
14
+ title: 'title',
15
+ 'recorded date': 'date',
16
+ comments: 'comment',
17
+ copyright: 'copyright',
18
+ 'source encoder': 'encodersettings',
19
+ 'encoding application': 'encodedby'
20
+ };
21
+ const commaSeparatedKeys = new Set(['artist', 'composer', 'lyricist', 'genre']);
22
+ export class CafTagMapper extends CommonTagMapper {
23
+ constructor() {
24
+ super(['CAF'], infoTagMap);
25
+ }
26
+ mapGenericTag(tag, warnings) {
27
+ if (commaSeparatedKeys.has(tag.id) && typeof tag.value === 'string') {
28
+ tag = {
29
+ id: tag.id,
30
+ value: tag.value
31
+ .split(',')
32
+ .map(value => value.trim())
33
+ .filter(value => value.length > 0)
34
+ };
35
+ }
36
+ return super.mapGenericTag(tag, warnings);
37
+ }
38
+ }
@@ -0,0 +1,75 @@
1
+ import type { IGetToken } from 'strtok3';
2
+ declare const CafContentError_base: {
3
+ new (message: string): {
4
+ message: string;
5
+ stack?: string;
6
+ cause?: unknown;
7
+ name: "UnexpectedFileContentError";
8
+ readonly fileType: string;
9
+ toString(): string;
10
+ };
11
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
12
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
13
+ stackTraceLimit: number;
14
+ };
15
+ export declare class CafContentError extends CafContentError_base {
16
+ }
17
+ export interface ICafFileHeader {
18
+ fileType: string;
19
+ fileVersion: number;
20
+ fileFlags: number;
21
+ }
22
+ export declare const FileHeader: IGetToken<ICafFileHeader>;
23
+ export interface ICafChunkHeader {
24
+ chunkType: string;
25
+ /** Size of the data section, -1 if the Audio Data chunk runs to the end of file */
26
+ chunkSize: number;
27
+ }
28
+ export declare const ChunkHeader: IGetToken<ICafChunkHeader>;
29
+ export interface IAudioDescription {
30
+ sampleRate: number;
31
+ formatId: string;
32
+ formatFlags: number;
33
+ bytesPerPacket: number;
34
+ framesPerPacket: number;
35
+ channelsPerFrame: number;
36
+ bitsPerChannel: number;
37
+ }
38
+ /** CAFAudioFormat: Float64 mSampleRate followed by six UInt32 fields */
39
+ export declare const AudioDescription: IGetToken<IAudioDescription>;
40
+ export interface IPacketTableHeader {
41
+ numberPackets: number;
42
+ numberValidFrames: number;
43
+ primingFrames: number;
44
+ remainderFrames: number;
45
+ }
46
+ /** Fixed 24 byte part of a Packet Table chunk */
47
+ export declare const PacketTableHeader: IGetToken<IPacketTableHeader>;
48
+ /**
49
+ * Decodes the base-128, MSB-first integers that follow a Packet Table header,
50
+ * high-order bit set meaning the number continues in the next byte.
51
+ * @returns number of bytes occupied by the entries
52
+ */
53
+ export declare function decodePacketTableEntries(body: Uint8Array, desc: IAudioDescription, numberPackets: number): number;
54
+ export interface IChannelLayoutHeader {
55
+ channelLayoutTag: number;
56
+ channelBitmap: number;
57
+ numberChannelDescriptions: number;
58
+ }
59
+ /** Leading 12 bytes of a Channel Layout chunk, mChannelBitmap is always present */
60
+ export declare const ChannelLayoutHeader: IGetToken<IChannelLayoutHeader>;
61
+ export interface IChannelDescription {
62
+ channelLabel: number;
63
+ channelFlags: number;
64
+ coordinates: [number, number, number];
65
+ }
66
+ export declare const channelDescriptionLen = 20;
67
+ /** Each CAFChannelDescription has a UInt32 label, UInt32 flags and three Float32 coordinates */
68
+ export declare function parseChannelDescriptions(body: Uint8Array, count: number): IChannelDescription[];
69
+ export interface IInfoEntry {
70
+ key: string;
71
+ value: string;
72
+ }
73
+ /** Reads the NUL terminated UTF-8 pairs, the declared entry count is authoritative */
74
+ export declare function parseInfoEntries(body: Uint8Array): IInfoEntry[];
75
+ export {};