|
|
@@ -1,4 +1,4 @@
|
|
|
-import { taskpool } from '@kit.ArkTS';
|
|
|
+import { taskpool, util } from '@kit.ArkTS';
|
|
|
import fileIo from '@ohos.file.fs';
|
|
|
import nativeBridge from 'libentry.so';
|
|
|
import Logger from './Logger';
|
|
|
@@ -20,6 +20,8 @@ const SACD_FRAME_SIZE_64: number = 4704;
|
|
|
const SACD_FRAME_RATE: number = 75;
|
|
|
const SACD_MAX_PACKET_COUNT: number = 7;
|
|
|
const SACD_MAX_FRAME_BUFFER_SIZE: number = 64 * 1024;
|
|
|
+const SACD_DST_BATCH_MAX_FRAMES: number = 64;
|
|
|
+const SACD_DST_BATCH_MAX_BYTES: number = 256 * 1024;
|
|
|
|
|
|
const SACD_PACKET_TYPE_AUDIO: number = 2;
|
|
|
|
|
|
@@ -34,11 +36,23 @@ const DSF_CHANNEL_TYPE_5_1_CHANNELS: number = 7;
|
|
|
|
|
|
const SACD_FRAME_FORMAT_DST: number = 0;
|
|
|
const BIT_REVERSE_TABLE: Uint8Array = createBitReverseTable();
|
|
|
+const ISO_LISTABLE_MEDIA_EXTENSIONS: string[] = [
|
|
|
+ '.mp3', '.wav', '.wma', '.mp2', '.mov', '.flac', '.midi', '.ra', '.aac', '.ape', '.cda', '.alac', '.m4a', '.mp4',
|
|
|
+ '.wmv', '.mkv', '.3gv', '.m4v', '.avi', '.rmvb', '.flv', '.3g2', '.mpg', '.webm', '.ogv', '.f4v', '.swf', '.dsf',
|
|
|
+ '.vob', '.3gp', '.mpeg', '.ts', '.ogg', '.opus', '.wv', '.aiff', '.amr', '.aif', '.dff', '.au', '.eac3', '.mlp',
|
|
|
+ '.tak', '.thd', '.tta', '.ac3', '.mka', '.mpc', '.dts', '.asf', '.m2ts', '.m4b', '.mts', '.rm', '.wtv', '.av3a',
|
|
|
+ '.dsd'
|
|
|
+];
|
|
|
|
|
|
-const AUDIO_EXTENSIONS: string[] = [
|
|
|
- '.mp3', '.wav', '.wma', '.mp2', '.mov', '.flac', '.midi', '.ra', '.aac', '.ape', '.cda', '.alac', '.m4a',
|
|
|
- '.ogg', '.opus', '.wv', '.aiff', '.amr', '.aif', '.dff', '.au', '.eac3', '.mlp', '.tak', '.thd', '.tta',
|
|
|
- '.ac3', '.mka', '.mpc', '.asf', '.m2ts', '.m4b', '.mts', '.rm', '.wtv', '.dts', '.av3a', '.dsd', '.dsf'
|
|
|
+const SACD_CHARSET_LABELS: string[] = [
|
|
|
+ 'us-ascii',
|
|
|
+ 'us-ascii',
|
|
|
+ 'iso-8859-1',
|
|
|
+ 'shift_jis',
|
|
|
+ 'euc-kr',
|
|
|
+ 'gb18030',
|
|
|
+ 'big5',
|
|
|
+ 'iso-8859-1'
|
|
|
];
|
|
|
|
|
|
interface IsoDirectoryRootDescriptor {
|
|
|
@@ -68,9 +82,28 @@ interface SacdMasterTocInfo {
|
|
|
area2TocSize: number;
|
|
|
}
|
|
|
|
|
|
+interface SacdMasterTextInfo {
|
|
|
+ albumTitle: string;
|
|
|
+ albumArtist: string;
|
|
|
+ discTitle: string;
|
|
|
+ discArtist: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface SacdTrackTextInfo {
|
|
|
+ title: string;
|
|
|
+ performer: string;
|
|
|
+ songwriter: string;
|
|
|
+ composer: string;
|
|
|
+ arranger: string;
|
|
|
+ message: string;
|
|
|
+ extraMessage: string;
|
|
|
+}
|
|
|
+
|
|
|
interface SacdAreaTocInfo {
|
|
|
trackOffset: number;
|
|
|
trackCount: number;
|
|
|
+ trackStartLsn: number;
|
|
|
+ trackEndLsn: number;
|
|
|
channelCount?: number;
|
|
|
frameFormat?: number;
|
|
|
}
|
|
|
@@ -89,6 +122,7 @@ interface SacdPacketInfo {
|
|
|
}
|
|
|
|
|
|
interface SacdFrameInfo {
|
|
|
+ timecodeFrames: number;
|
|
|
sectorCount: number;
|
|
|
channelBit2?: boolean;
|
|
|
channelBit3?: boolean;
|
|
|
@@ -98,11 +132,25 @@ interface SacdFrameAssembler {
|
|
|
started: boolean;
|
|
|
size: number;
|
|
|
channelCount: number;
|
|
|
+ timecodeFrames: number;
|
|
|
dstEncoded: boolean;
|
|
|
sectorCount: number;
|
|
|
buffer: Uint8Array;
|
|
|
}
|
|
|
|
|
|
+interface SacdTrackFrameFilter {
|
|
|
+ enabled: boolean;
|
|
|
+ startFrame: number;
|
|
|
+ endFrameExclusive: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface SacdDstFrameBatch {
|
|
|
+ totalPayloadBytes: number;
|
|
|
+ frameCount: number;
|
|
|
+ frameLengths: number[];
|
|
|
+ frameChunks: Uint8Array[];
|
|
|
+}
|
|
|
+
|
|
|
interface DsfWriterState {
|
|
|
file: fileIo.File;
|
|
|
channelCount: number;
|
|
|
@@ -135,6 +183,11 @@ export interface IsoArchiveHelperAudioEntry {
|
|
|
areaType?: string;
|
|
|
channelCount?: number;
|
|
|
frameFormat?: number;
|
|
|
+ title?: string;
|
|
|
+ album?: string;
|
|
|
+ artist?: string;
|
|
|
+ albumArtist?: string;
|
|
|
+ comment?: string;
|
|
|
}
|
|
|
|
|
|
export interface IsoArchiveHelperExtractTaskResult {
|
|
|
@@ -161,6 +214,7 @@ export type IsoArchiveExtractProgressReporter = (message: IsoArchiveHelperExtrac
|
|
|
interface SacdDstNativeModule {
|
|
|
createSacdDstDecoder(channelCount: number, samplerate: number, framerate: number): number;
|
|
|
decodeSacdDstFrame(decoderId: number, dstFrame: ArrayBuffer): ArrayBuffer;
|
|
|
+ decodeSacdDstFrames(decoderId: number, payload: ArrayBuffer, frameLengths: ArrayBuffer): ArrayBuffer;
|
|
|
releaseSacdDstDecoder(decoderId: number): void;
|
|
|
}
|
|
|
|
|
|
@@ -179,11 +233,32 @@ interface IsoEntryExtractResult {
|
|
|
|
|
|
export function listIsoAudioEntriesCore(isoPath: string): IsoArchiveHelperAudioEntry[] {
|
|
|
Logger.info('IsoArchiveTaskHelper', `heanup ISO list start path=${isoPath}`);
|
|
|
- const standardEntries: IsoArchiveHelperAudioEntry[] = listStandardIsoAudioEntries(isoPath);
|
|
|
- if (standardEntries.length > 0) {
|
|
|
- return standardEntries;
|
|
|
+ const sacdEntries: IsoArchiveHelperAudioEntry[] = listSacdAudioEntries(isoPath);
|
|
|
+ if (sacdEntries.length > 0) {
|
|
|
+ return sacdEntries;
|
|
|
+ }
|
|
|
+ if (isSacdIsoPath(isoPath)) {
|
|
|
+ Logger.warn('IsoArchiveTaskHelper', `heanup SACD ISO detected but no track entries parsed path=${isoPath}`);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ return listStandardIsoEntries(isoPath);
|
|
|
+}
|
|
|
+
|
|
|
+function isSacdIsoPath(isoPath: string): boolean {
|
|
|
+ let isoFile: fileIo.File | undefined = undefined;
|
|
|
+ try {
|
|
|
+ isoFile = fileIo.openSync(isoPath, fileIo.OpenMode.READ_ONLY);
|
|
|
+ return parseSacdMasterToc(isoFile) !== null;
|
|
|
+ } catch (_error) {
|
|
|
+ return false;
|
|
|
+ } finally {
|
|
|
+ if (isoFile) {
|
|
|
+ try {
|
|
|
+ fileIo.closeSync(isoFile);
|
|
|
+ } catch (_error) {
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- return listSacdAudioEntries(isoPath);
|
|
|
}
|
|
|
|
|
|
export function extractIsoAudioEntriesCore(payloadJson: string,
|
|
|
@@ -256,7 +331,8 @@ export function extractIsoAudioEntriesCore(payloadJson: string,
|
|
|
}
|
|
|
if (progressState) {
|
|
|
progressState.completedUnits = progressState.totalUnits;
|
|
|
- reportIsoExtractProgress(progressState, '', 'done', true, progressReporter);
|
|
|
+ const finalStage: string = result.failedEntries.length > 0 ? 'error' : 'done';
|
|
|
+ reportIsoExtractProgress(progressState, '', finalStage, true, progressReporter);
|
|
|
}
|
|
|
} catch (_error) {
|
|
|
for (let index: number = 0; index < payload.entries.length; index++) {
|
|
|
@@ -337,6 +413,9 @@ function reportIsoExtractProgress(progressState: IsoExtractProgressState | undef
|
|
|
} else {
|
|
|
progress = Math.ceil((progressState.completedUnits / progressState.totalUnits) * 100);
|
|
|
}
|
|
|
+ if (stage !== 'done' && progress >= 100) {
|
|
|
+ progress = 99;
|
|
|
+ }
|
|
|
if (!force && progress <= progressState.lastProgress) {
|
|
|
return;
|
|
|
}
|
|
|
@@ -358,7 +437,7 @@ function reportIsoExtractProgress(progressState: IsoExtractProgressState | undef
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function listStandardIsoAudioEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
|
|
|
+function listStandardIsoEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
|
|
|
let isoFile: fileIo.File | undefined = undefined;
|
|
|
try {
|
|
|
isoFile = fileIo.openSync(isoPath, fileIo.OpenMode.READ_ONLY);
|
|
|
@@ -492,7 +571,7 @@ function collectIsoAudioEntries(isoFile: fileIo.File, rootDescriptor: IsoDirecto
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- if (!isAudioFileName(record.name)) {
|
|
|
+ if (!isListableMediaFile(record.name)) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
@@ -585,16 +664,6 @@ function decodeJolietString(bytes: Uint8Array): string {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
-function isAudioFileName(fileName: string): boolean {
|
|
|
- const extension: string = getFileExtension(fileName).toLowerCase();
|
|
|
- for (let index: number = 0; index < AUDIO_EXTENSIONS.length; index++) {
|
|
|
- if (extension === AUDIO_EXTENSIONS[index]) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- return false;
|
|
|
-}
|
|
|
-
|
|
|
function getFileExtension(fileName: string): string {
|
|
|
const lastDot: number = fileName.lastIndexOf('.');
|
|
|
if (lastDot < 0) {
|
|
|
@@ -603,6 +672,16 @@ function getFileExtension(fileName: string): string {
|
|
|
return fileName.substring(lastDot);
|
|
|
}
|
|
|
|
|
|
+function isListableMediaFile(fileName: string): boolean {
|
|
|
+ const extension: string = getFileExtension(fileName).toLowerCase();
|
|
|
+ for (let index: number = 0; index < ISO_LISTABLE_MEDIA_EXTENSIONS.length; index++) {
|
|
|
+ if (extension === ISO_LISTABLE_MEDIA_EXTENSIONS[index]) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
function extractStandardIsoEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudioEntry, outputPath: string,
|
|
|
progressState?: IsoExtractProgressState, progressReporter?: IsoArchiveExtractProgressReporter): IsoEntryExtractResult {
|
|
|
if (entry.extent <= 0 || entry.size <= 0) {
|
|
|
@@ -668,18 +747,18 @@ function listSacdAudioEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
|
|
|
if (!masterToc) {
|
|
|
return [];
|
|
|
}
|
|
|
+ const masterText: SacdMasterTextInfo = parseSacdMasterText(isoFile);
|
|
|
|
|
|
const stereoEntries: IsoArchiveHelperAudioEntry[] = parseSacdAreaEntries(
|
|
|
isoFile,
|
|
|
masterToc.area1Toc1Start,
|
|
|
masterToc.area1TocSize,
|
|
|
- '2CH'
|
|
|
+ '2CH',
|
|
|
+ masterText
|
|
|
);
|
|
|
- if (stereoEntries.length > 0) {
|
|
|
- return stereoEntries;
|
|
|
- }
|
|
|
-
|
|
|
- return parseSacdAreaEntries(isoFile, masterToc.area2Toc1Start, masterToc.area2TocSize, 'MCH');
|
|
|
+ const multichannelEntries: IsoArchiveHelperAudioEntry[] =
|
|
|
+ parseSacdAreaEntries(isoFile, masterToc.area2Toc1Start, masterToc.area2TocSize, 'MCH', masterText);
|
|
|
+ return stereoEntries.concat(multichannelEntries);
|
|
|
} catch (_error) {
|
|
|
return [];
|
|
|
} finally {
|
|
|
@@ -711,7 +790,7 @@ function parseSacdMasterToc(isoFile: fileIo.File): SacdMasterTocInfo | null {
|
|
|
}
|
|
|
|
|
|
function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTocSize: number,
|
|
|
- areaType: string): IsoArchiveHelperAudioEntry[] {
|
|
|
+ areaType: string, masterText: SacdMasterTextInfo): IsoArchiveHelperAudioEntry[] {
|
|
|
if (areaStartLsn <= 0 || areaTocSize <= 0) {
|
|
|
return [];
|
|
|
}
|
|
|
@@ -729,12 +808,15 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
|
|
|
const areaInfo: SacdAreaTocInfo = {
|
|
|
trackOffset: areaData[68],
|
|
|
trackCount: areaData[69],
|
|
|
+ trackStartLsn: readUInt32BE(areaData, 72),
|
|
|
+ trackEndLsn: readUInt32BE(areaData, 76),
|
|
|
channelCount: areaData[32],
|
|
|
frameFormat: areaData[21] & 0x0f
|
|
|
};
|
|
|
- if (areaInfo.trackCount <= 0) {
|
|
|
+ if (areaInfo.trackCount <= 0 || areaInfo.trackStartLsn <= 0 || areaInfo.trackEndLsn < areaInfo.trackStartLsn) {
|
|
|
return [];
|
|
|
}
|
|
|
+ const trackTextList: SacdTrackTextInfo[] = parseSacdAreaTrackTexts(areaData, areaInfo.trackCount);
|
|
|
|
|
|
let trackListOffsetBlock: Uint8Array | null = null;
|
|
|
let trackListTimeBlock: Uint8Array | null = null;
|
|
|
@@ -757,14 +839,17 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
|
|
|
const result: IsoArchiveHelperAudioEntry[] = [];
|
|
|
const maxTrackCount: number = areaInfo.trackCount > 255 ? 255 : areaInfo.trackCount;
|
|
|
for (let index: number = 0; index < maxTrackCount; index++) {
|
|
|
- const startLsn: number = readUInt32BE(offsetBlock, 8 + index * 4);
|
|
|
+ const listedStartLsn: number = readUInt32BE(offsetBlock, 8 + index * 4);
|
|
|
+ const startLsn: number = index > 0 ? listedStartLsn : areaInfo.trackStartLsn;
|
|
|
const rawLengthLsn: number = readUInt32BE(offsetBlock, 8 + 255 * 4 + index * 4);
|
|
|
let lengthLsn: number = rawLengthLsn;
|
|
|
if (index + 1 < maxTrackCount) {
|
|
|
const nextStartLsn: number = readUInt32BE(offsetBlock, 8 + (index + 1) * 4);
|
|
|
if (nextStartLsn > startLsn) {
|
|
|
- lengthLsn = nextStartLsn - startLsn;
|
|
|
+ lengthLsn = nextStartLsn - startLsn + 1;
|
|
|
}
|
|
|
+ } else if (areaInfo.trackEndLsn >= startLsn) {
|
|
|
+ lengthLsn = areaInfo.trackEndLsn - startLsn + 1;
|
|
|
}
|
|
|
if (startLsn <= 0 || lengthLsn <= 0) {
|
|
|
continue;
|
|
|
@@ -778,12 +863,33 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
|
|
|
}
|
|
|
|
|
|
const trackNumber: number = areaInfo.trackOffset > 0 ? areaInfo.trackOffset + index : index + 1;
|
|
|
- const trackLabel: string = areaType === 'MCH' ? `MCH ${padTrackNumber(trackNumber)}. Track ${padTrackNumber(trackNumber)}.dsf`
|
|
|
- : `${padTrackNumber(trackNumber)}. Track ${padTrackNumber(trackNumber)}.dsf`;
|
|
|
+ const trackText: SacdTrackTextInfo = index < trackTextList.length ? trackTextList[index] : createEmptySacdTrackText();
|
|
|
+ const trackTitle: string = firstNonEmptySacdText([
|
|
|
+ trackText.title,
|
|
|
+ trackText.message,
|
|
|
+ `Track ${padTrackNumber(trackNumber)}`
|
|
|
+ ]);
|
|
|
+ const safeTrackTitle: string = sanitizeSacdPathSegment(trackTitle);
|
|
|
+ const displayLabel: string = `${padTrackNumber(trackNumber)}. ${safeTrackTitle}.dsf`;
|
|
|
+ const trackPath: string = areaType === 'MCH' ? `MCH/${displayLabel}` : displayLabel;
|
|
|
+ const outputName: string = `${trackTitle}.dsf`;
|
|
|
+ const albumTitle: string = firstNonEmptySacdText([
|
|
|
+ masterText.albumTitle,
|
|
|
+ masterText.discTitle
|
|
|
+ ]);
|
|
|
+ const artistName: string = firstNonEmptySacdText([
|
|
|
+ trackText.performer,
|
|
|
+ masterText.albumArtist,
|
|
|
+ masterText.discArtist
|
|
|
+ ]);
|
|
|
+ const commentText: string = firstNonEmptySacdText([
|
|
|
+ trackText.message,
|
|
|
+ trackText.extraMessage
|
|
|
+ ]);
|
|
|
|
|
|
result.push({
|
|
|
- path: trackLabel,
|
|
|
- name: trackLabel,
|
|
|
+ path: trackPath,
|
|
|
+ name: outputName,
|
|
|
size: lengthLsn * ISO_SECTOR_SIZE,
|
|
|
extent: startLsn,
|
|
|
entryType: 'sacd_track',
|
|
|
@@ -794,7 +900,12 @@ function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTo
|
|
|
durationFrames,
|
|
|
areaType,
|
|
|
channelCount: areaInfo.channelCount,
|
|
|
- frameFormat: areaInfo.frameFormat
|
|
|
+ frameFormat: areaInfo.frameFormat,
|
|
|
+ title: trackTitle,
|
|
|
+ album: albumTitle,
|
|
|
+ artist: artistName,
|
|
|
+ albumArtist: firstNonEmptySacdText([masterText.albumArtist, masterText.discArtist]),
|
|
|
+ comment: commentText
|
|
|
});
|
|
|
Logger.info('IsoArchiveTaskHelper',
|
|
|
`heanup SACD track meta index=${index}, startLsn=${startLsn}, rawLengthLsn=${rawLengthLsn}, ` +
|
|
|
@@ -814,6 +925,186 @@ function readSacdTrackTimeFrames(trackListTimeBlock: Uint8Array, offset: number)
|
|
|
return minutes * 60 * 75 + seconds * 75 + frames;
|
|
|
}
|
|
|
|
|
|
+function parseSacdMasterText(isoFile: fileIo.File): SacdMasterTextInfo {
|
|
|
+ const emptyText: SacdMasterTextInfo = createEmptySacdMasterText();
|
|
|
+ const masterData: Uint8Array = readBytes(isoFile, SACD_START_OF_MASTER_TOC * ISO_SECTOR_SIZE,
|
|
|
+ SACD_MASTER_TOC_LEN * ISO_SECTOR_SIZE);
|
|
|
+ if (masterData.length < ISO_SECTOR_SIZE * 2 || readAscii(masterData, 0, 8) !== 'SACDMTOC') {
|
|
|
+ return emptyText;
|
|
|
+ }
|
|
|
+ const textAreaCount: number = masterData[128];
|
|
|
+ if (textAreaCount <= 0) {
|
|
|
+ return emptyText;
|
|
|
+ }
|
|
|
+
|
|
|
+ const textBlockStart: number = ISO_SECTOR_SIZE;
|
|
|
+ const textBlock: Uint8Array = masterData.subarray(textBlockStart, textBlockStart + ISO_SECTOR_SIZE);
|
|
|
+ if (textBlock.length < ISO_SECTOR_SIZE || readAscii(textBlock, 0, 8) !== 'SACDText') {
|
|
|
+ return emptyText;
|
|
|
+ }
|
|
|
+
|
|
|
+ const charsetIndex: number = masterData[138] & 0x07;
|
|
|
+ return {
|
|
|
+ albumTitle: readSacdTextByOffset(textBlock, readUInt16BE(textBlock, 16), charsetIndex),
|
|
|
+ albumArtist: readSacdTextByOffset(textBlock, readUInt16BE(textBlock, 18), charsetIndex),
|
|
|
+ discTitle: readSacdTextByOffset(textBlock, readUInt16BE(textBlock, 32), charsetIndex),
|
|
|
+ discArtist: readSacdTextByOffset(textBlock, readUInt16BE(textBlock, 34), charsetIndex)
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function parseSacdAreaTrackTexts(areaData: Uint8Array, trackCount: number): SacdTrackTextInfo[] {
|
|
|
+ const texts: SacdTrackTextInfo[] = [];
|
|
|
+ for (let index: number = 0; index < trackCount; index++) {
|
|
|
+ texts.push(createEmptySacdTrackText());
|
|
|
+ }
|
|
|
+ if (trackCount <= 0 || areaData.length < ISO_SECTOR_SIZE) {
|
|
|
+ return texts;
|
|
|
+ }
|
|
|
+
|
|
|
+ const charsetIndex: number = areaData.length > 90 ? (areaData[90] & 0x07) : 0;
|
|
|
+ const sectorCount: number = Math.floor(areaData.length / ISO_SECTOR_SIZE);
|
|
|
+ for (let sectorIndex: number = 0; sectorIndex < sectorCount; sectorIndex++) {
|
|
|
+ const offset: number = sectorIndex * ISO_SECTOR_SIZE;
|
|
|
+ if (readAscii(areaData, offset, 8) !== 'SACDTTxt') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const textBlock: Uint8Array = areaData.subarray(offset, offset + ISO_SECTOR_SIZE);
|
|
|
+ for (let trackIndex: number = 0; trackIndex < trackCount; trackIndex++) {
|
|
|
+ const textOffset: number = readUInt16BE(textBlock, 8 + trackIndex * 2);
|
|
|
+ if (textOffset <= 0 || textOffset >= textBlock.length) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ let textPointer: number = textOffset;
|
|
|
+ const textAmount: number = textBlock[textPointer];
|
|
|
+ if (textAmount <= 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ textPointer += 4;
|
|
|
+ for (let textIndex: number = 0; textIndex < textAmount && textPointer < textBlock.length; textIndex++) {
|
|
|
+ const textType: number = textBlock[textPointer];
|
|
|
+ textPointer += 2;
|
|
|
+ const endOffset: number = findSacdCStringEnd(textBlock, textPointer);
|
|
|
+ const value: string = decodeSacdTextRange(textBlock, textPointer, endOffset, charsetIndex);
|
|
|
+ applySacdTrackText(texts[trackIndex], textType, value);
|
|
|
+ textPointer = endOffset;
|
|
|
+ while (textPointer < textBlock.length && textBlock[textPointer] === 0) {
|
|
|
+ textPointer++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return texts;
|
|
|
+}
|
|
|
+
|
|
|
+function createEmptySacdMasterText(): SacdMasterTextInfo {
|
|
|
+ return {
|
|
|
+ albumTitle: '',
|
|
|
+ albumArtist: '',
|
|
|
+ discTitle: '',
|
|
|
+ discArtist: ''
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function createEmptySacdTrackText(): SacdTrackTextInfo {
|
|
|
+ return {
|
|
|
+ title: '',
|
|
|
+ performer: '',
|
|
|
+ songwriter: '',
|
|
|
+ composer: '',
|
|
|
+ arranger: '',
|
|
|
+ message: '',
|
|
|
+ extraMessage: ''
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function readSacdTextByOffset(textBlock: Uint8Array, offset: number, charsetIndex: number): string {
|
|
|
+ if (offset <= 0 || offset >= textBlock.length) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ const endOffset: number = findSacdCStringEnd(textBlock, offset);
|
|
|
+ return decodeSacdTextRange(textBlock, offset, endOffset, charsetIndex);
|
|
|
+}
|
|
|
+
|
|
|
+function findSacdCStringEnd(buffer: Uint8Array, start: number): number {
|
|
|
+ let cursor: number = start;
|
|
|
+ while (cursor < buffer.length && buffer[cursor] !== 0) {
|
|
|
+ cursor++;
|
|
|
+ }
|
|
|
+ return cursor;
|
|
|
+}
|
|
|
+
|
|
|
+function decodeSacdTextRange(buffer: Uint8Array, start: number, end: number, charsetIndex: number): string {
|
|
|
+ if (start >= end || start < 0 || end > buffer.length) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ const bytes: Uint8Array = buffer.subarray(start, end);
|
|
|
+ const encoding: string = charsetIndex >= 0 && charsetIndex < SACD_CHARSET_LABELS.length ?
|
|
|
+ SACD_CHARSET_LABELS[charsetIndex] : 'utf-8';
|
|
|
+ try {
|
|
|
+ const decoder = new util.TextDecoder(encoding);
|
|
|
+ return sanitizeSacdText(decoder.decode(bytes));
|
|
|
+ } catch (_error) {
|
|
|
+ return sanitizeSacdText(decodeAsciiString(bytes));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function sanitizeSacdText(value: string): string {
|
|
|
+ if (!value) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ return value.replace(/\u0000/g, '').replace(/\s+/g, ' ').trim();
|
|
|
+}
|
|
|
+
|
|
|
+function sanitizeSacdPathSegment(value: string): string {
|
|
|
+ const sanitized: string = sanitizeSacdText(value).replace(/[\\/:*?"<>|]/g, '_').trim();
|
|
|
+ if (sanitized.length > 0) {
|
|
|
+ return sanitized;
|
|
|
+ }
|
|
|
+ return 'Track';
|
|
|
+}
|
|
|
+
|
|
|
+function applySacdTrackText(target: SacdTrackTextInfo, textType: number, value: string): void {
|
|
|
+ if (!value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ switch (textType) {
|
|
|
+ case 0x01:
|
|
|
+ target.title = value;
|
|
|
+ break;
|
|
|
+ case 0x02:
|
|
|
+ target.performer = value;
|
|
|
+ break;
|
|
|
+ case 0x03:
|
|
|
+ target.songwriter = value;
|
|
|
+ break;
|
|
|
+ case 0x04:
|
|
|
+ target.composer = value;
|
|
|
+ break;
|
|
|
+ case 0x05:
|
|
|
+ target.arranger = value;
|
|
|
+ break;
|
|
|
+ case 0x06:
|
|
|
+ target.message = value;
|
|
|
+ break;
|
|
|
+ case 0x07:
|
|
|
+ target.extraMessage = value;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function firstNonEmptySacdText(values: string[]): string {
|
|
|
+ for (let index: number = 0; index < values.length; index++) {
|
|
|
+ const value: string = sanitizeSacdText(values[index]);
|
|
|
+ if (value.length > 0) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+}
|
|
|
+
|
|
|
function padTrackNumber(trackNumber: number): string {
|
|
|
if (trackNumber < 10) {
|
|
|
return `0${trackNumber}`;
|
|
|
@@ -914,6 +1205,7 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
|
|
|
}
|
|
|
const channelCount: number = entry.channelCount && entry.channelCount > 0 ? entry.channelCount : areaInfo.channelCount;
|
|
|
const frameFormat: number = entry.frameFormat !== undefined ? entry.frameFormat : areaInfo.frameFormat;
|
|
|
+ const frameFilter: SacdTrackFrameFilter = createSacdTrackFrameFilter(entry);
|
|
|
Logger.info('IsoArchiveTaskHelper', `heanup SACD area resolved channels=${channelCount}, frameFormat=${frameFormat}, areaFrameFormat=${areaInfo.frameFormat}`);
|
|
|
let dstDecoderId: number | undefined = undefined;
|
|
|
if (frameFormat === SACD_FRAME_FORMAT_DST) {
|
|
|
@@ -950,12 +1242,14 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
|
|
|
started: false,
|
|
|
size: 0,
|
|
|
channelCount,
|
|
|
+ timecodeFrames: -1,
|
|
|
dstEncoded: false,
|
|
|
sectorCount: 0,
|
|
|
buffer: new Uint8Array(SACD_MAX_FRAME_BUFFER_SIZE)
|
|
|
};
|
|
|
+ const dstFrameBatch: SacdDstFrameBatch = createSacdDstFrameBatch();
|
|
|
|
|
|
- const sectorsPerBatch: number = 128;
|
|
|
+ const sectorsPerBatch: number = 512;
|
|
|
let sectorIndex: number = 0;
|
|
|
while (sectorIndex < entry.lengthLsn) {
|
|
|
const remainingSectors: number = entry.lengthLsn - sectorIndex;
|
|
|
@@ -978,6 +1272,8 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
|
|
|
assembler,
|
|
|
writer,
|
|
|
dstDecoderId,
|
|
|
+ dstFrameBatch,
|
|
|
+ frameFilter,
|
|
|
lastSector
|
|
|
);
|
|
|
if (!processed) {
|
|
|
@@ -993,6 +1289,18 @@ function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudi
|
|
|
progressReporter);
|
|
|
}
|
|
|
|
|
|
+ if (!flushSacdDstFrameBatch(dstFrameBatch, writer, dstDecoderId)) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ reason: '批量解码 SACD 音轨失败'
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (writer.sampleBytes <= 0) {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ reason: 'SACD 音轨未提取到有效音频帧'
|
|
|
+ };
|
|
|
+ }
|
|
|
finalizeDsfWriter(writer);
|
|
|
Logger.info('IsoArchiveTaskHelper', `heanup SACD extract success path=${entry.path}, output=${outputPath}`);
|
|
|
return { success: true };
|
|
|
@@ -1154,7 +1462,9 @@ function writeSacdFrameToDsf(writer: DsfWriterState, frameData: Uint8Array, chan
|
|
|
}
|
|
|
|
|
|
function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, assembler: SacdFrameAssembler,
|
|
|
- writer: DsfWriterState, dstDecoderId: number | undefined, lastSector: boolean): boolean {
|
|
|
+ writer: DsfWriterState, dstDecoderId: number | undefined, dstFrameBatch: SacdDstFrameBatch,
|
|
|
+ frameFilter: SacdTrackFrameFilter,
|
|
|
+ lastSector: boolean): boolean {
|
|
|
let offset: number = 0;
|
|
|
const sectorHeader: number = sectorBuffer[offset];
|
|
|
offset++;
|
|
|
@@ -1192,6 +1502,7 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
|
|
|
}
|
|
|
const extraByte: number = sectorBuffer[offset + 3];
|
|
|
frameInfos.push({
|
|
|
+ timecodeFrames: readSacdTimecodeFrames(sectorBuffer, offset),
|
|
|
sectorCount: (extraByte >> 2) & 0x1f,
|
|
|
channelBit2: ((extraByte >> 1) & 0x01) === 1,
|
|
|
channelBit3: (extraByte & 0x01) === 1
|
|
|
@@ -1199,6 +1510,7 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
|
|
|
offset += 4;
|
|
|
} else {
|
|
|
frameInfos.push({
|
|
|
+ timecodeFrames: readSacdTimecodeFrames(sectorBuffer, offset),
|
|
|
sectorCount: 0,
|
|
|
channelBit2: false,
|
|
|
channelBit3: false
|
|
|
@@ -1216,14 +1528,8 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
|
|
|
|
|
|
if (packet.dataType === SACD_PACKET_TYPE_AUDIO) {
|
|
|
if (packet.frameStart) {
|
|
|
- if (assembler.started && assembler.size > 0) {
|
|
|
- if (!isSacdFrameReady(assembler)) {
|
|
|
- Logger.error('IsoArchiveTaskHelper',
|
|
|
- `heanup SACD frameStart before previous frame ready: size=${assembler.size}, ` +
|
|
|
- `dst=${assembler.dstEncoded}, remainingSectorCount=${assembler.sectorCount}`);
|
|
|
- return false;
|
|
|
- }
|
|
|
- if (!finalizeSacdFrame(assembler, writer, dstDecoderId)) {
|
|
|
+ if (assembler.started && assembler.size > 0 && isSacdFrameReady(assembler)) {
|
|
|
+ if (!finalizeSacdFrame(assembler, writer, dstDecoderId, dstFrameBatch, frameFilter)) {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
@@ -1234,6 +1540,7 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
|
|
|
assembler.dstEncoded = dstEncoded;
|
|
|
assembler.sectorCount = frameInfo ? frameInfo.sectorCount : 0;
|
|
|
assembler.channelCount = resolveSacdFrameChannelCount(frameInfo, areaChannelCount);
|
|
|
+ assembler.timecodeFrames = frameInfo ? frameInfo.timecodeFrames : -1;
|
|
|
frameInfoIndex++;
|
|
|
}
|
|
|
|
|
|
@@ -1242,12 +1549,6 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
|
|
|
return false;
|
|
|
}
|
|
|
if (assembler.dstEncoded) {
|
|
|
- if (assembler.sectorCount <= 0) {
|
|
|
- Logger.error('IsoArchiveTaskHelper',
|
|
|
- `heanup SACD DST packet overflow size=${assembler.size}, packetLength=${packet.packetLength}, ` +
|
|
|
- `remainingSectorCount=${assembler.sectorCount}`);
|
|
|
- return false;
|
|
|
- }
|
|
|
assembler.sectorCount--;
|
|
|
}
|
|
|
}
|
|
|
@@ -1261,19 +1562,17 @@ function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, a
|
|
|
return true;
|
|
|
}
|
|
|
if (!isSacdFrameReady(assembler)) {
|
|
|
- Logger.error('IsoArchiveTaskHelper',
|
|
|
- `heanup SACD last sector frame incomplete size=${assembler.size}, dst=${assembler.dstEncoded}, ` +
|
|
|
- `remainingSectorCount=${assembler.sectorCount}`);
|
|
|
resetSacdFrameAssembler(assembler);
|
|
|
- return false;
|
|
|
+ return true;
|
|
|
}
|
|
|
- return finalizeSacdFrame(assembler, writer, dstDecoderId);
|
|
|
+ return finalizeSacdFrame(assembler, writer, dstDecoderId, dstFrameBatch, frameFilter);
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
function finalizeSacdFrame(assembler: SacdFrameAssembler, writer: DsfWriterState,
|
|
|
- dstDecoderId: number | undefined): boolean {
|
|
|
+ dstDecoderId: number | undefined, dstFrameBatch: SacdDstFrameBatch,
|
|
|
+ frameFilter: SacdTrackFrameFilter): boolean {
|
|
|
if (!assembler.started || assembler.size <= 0) {
|
|
|
resetSacdFrameAssembler(assembler);
|
|
|
return true;
|
|
|
@@ -1285,26 +1584,33 @@ function finalizeSacdFrame(assembler: SacdFrameAssembler, writer: DsfWriterState
|
|
|
resetSacdFrameAssembler(assembler);
|
|
|
return false;
|
|
|
}
|
|
|
- let frameData: Uint8Array = assembler.buffer.subarray(0, assembler.size);
|
|
|
+ const frameData: Uint8Array = assembler.buffer.subarray(0, assembler.size);
|
|
|
+ if (!shouldWriteSacdFrame(frameFilter, assembler.timecodeFrames)) {
|
|
|
+ resetSacdFrameAssembler(assembler);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
if (assembler.dstEncoded) {
|
|
|
if (!dstDecoderId || dstDecoderId <= 0) {
|
|
|
resetSacdFrameAssembler(assembler);
|
|
|
return false;
|
|
|
}
|
|
|
- try {
|
|
|
- const dstFrameBuffer: ArrayBuffer = copySacdFrameToArrayBuffer(frameData);
|
|
|
- const decodedBuffer: ArrayBuffer = sacdDstNativeBridge.decodeSacdDstFrame(dstDecoderId, dstFrameBuffer);
|
|
|
- frameData = new Uint8Array(decodedBuffer);
|
|
|
- } catch (error) {
|
|
|
- Logger.error('IsoArchiveTaskHelper', `heanup DST decode throw: ${(error as Error).message}`);
|
|
|
+ if (!appendSacdDstFrameBatch(dstFrameBatch, frameData)) {
|
|
|
resetSacdFrameAssembler(assembler);
|
|
|
return false;
|
|
|
}
|
|
|
- if (frameData.length === 0 || frameData.length % assembler.channelCount !== 0) {
|
|
|
+ if (shouldFlushSacdDstFrameBatch(dstFrameBatch)) {
|
|
|
+ const flushed: boolean = flushSacdDstFrameBatch(dstFrameBatch, writer, dstDecoderId);
|
|
|
resetSacdFrameAssembler(assembler);
|
|
|
- return false;
|
|
|
+ return flushed;
|
|
|
}
|
|
|
- } else if (assembler.size % assembler.channelCount !== 0) {
|
|
|
+ resetSacdFrameAssembler(assembler);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (!flushSacdDstFrameBatch(dstFrameBatch, writer, dstDecoderId)) {
|
|
|
+ resetSacdFrameAssembler(assembler);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (assembler.size % assembler.channelCount !== 0) {
|
|
|
resetSacdFrameAssembler(assembler);
|
|
|
return false;
|
|
|
}
|
|
|
@@ -1320,14 +1626,7 @@ function isSacdFrameReady(assembler: SacdFrameAssembler): boolean {
|
|
|
if (assembler.dstEncoded) {
|
|
|
return assembler.sectorCount === 0;
|
|
|
}
|
|
|
- return assembler.size % SACD_FRAME_SIZE_64 === 0;
|
|
|
-}
|
|
|
-
|
|
|
-function copySacdFrameToArrayBuffer(frameData: Uint8Array): ArrayBuffer {
|
|
|
- const buffer: ArrayBuffer = new ArrayBuffer(frameData.length);
|
|
|
- const copied: Uint8Array = new Uint8Array(buffer);
|
|
|
- copied.set(frameData);
|
|
|
- return buffer;
|
|
|
+ return assembler.size === assembler.channelCount * SACD_FRAME_SIZE_64;
|
|
|
}
|
|
|
|
|
|
function appendSacdFrameBytes(assembler: SacdFrameAssembler, source: Uint8Array): boolean {
|
|
|
@@ -1342,10 +1641,128 @@ function appendSacdFrameBytes(assembler: SacdFrameAssembler, source: Uint8Array)
|
|
|
function resetSacdFrameAssembler(assembler: SacdFrameAssembler): void {
|
|
|
assembler.started = false;
|
|
|
assembler.size = 0;
|
|
|
+ assembler.timecodeFrames = -1;
|
|
|
assembler.dstEncoded = false;
|
|
|
assembler.sectorCount = 0;
|
|
|
}
|
|
|
|
|
|
+function createSacdTrackFrameFilter(entry: IsoArchiveHelperAudioEntry): SacdTrackFrameFilter {
|
|
|
+ if ((entry.startTimeFrames === undefined || entry.startTimeFrames < 0) ||
|
|
|
+ !entry.durationFrames || entry.durationFrames <= 0) {
|
|
|
+ return {
|
|
|
+ enabled: false,
|
|
|
+ startFrame: 0,
|
|
|
+ endFrameExclusive: 0
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ enabled: true,
|
|
|
+ startFrame: entry.startTimeFrames,
|
|
|
+ endFrameExclusive: entry.startTimeFrames + entry.durationFrames
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function createSacdDstFrameBatch(): SacdDstFrameBatch {
|
|
|
+ return {
|
|
|
+ totalPayloadBytes: 0,
|
|
|
+ frameCount: 0,
|
|
|
+ frameLengths: [],
|
|
|
+ frameChunks: []
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function appendSacdDstFrameBatch(batch: SacdDstFrameBatch, frameData: Uint8Array): boolean {
|
|
|
+ if (frameData.length <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const copiedFrame: Uint8Array = new Uint8Array(frameData.length);
|
|
|
+ copiedFrame.set(frameData);
|
|
|
+ batch.frameChunks.push(copiedFrame);
|
|
|
+ batch.frameLengths.push(frameData.length);
|
|
|
+ batch.totalPayloadBytes += frameData.length;
|
|
|
+ batch.frameCount++;
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+function shouldFlushSacdDstFrameBatch(batch: SacdDstFrameBatch): boolean {
|
|
|
+ return batch.frameCount >= SACD_DST_BATCH_MAX_FRAMES || batch.totalPayloadBytes >= SACD_DST_BATCH_MAX_BYTES;
|
|
|
+}
|
|
|
+
|
|
|
+function flushSacdDstFrameBatch(batch: SacdDstFrameBatch, writer: DsfWriterState,
|
|
|
+ dstDecoderId: number | undefined): boolean {
|
|
|
+ if (batch.frameCount <= 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (!dstDecoderId || dstDecoderId <= 0) {
|
|
|
+ clearSacdDstFrameBatch(batch);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const payloadBuffer: ArrayBuffer = new ArrayBuffer(batch.totalPayloadBytes);
|
|
|
+ const payloadBytes: Uint8Array = new Uint8Array(payloadBuffer);
|
|
|
+ let payloadOffset: number = 0;
|
|
|
+ for (let index: number = 0; index < batch.frameChunks.length; index++) {
|
|
|
+ const frameChunk: Uint8Array = batch.frameChunks[index];
|
|
|
+ payloadBytes.set(frameChunk, payloadOffset);
|
|
|
+ payloadOffset += frameChunk.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ const frameLengthsBuffer: ArrayBuffer = new ArrayBuffer(batch.frameCount * 4);
|
|
|
+ const frameLengthsArray: Uint32Array = new Uint32Array(frameLengthsBuffer);
|
|
|
+ for (let index: number = 0; index < batch.frameLengths.length; index++) {
|
|
|
+ frameLengthsArray[index] = batch.frameLengths[index];
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const decodedBuffer: ArrayBuffer = sacdDstNativeBridge.decodeSacdDstFrames(dstDecoderId, payloadBuffer,
|
|
|
+ frameLengthsBuffer);
|
|
|
+ const decodedBytes: Uint8Array = new Uint8Array(decodedBuffer);
|
|
|
+ const frameOutputSize: number = writer.channelCount * SACD_FRAME_SIZE_64;
|
|
|
+ if (decodedBytes.length !== frameOutputSize * batch.frameCount) {
|
|
|
+ clearSacdDstFrameBatch(batch);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ let decodedOffset: number = 0;
|
|
|
+ for (let index: number = 0; index < batch.frameCount; index++) {
|
|
|
+ const frameSlice: Uint8Array = decodedBytes.subarray(decodedOffset, decodedOffset + frameOutputSize);
|
|
|
+ if (!writeSacdFrameToDsf(writer, frameSlice, writer.channelCount)) {
|
|
|
+ clearSacdDstFrameBatch(batch);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ decodedOffset += frameOutputSize;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ Logger.error('IsoArchiveTaskHelper', `heanup DST batch decode throw: ${(error as Error).message}`);
|
|
|
+ clearSacdDstFrameBatch(batch);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ clearSacdDstFrameBatch(batch);
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+function clearSacdDstFrameBatch(batch: SacdDstFrameBatch): void {
|
|
|
+ batch.totalPayloadBytes = 0;
|
|
|
+ batch.frameCount = 0;
|
|
|
+ batch.frameLengths = [];
|
|
|
+ batch.frameChunks = [];
|
|
|
+}
|
|
|
+
|
|
|
+function shouldWriteSacdFrame(frameFilter: SacdTrackFrameFilter, timecodeFrames: number): boolean {
|
|
|
+ if (!frameFilter.enabled || timecodeFrames < 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return timecodeFrames >= frameFilter.startFrame && timecodeFrames < frameFilter.endFrameExclusive;
|
|
|
+}
|
|
|
+
|
|
|
+function readSacdTimecodeFrames(buffer: Uint8Array, offset: number): number {
|
|
|
+ if (offset + 2 >= buffer.length) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ return buffer[offset] * 60 * SACD_FRAME_RATE + buffer[offset + 1] * SACD_FRAME_RATE + buffer[offset + 2];
|
|
|
+}
|
|
|
+
|
|
|
function resolveSacdFrameChannelCount(frameInfo: SacdFrameInfo | undefined, defaultChannelCount: number): number {
|
|
|
if (!frameInfo) {
|
|
|
return defaultChannelCount > 0 ? defaultChannelCount : 2;
|