music-metadata 12.1.1 → 12.2.1

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))
@@ -175,6 +177,8 @@ parseFile(filePath: string, options?: IOptions): Promise<IAudioMetadata>
175
177
 
176
178
  - This function is **Node.js-only** and relies on Node.js-specific APIs to access the filesystem.
177
179
 
180
+ - The path must resolve to a regular file. Symlinks to regular files are supported. Directories, FIFOs (named pipes), and other non-regular filesystem objects are rejected with `NotRegularFileError` from `strtok3`. FIFOs are rejected without waiting for a writer. To parse streamed audio, use [`parseStream()`](#parsestream-function).
181
+
178
182
  - For browser environments, consider using the [parseBlob](#parseblob-function) to parse [File object](https://developer.mozilla.org/en-US/docs/Web/API/File) objects.
179
183
 
180
184
  ##### Example:
@@ -629,6 +633,10 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
629
633
  - Metadata not listed in the SeekHead may be skipped.
630
634
  - If the SeekHead is missing, this option has no effect.
631
635
 
636
+ For large MKV files on network shares, use `parseFile(filePath, { mkvUseIndex: true })`
637
+ when the file's SeekHead index is reliable. The default traversal reads every cluster header,
638
+ so network latency can dominate even though the audio/video payloads are skipped.
639
+
632
640
  - `observer`: `(update: IMetadataEvent) => void`:
633
641
 
634
642
  Callback function triggered when common tags or format properties are updated during parsing.
@@ -666,8 +674,10 @@ Audio format information. Defined in the TypeScript `IFormat` interface:
666
674
  - `format.codec?` Name of the codec (algorithm used for the audio compression)
667
675
  - `format.codecProfile?: string` Codec profile / settings
668
676
  - `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
677
+ - `format.duration?: number` Duration in seconds (existing audio/container summary; complete presentation duration for video-only files)
678
+ - `format.bitrate?: number` Existing audio/container summary bitrate in bits per second
679
+ - `format.containerDuration?: number` Complete media presentation duration in seconds, including video
680
+ - `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
681
  - `format.sampleRate?: number` Sampling rate in Samples per second (S/s)
672
682
  - `format.bitsPerSample?: number` Audio bit depth
673
683
  - `format.lossless?: boolean` True if lossless, false for lossy encoding
@@ -686,9 +696,14 @@ metadata. Import `TrackType` from `music-metadata` to select tracks without know
686
696
  ```js
687
697
  import { parseFile, TrackType } from 'music-metadata';
688
698
 
689
- const { format } = await parseFile('movie.mp4');
699
+ const { format } = await parseFile('movie.mp4', { duration: true });
690
700
  const audioTracks = format.trackInfo.filter(track => track.type === TrackType.audio);
691
701
  const videoTracks = format.trackInfo.filter(track => track.type === TrackType.video);
702
+ for (const track of videoTracks) {
703
+ console.log(track.duration, track.bitrate, track.video?.pixelWidth, track.video?.pixelHeight);
704
+ console.log(track.video?.frameRate);
705
+ }
706
+ console.log(format.containerDuration, format.overallBitrate);
692
707
  ```
693
708
 
694
709
  Each entry describes one discovered track. The order follows container discovery and does not identify
@@ -698,12 +713,27 @@ logical media streams; Skeleton headers do not create media tracks. Single-strea
698
713
  their format properties to one audio track after parsing.
699
714
 
700
715
  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.
716
+ or summary bitrate is not a per-track bitrate. The existing `format.duration` and `format.bitrate`
717
+ fields retain their summary semantics. Use `containerDuration` for the complete presentation and
718
+ `overallBitrate` for the actual file size multiplied by eight and divided by that duration. These
719
+ statistics remain unset when timing is unavailable; `overallBitrate` also requires a known input size.
720
+
721
+ MPEG-4 derives audio and video timing from media headers, sample tables, and fragments, including
722
+ fragment defaults and decode timestamps. Track bitrates use sample sizes and duration; video properties
723
+ include encoded dimensions, display dimensions, and average frame rate. ASF supplies stream dimensions,
724
+ stream bitrates, and extended stream timing and frame rate when present. Standalone and nested Stream
725
+ Properties Objects describe actual streams; Codec List entries supply names without creating unused tracks.
726
+
727
+ Matroska applies specified defaults for flags, language, channels, and sample rate, and prefers IETF
728
+ language tags when present. Track statistics tags provide duration, bitrate, and frame count; without
729
+ these tags, per-track timing may be unavailable even when segment duration is known. Media clusters
730
+ are not scanned to calculate missing track statistics.
731
+
732
+ Ogg/Theora duration comes from granule positions. Set `duration: true` to allow scanning beyond the
733
+ initial metadata pages. Once a video stream has been fully read, its average bitrate uses that logical
734
+ stream's payload bytes, including codec headers and excluding Ogg framing and other streams. Resolution
735
+ and frame rate are available from the identification header. Audio streams retain their own codec
736
+ bitrate semantics.
707
737
 
708
738
  For Ogg/Opus, the per-track average bitrate uses the logical stream's payload bytes, including codec
709
739
  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 {};