|
@@ -0,0 +1,1278 @@
|
|
|
|
|
+import { taskpool } from '@kit.ArkTS';
|
|
|
|
|
+import fileIo from '@ohos.file.fs';
|
|
|
|
|
+
|
|
|
|
|
+const ISO_SECTOR_SIZE: number = 2048;
|
|
|
|
|
+const ISO_DESCRIPTOR_START_LSN: number = 16;
|
|
|
|
|
+const ISO_DESCRIPTOR_END_LSN: number = 64;
|
|
|
|
|
+const ISO_PRIMARY_VOLUME_DESCRIPTOR: number = 1;
|
|
|
|
|
+const ISO_SUPPLEMENTARY_VOLUME_DESCRIPTOR: number = 2;
|
|
|
|
|
+const ISO_TERMINATOR_DESCRIPTOR: number = 255;
|
|
|
|
|
+const ISO_STANDARD_IDENTIFIER: string = 'CD001';
|
|
|
|
|
+
|
|
|
|
|
+const SACD_START_OF_MASTER_TOC: number = 510;
|
|
|
|
|
+const SACD_MASTER_TOC_LEN: number = 10;
|
|
|
|
|
+const SACD_SAMPLING_FREQUENCY: number = 2822400;
|
|
|
|
|
+const SACD_BLOCK_SIZE_PER_CHANNEL: number = 4096;
|
|
|
|
|
+const SACD_BITS_PER_SAMPLE: number = 1;
|
|
|
|
|
+const SACD_FRAME_SIZE_64: number = 4704;
|
|
|
|
|
+const SACD_MAX_PACKET_COUNT: number = 7;
|
|
|
|
|
+const SACD_MAX_FRAME_BUFFER_SIZE: number = 64 * 1024;
|
|
|
|
|
+
|
|
|
|
|
+const SACD_PACKET_TYPE_AUDIO: number = 2;
|
|
|
|
|
+
|
|
|
|
|
+const DSF_HEADER_SIZE: number = 92;
|
|
|
|
|
+const DSF_CHANNEL_TYPE_MONO: number = 1;
|
|
|
|
|
+const DSF_CHANNEL_TYPE_STEREO: number = 2;
|
|
|
|
|
+const DSF_CHANNEL_TYPE_3_CHANNELS: number = 3;
|
|
|
|
|
+const DSF_CHANNEL_TYPE_QUAD: number = 4;
|
|
|
|
|
+const DSF_CHANNEL_TYPE_4_CHANNELS: number = 5;
|
|
|
|
|
+const DSF_CHANNEL_TYPE_5_CHANNELS: number = 6;
|
|
|
|
|
+const DSF_CHANNEL_TYPE_5_1_CHANNELS: number = 7;
|
|
|
|
|
+
|
|
|
|
|
+const SACD_FRAME_FORMAT_DST: number = 0;
|
|
|
|
|
+const BIT_REVERSE_TABLE: Uint8Array = createBitReverseTable();
|
|
|
|
|
+
|
|
|
|
|
+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'
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+interface IsoDirectoryRootDescriptor {
|
|
|
|
|
+ extent: number;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ isJoliet: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IsoDirectoryTaskItem {
|
|
|
|
|
+ extent: number;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ isJoliet: boolean;
|
|
|
|
|
+ relativePath: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IsoDirectoryRecord {
|
|
|
|
|
+ extent: number;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ isDirectory: boolean;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SacdMasterTocInfo {
|
|
|
|
|
+ area1Toc1Start: number;
|
|
|
|
|
+ area1TocSize: number;
|
|
|
|
|
+ area2Toc1Start: number;
|
|
|
|
|
+ area2TocSize: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SacdAreaTocInfo {
|
|
|
|
|
+ trackOffset: number;
|
|
|
|
|
+ trackCount: number;
|
|
|
|
|
+ channelCount?: number;
|
|
|
|
|
+ frameFormat?: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SacdAreaExtractInfo {
|
|
|
|
|
+ areaStartLsn: number;
|
|
|
|
|
+ areaTocSize: number;
|
|
|
|
|
+ channelCount: number;
|
|
|
|
|
+ frameFormat: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SacdPacketInfo {
|
|
|
|
|
+ frameStart: boolean;
|
|
|
|
|
+ dataType: number;
|
|
|
|
|
+ packetLength: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SacdFrameInfo {
|
|
|
|
|
+ sectorCount: number;
|
|
|
|
|
+ channelBit2?: boolean;
|
|
|
|
|
+ channelBit3?: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SacdFrameAssembler {
|
|
|
|
|
+ started: boolean;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ channelCount: number;
|
|
|
|
|
+ dstEncoded: boolean;
|
|
|
|
|
+ sectorCount: number;
|
|
|
|
|
+ buffer: Uint8Array;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface DsfWriterState {
|
|
|
|
|
+ file: fileIo.File;
|
|
|
|
|
+ channelCount: number;
|
|
|
|
|
+ channelType: number;
|
|
|
|
|
+ channelBuffers: Uint8Array[];
|
|
|
|
|
+ blockFill: number;
|
|
|
|
|
+ audioDataSize: number;
|
|
|
|
|
+ sampleBytes: number;
|
|
|
|
|
+ outputOffset: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IsoExtractTaskPayload {
|
|
|
|
|
+ isoPath: string;
|
|
|
|
|
+ destinationDir: string;
|
|
|
|
|
+ entries: IsoArchiveHelperAudioEntry[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface IsoArchiveHelperAudioEntry {
|
|
|
|
|
+ path: string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ extent: number;
|
|
|
|
|
+ entryType?: string;
|
|
|
|
|
+ trackNumber?: number;
|
|
|
|
|
+ startLsn?: number;
|
|
|
|
|
+ lengthLsn?: number;
|
|
|
|
|
+ startTimeFrames?: number;
|
|
|
|
|
+ durationFrames?: number;
|
|
|
|
|
+ areaType?: string;
|
|
|
|
|
+ channelCount?: number;
|
|
|
|
|
+ frameFormat?: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface IsoArchiveHelperExtractTaskResult {
|
|
|
|
|
+ extractedPaths: string[];
|
|
|
|
|
+ failedEntries: string[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IsoExtractProgressState {
|
|
|
|
|
+ totalUnits: number;
|
|
|
|
|
+ completedUnits: number;
|
|
|
|
|
+ lastProgress: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IsoExtractProgressMessage {
|
|
|
|
|
+ progress: number;
|
|
|
|
|
+ current: number;
|
|
|
|
|
+ total: number;
|
|
|
|
|
+ name?: string;
|
|
|
|
|
+ stage?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function listIsoAudioEntriesCore(isoPath: string): IsoArchiveHelperAudioEntry[] {
|
|
|
|
|
+ const standardEntries: IsoArchiveHelperAudioEntry[] = listStandardIsoAudioEntries(isoPath);
|
|
|
|
|
+ if (standardEntries.length > 0) {
|
|
|
|
|
+ return standardEntries;
|
|
|
|
|
+ }
|
|
|
|
|
+ return listSacdAudioEntries(isoPath);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function extractIsoAudioEntriesCore(payloadJson: string): IsoArchiveHelperExtractTaskResult {
|
|
|
|
|
+ const result: IsoArchiveHelperExtractTaskResult = {
|
|
|
|
|
+ extractedPaths: [],
|
|
|
|
|
+ failedEntries: []
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let payload: IsoExtractTaskPayload | null = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ payload = JSON.parse(payloadJson) as IsoExtractTaskPayload;
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!payload || !payload.isoPath || !payload.destinationDir || !Array.isArray(payload.entries)) {
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let isoFile: fileIo.File | undefined = undefined;
|
|
|
|
|
+ let progressState: IsoExtractProgressState | undefined = undefined;
|
|
|
|
|
+ try {
|
|
|
|
|
+ isoFile = fileIo.openSync(payload.isoPath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
+ ensureDirectoryExists(payload.destinationDir);
|
|
|
|
|
+ progressState = createIsoExtractProgressState(payload.entries);
|
|
|
|
|
+ reportIsoExtractProgress(progressState, '', 'prepare', true);
|
|
|
|
|
+
|
|
|
|
|
+ for (let index: number = 0; index < payload.entries.length; index++) {
|
|
|
|
|
+ const entry: IsoArchiveHelperAudioEntry = payload.entries[index];
|
|
|
|
|
+ if (!entry || !entry.path) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const outputPath: string = buildUniqueOutputPath(payload.destinationDir, entry.name || getLastPathSegment(entry.path));
|
|
|
|
|
+ let extracted: boolean = false;
|
|
|
|
|
+ if (entry.entryType === 'sacd_track') {
|
|
|
|
|
+ extracted = extractSacdTrackEntry(isoFile, entry, outputPath, progressState);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ extracted = extractStandardIsoEntry(isoFile, entry, outputPath, progressState);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (extracted) {
|
|
|
|
|
+ result.extractedPaths.push(outputPath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (pathExists(outputPath)) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ fileIo.unlinkSync(outputPath);
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ result.failedEntries.push(entry.path);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (progressState) {
|
|
|
|
|
+ progressState.completedUnits = progressState.totalUnits;
|
|
|
|
|
+ reportIsoExtractProgress(progressState, '', 'done', true);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ for (let index: number = 0; index < payload.entries.length; index++) {
|
|
|
|
|
+ const entry: IsoArchiveHelperAudioEntry = payload.entries[index];
|
|
|
|
|
+ if (entry && entry.path) {
|
|
|
|
|
+ result.failedEntries.push(entry.path);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (progressState) {
|
|
|
|
|
+ reportIsoExtractProgress(progressState, '', 'error', true);
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (isoFile) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ fileIo.closeSync(isoFile);
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createIsoExtractProgressState(entries: IsoArchiveHelperAudioEntry[]): IsoExtractProgressState {
|
|
|
|
|
+ let totalUnits: number = 0;
|
|
|
|
|
+ for (let index: number = 0; index < entries.length; index++) {
|
|
|
|
|
+ totalUnits += getIsoExtractEntryUnits(entries[index]);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (totalUnits <= 0) {
|
|
|
|
|
+ totalUnits = 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ totalUnits,
|
|
|
|
|
+ completedUnits: 0,
|
|
|
|
|
+ lastProgress: -1
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getIsoExtractEntryUnits(entry: IsoArchiveHelperAudioEntry): number {
|
|
|
|
|
+ if (!entry) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (entry.entryType === 'sacd_track' && entry.lengthLsn && entry.lengthLsn > 0) {
|
|
|
|
|
+ return entry.lengthLsn * ISO_SECTOR_SIZE;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (entry.size > 0) {
|
|
|
|
|
+ return entry.size;
|
|
|
|
|
+ }
|
|
|
|
|
+ return ISO_SECTOR_SIZE;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function advanceIsoExtractProgress(progressState: IsoExtractProgressState | undefined, deltaUnits: number,
|
|
|
|
|
+ entryName: string, stage: string): void {
|
|
|
|
|
+ if (!progressState || deltaUnits <= 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ progressState.completedUnits += deltaUnits;
|
|
|
|
|
+ if (progressState.completedUnits > progressState.totalUnits) {
|
|
|
|
|
+ progressState.completedUnits = progressState.totalUnits;
|
|
|
|
|
+ }
|
|
|
|
|
+ reportIsoExtractProgress(progressState, entryName, stage, false);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function reportIsoExtractProgress(progressState: IsoExtractProgressState | undefined, entryName: string,
|
|
|
|
|
+ stage: string, force: boolean): void {
|
|
|
|
|
+ if (!progressState) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ let progress: number = 0;
|
|
|
|
|
+ if (progressState.completedUnits <= 0) {
|
|
|
|
|
+ progress = 0;
|
|
|
|
|
+ } else if (progressState.completedUnits >= progressState.totalUnits) {
|
|
|
|
|
+ progress = 100;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ progress = Math.ceil((progressState.completedUnits / progressState.totalUnits) * 100);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!force && progress <= progressState.lastProgress) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ progressState.lastProgress = progress;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const progressMessage: IsoExtractProgressMessage = {
|
|
|
|
|
+ progress,
|
|
|
|
|
+ current: progressState.completedUnits,
|
|
|
|
|
+ total: progressState.totalUnits,
|
|
|
|
|
+ name: entryName,
|
|
|
|
|
+ stage
|
|
|
|
|
+ };
|
|
|
|
|
+ taskpool.Task.sendData(progressMessage);
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function listStandardIsoAudioEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
|
|
|
|
|
+ let isoFile: fileIo.File | undefined = undefined;
|
|
|
|
|
+ try {
|
|
|
|
|
+ isoFile = fileIo.openSync(isoPath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
+ const rootDescriptor: IsoDirectoryRootDescriptor | null = resolveIsoRootDescriptor(isoFile);
|
|
|
|
|
+ if (!rootDescriptor) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+ return collectIsoAudioEntries(isoFile, rootDescriptor);
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (isoFile) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ fileIo.closeSync(isoFile);
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resolveIsoRootDescriptor(isoFile: fileIo.File): IsoDirectoryRootDescriptor | null {
|
|
|
|
|
+ let primaryRoot: IsoDirectoryRootDescriptor | null = null;
|
|
|
|
|
+ let jolietRoot: IsoDirectoryRootDescriptor | null = null;
|
|
|
|
|
+
|
|
|
|
|
+ for (let lsn: number = ISO_DESCRIPTOR_START_LSN; lsn < ISO_DESCRIPTOR_END_LSN; lsn++) {
|
|
|
|
|
+ const descriptor: Uint8Array = readBytes(isoFile, lsn * ISO_SECTOR_SIZE, ISO_SECTOR_SIZE);
|
|
|
|
|
+ if (descriptor.length < ISO_SECTOR_SIZE) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (readAscii(descriptor, 1, 5) !== ISO_STANDARD_IDENTIFIER) {
|
|
|
|
|
+ if (descriptor[0] === ISO_TERMINATOR_DESCRIPTOR) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (descriptor[0] === ISO_PRIMARY_VOLUME_DESCRIPTOR) {
|
|
|
|
|
+ primaryRoot = parseIsoRootFromVolumeDescriptor(descriptor, false);
|
|
|
|
|
+ } else if (descriptor[0] === ISO_SUPPLEMENTARY_VOLUME_DESCRIPTOR && isJolietDescriptor(descriptor)) {
|
|
|
|
|
+ jolietRoot = parseIsoRootFromVolumeDescriptor(descriptor, true);
|
|
|
|
|
+ } else if (descriptor[0] === ISO_TERMINATOR_DESCRIPTOR) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (descriptor[0] === ISO_TERMINATOR_DESCRIPTOR) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (jolietRoot) {
|
|
|
|
|
+ return jolietRoot;
|
|
|
|
|
+ }
|
|
|
|
|
+ return primaryRoot;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function isJolietDescriptor(descriptor: Uint8Array): boolean {
|
|
|
|
|
+ if (descriptor.length < 91) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (descriptor[88] !== 0x25 || descriptor[89] !== 0x2f) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return descriptor[90] === 0x40 || descriptor[90] === 0x43 || descriptor[90] === 0x45;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseIsoRootFromVolumeDescriptor(descriptor: Uint8Array, isJoliet: boolean): IsoDirectoryRootDescriptor | null {
|
|
|
|
|
+ const recordLength: number = descriptor[156];
|
|
|
|
|
+ if (recordLength <= 0 || 156 + recordLength > descriptor.length) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const extent: number = readUInt32LE(descriptor, 158);
|
|
|
|
|
+ const size: number = readUInt32LE(descriptor, 166);
|
|
|
|
|
+ if (extent <= 0 || size <= 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ extent,
|
|
|
|
|
+ size,
|
|
|
|
|
+ isJoliet
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function collectIsoAudioEntries(isoFile: fileIo.File, rootDescriptor: IsoDirectoryRootDescriptor): IsoArchiveHelperAudioEntry[] {
|
|
|
|
|
+ const result: IsoArchiveHelperAudioEntry[] = [];
|
|
|
|
|
+ const visitedKeys: Set<string> = new Set<string>();
|
|
|
|
|
+ const stack: IsoDirectoryTaskItem[] = [{
|
|
|
|
|
+ extent: rootDescriptor.extent,
|
|
|
|
|
+ size: rootDescriptor.size,
|
|
|
|
|
+ isJoliet: rootDescriptor.isJoliet,
|
|
|
|
|
+ relativePath: ''
|
|
|
|
|
+ }];
|
|
|
|
|
+
|
|
|
|
|
+ while (stack.length > 0) {
|
|
|
|
|
+ const current: IsoDirectoryTaskItem = stack.pop() as IsoDirectoryTaskItem;
|
|
|
|
|
+ const visitKey: string = `${current.extent}_${current.size}_${current.relativePath}_${current.isJoliet ? '1' : '0'}`;
|
|
|
|
|
+ if (visitedKeys.has(visitKey)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ visitedKeys.add(visitKey);
|
|
|
|
|
+
|
|
|
|
|
+ const directoryData: Uint8Array = readBytes(isoFile, current.extent * ISO_SECTOR_SIZE, current.size);
|
|
|
|
|
+ let offset: number = 0;
|
|
|
|
|
+ while (offset < directoryData.length) {
|
|
|
|
|
+ const recordLength: number = directoryData[offset];
|
|
|
|
|
+ if (recordLength === 0) {
|
|
|
|
|
+ offset = moveToNextSector(offset);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (offset + recordLength > directoryData.length) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const record: IsoDirectoryRecord | null = parseIsoDirectoryRecord(directoryData, offset, current.isJoliet);
|
|
|
|
|
+ offset += recordLength;
|
|
|
|
|
+ if (!record || !record.name) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const relativePath: string = current.relativePath.length > 0 ? `${current.relativePath}/${record.name}` : record.name;
|
|
|
|
|
+ if (record.isDirectory) {
|
|
|
|
|
+ if (record.extent > 0 && record.size > 0) {
|
|
|
|
|
+ stack.push({
|
|
|
|
|
+ extent: record.extent,
|
|
|
|
|
+ size: record.size,
|
|
|
|
|
+ isJoliet: current.isJoliet,
|
|
|
|
|
+ relativePath
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!isAudioFileName(record.name)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.push({
|
|
|
|
|
+ path: relativePath,
|
|
|
|
|
+ name: record.name,
|
|
|
|
|
+ size: record.size,
|
|
|
|
|
+ extent: record.extent,
|
|
|
|
|
+ entryType: 'iso_file'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.sort((left: IsoArchiveHelperAudioEntry, right: IsoArchiveHelperAudioEntry) => left.path.localeCompare(right.path));
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function moveToNextSector(offset: number): number {
|
|
|
|
|
+ const sectorIndex: number = Math.floor(offset / ISO_SECTOR_SIZE);
|
|
|
|
|
+ return (sectorIndex + 1) * ISO_SECTOR_SIZE;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseIsoDirectoryRecord(buffer: Uint8Array, offset: number, isJoliet: boolean): IsoDirectoryRecord | null {
|
|
|
|
|
+ const recordLength: number = buffer[offset];
|
|
|
|
|
+ if (recordLength < 34) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nameLength: number = buffer[offset + 32];
|
|
|
|
|
+ const nameStart: number = offset + 33;
|
|
|
|
|
+ if (nameStart + nameLength > offset + recordLength) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (nameLength === 1) {
|
|
|
|
|
+ const specialName: number = buffer[nameStart];
|
|
|
|
|
+ if (specialName === 0 || specialName === 1) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nameBytes: Uint8Array = buffer.subarray(nameStart, nameStart + nameLength);
|
|
|
|
|
+ let decodedName: string = isJoliet ? decodeJolietString(nameBytes) : decodeAsciiString(nameBytes);
|
|
|
|
|
+ decodedName = sanitizeIsoEntryName(decodedName);
|
|
|
|
|
+ if (!decodedName) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ extent: readUInt32LE(buffer, offset + 2),
|
|
|
|
|
+ size: readUInt32LE(buffer, offset + 10),
|
|
|
|
|
+ isDirectory: (buffer[offset + 25] & 0x02) !== 0,
|
|
|
|
|
+ name: decodedName
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function sanitizeIsoEntryName(name: string): string {
|
|
|
|
|
+ let sanitized: string = name;
|
|
|
|
|
+ const versionIndex: number = sanitized.indexOf(';');
|
|
|
|
|
+ if (versionIndex >= 0) {
|
|
|
|
|
+ sanitized = sanitized.substring(0, versionIndex);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sanitized.endsWith('.')) {
|
|
|
|
|
+ sanitized = sanitized.substring(0, sanitized.length - 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return sanitized.trim();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function decodeAsciiString(bytes: Uint8Array): string {
|
|
|
|
|
+ let result: string = '';
|
|
|
|
|
+ for (let index: number = 0; index < bytes.length; index++) {
|
|
|
|
|
+ const value: number = bytes[index];
|
|
|
|
|
+ if (value === 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ result += String.fromCharCode(value);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function decodeJolietString(bytes: Uint8Array): string {
|
|
|
|
|
+ let result: string = '';
|
|
|
|
|
+ for (let index: number = 0; index + 1 < bytes.length; index += 2) {
|
|
|
|
|
+ const codePoint: number = (bytes[index] << 8) | bytes[index + 1];
|
|
|
|
|
+ if (codePoint === 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ result += String.fromCharCode(codePoint);
|
|
|
|
|
+ }
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+ return fileName.substring(lastDot);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function extractStandardIsoEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudioEntry, outputPath: string,
|
|
|
|
|
+ progressState?: IsoExtractProgressState): boolean {
|
|
|
|
|
+ if (entry.extent <= 0 || entry.size <= 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let targetFile: fileIo.File | undefined = undefined;
|
|
|
|
|
+ try {
|
|
|
|
|
+ targetFile = fileIo.openSync(outputPath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
|
|
|
|
|
+
|
|
|
|
|
+ let remaining: number = entry.size;
|
|
|
|
|
+ let readOffset: number = entry.extent * ISO_SECTOR_SIZE;
|
|
|
|
|
+ const chunkSize: number = 256 * 1024;
|
|
|
|
|
+
|
|
|
|
|
+ while (remaining > 0) {
|
|
|
|
|
+ const currentChunkSize: number = remaining > chunkSize ? chunkSize : remaining;
|
|
|
|
|
+ const buffer: ArrayBuffer = new ArrayBuffer(currentChunkSize);
|
|
|
|
|
+ const bytesRead: number = fileIo.readSync(isoFile.fd, buffer, { offset: readOffset, length: currentChunkSize });
|
|
|
|
|
+ if (bytesRead <= 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (bytesRead === currentChunkSize) {
|
|
|
|
|
+ fileIo.writeSync(targetFile.fd, buffer);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ fileIo.writeSync(targetFile.fd, buffer, { length: bytesRead });
|
|
|
|
|
+ }
|
|
|
|
|
+ remaining -= bytesRead;
|
|
|
|
|
+ readOffset += bytesRead;
|
|
|
|
|
+ advanceIsoExtractProgress(progressState, bytesRead, entry.name, 'extract');
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (targetFile) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ fileIo.closeSync(targetFile);
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readSectors(file: fileIo.File, startLsn: number, sectorCount: number): Uint8Array {
|
|
|
|
|
+ return readBytes(file, startLsn * ISO_SECTOR_SIZE, sectorCount * ISO_SECTOR_SIZE);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function listSacdAudioEntries(isoPath: string): IsoArchiveHelperAudioEntry[] {
|
|
|
|
|
+ let isoFile: fileIo.File | undefined = undefined;
|
|
|
|
|
+ try {
|
|
|
|
|
+ isoFile = fileIo.openSync(isoPath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
+ const masterToc: SacdMasterTocInfo | null = parseSacdMasterToc(isoFile);
|
|
|
|
|
+ if (!masterToc) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const stereoEntries: IsoArchiveHelperAudioEntry[] = parseSacdAreaEntries(
|
|
|
|
|
+ isoFile,
|
|
|
|
|
+ masterToc.area1Toc1Start,
|
|
|
|
|
+ masterToc.area1TocSize,
|
|
|
|
|
+ '2CH'
|
|
|
|
|
+ );
|
|
|
|
|
+ if (stereoEntries.length > 0) {
|
|
|
|
|
+ return stereoEntries;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return parseSacdAreaEntries(isoFile, masterToc.area2Toc1Start, masterToc.area2TocSize, 'MCH');
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (isoFile) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ fileIo.closeSync(isoFile);
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseSacdMasterToc(isoFile: fileIo.File): SacdMasterTocInfo | null {
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (readAscii(masterData, 0, 8) !== 'SACDMTOC') {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ area1Toc1Start: readUInt32BE(masterData, 64),
|
|
|
|
|
+ area1TocSize: readUInt16BE(masterData, 84),
|
|
|
|
|
+ area2Toc1Start: readUInt32BE(masterData, 72),
|
|
|
|
|
+ area2TocSize: readUInt16BE(masterData, 86)
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseSacdAreaEntries(isoFile: fileIo.File, areaStartLsn: number, areaTocSize: number,
|
|
|
|
|
+ areaType: string): IsoArchiveHelperAudioEntry[] {
|
|
|
|
|
+ if (areaStartLsn <= 0 || areaTocSize <= 0) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const areaData: Uint8Array = readBytes(isoFile, areaStartLsn * ISO_SECTOR_SIZE, areaTocSize * ISO_SECTOR_SIZE);
|
|
|
|
|
+ if (areaData.length < ISO_SECTOR_SIZE) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const areaId: string = readAscii(areaData, 0, 8);
|
|
|
|
|
+ if (areaId !== 'TWOCHTOC' && areaId !== 'MULCHTOC') {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const areaInfo: SacdAreaTocInfo = {
|
|
|
|
|
+ trackOffset: areaData[68],
|
|
|
|
|
+ trackCount: areaData[69],
|
|
|
|
|
+ channelCount: areaData[32],
|
|
|
|
|
+ frameFormat: areaData[21] & 0x0f
|
|
|
|
|
+ };
|
|
|
|
|
+ if (areaInfo.trackCount <= 0) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let trackListOffsetBlock: Uint8Array | null = null;
|
|
|
|
|
+ let trackListTimeBlock: Uint8Array | null = null;
|
|
|
|
|
+ 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;
|
|
|
|
|
+ const blockId: string = readAscii(areaData, offset, 8);
|
|
|
|
|
+ if (blockId === 'SACDTRL1') {
|
|
|
|
|
+ trackListOffsetBlock = areaData.subarray(offset, offset + ISO_SECTOR_SIZE);
|
|
|
|
|
+ } else if (blockId === 'SACDTRL2') {
|
|
|
|
|
+ trackListTimeBlock = areaData.subarray(offset, offset + ISO_SECTOR_SIZE);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!trackListOffsetBlock) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+ const offsetBlock: Uint8Array = trackListOffsetBlock;
|
|
|
|
|
+
|
|
|
|
|
+ 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 lengthLsn: number = readUInt32BE(offsetBlock, 8 + 255 * 4 + index * 4);
|
|
|
|
|
+ if (startLsn <= 0 || lengthLsn <= 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let startFrames: number = 0;
|
|
|
|
|
+ let durationFrames: number = 0;
|
|
|
|
|
+ if (trackListTimeBlock) {
|
|
|
|
|
+ startFrames = readSacdTrackTimeFrames(trackListTimeBlock, 8 + index * 4);
|
|
|
|
|
+ durationFrames = readSacdTrackTimeFrames(trackListTimeBlock, 8 + 255 * 4 + index * 4);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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`;
|
|
|
|
|
+
|
|
|
|
|
+ result.push({
|
|
|
|
|
+ path: trackLabel,
|
|
|
|
|
+ name: trackLabel,
|
|
|
|
|
+ size: lengthLsn * ISO_SECTOR_SIZE,
|
|
|
|
|
+ extent: startLsn,
|
|
|
|
|
+ entryType: 'sacd_track',
|
|
|
|
|
+ trackNumber,
|
|
|
|
|
+ startLsn,
|
|
|
|
|
+ lengthLsn,
|
|
|
|
|
+ startTimeFrames: startFrames,
|
|
|
|
|
+ durationFrames,
|
|
|
|
|
+ areaType,
|
|
|
|
|
+ channelCount: areaInfo.channelCount,
|
|
|
|
|
+ frameFormat: areaInfo.frameFormat
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readSacdTrackTimeFrames(trackListTimeBlock: Uint8Array, offset: number): number {
|
|
|
|
|
+ if (offset + 3 >= trackListTimeBlock.length) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ const minutes: number = trackListTimeBlock[offset];
|
|
|
|
|
+ const seconds: number = trackListTimeBlock[offset + 1];
|
|
|
|
|
+ const frames: number = trackListTimeBlock[offset + 2];
|
|
|
|
|
+ return minutes * 60 * 75 + seconds * 75 + frames;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function padTrackNumber(trackNumber: number): string {
|
|
|
|
|
+ if (trackNumber < 10) {
|
|
|
|
|
+ return `0${trackNumber}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return `${trackNumber}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildUniqueOutputPath(destinationDir: string, fileName: string): string {
|
|
|
|
|
+ const cleanName: string = sanitizeOutputFileName(fileName);
|
|
|
|
|
+ const extension: string = getFileExtension(cleanName);
|
|
|
|
|
+ const baseName: string = extension.length > 0 ? cleanName.substring(0, cleanName.length - extension.length) : cleanName;
|
|
|
|
|
+
|
|
|
|
|
+ let candidatePath: string = `${trimTrailingSeparator(destinationDir)}/${cleanName}`;
|
|
|
|
|
+ let suffix: number = 1;
|
|
|
|
|
+ while (pathExists(candidatePath)) {
|
|
|
|
|
+ suffix++;
|
|
|
|
|
+ candidatePath = `${trimTrailingSeparator(destinationDir)}/${baseName} (${suffix})${extension}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return candidatePath;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function sanitizeOutputFileName(fileName: string): string {
|
|
|
|
|
+ const lastSegment: string = getLastPathSegment(fileName);
|
|
|
|
|
+ let sanitized: string = '';
|
|
|
|
|
+ for (let index: number = 0; index < lastSegment.length; index++) {
|
|
|
|
|
+ const char: string = lastSegment[index];
|
|
|
|
|
+ if (char === '/' || char === '\\' || char === ':' || char === '*' || char === '?' || char === '"' ||
|
|
|
|
|
+ char === '<' || char === '>' || char === '|') {
|
|
|
|
|
+ sanitized += '_';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ sanitized += char;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sanitized.trim().length === 0) {
|
|
|
|
|
+ return 'iso_audio.bin';
|
|
|
|
|
+ }
|
|
|
|
|
+ return sanitized;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getLastPathSegment(path: string): string {
|
|
|
|
|
+ const normalizedPath: string = path.replace(/\\/g, '/');
|
|
|
|
|
+ const lastSeparator: number = normalizedPath.lastIndexOf('/');
|
|
|
|
|
+ if (lastSeparator < 0) {
|
|
|
|
|
+ return normalizedPath;
|
|
|
|
|
+ }
|
|
|
|
|
+ return normalizedPath.substring(lastSeparator + 1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function trimTrailingSeparator(path: string): string {
|
|
|
|
|
+ if (path.endsWith('/')) {
|
|
|
|
|
+ return path.substring(0, path.length - 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return path;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function ensureDirectoryExists(dirPath: string): void {
|
|
|
|
|
+ if (!dirPath || pathExists(dirPath)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const normalizedPath: string = trimTrailingSeparator(dirPath);
|
|
|
|
|
+ const lastSeparator: number = normalizedPath.lastIndexOf('/');
|
|
|
|
|
+ if (lastSeparator > 0) {
|
|
|
|
|
+ ensureDirectoryExists(normalizedPath.substring(0, lastSeparator));
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ fileIo.mkdirSync(normalizedPath);
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function pathExists(path: string): boolean {
|
|
|
|
|
+ try {
|
|
|
|
|
+ fileIo.accessSync(path);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function extractSacdTrackEntry(isoFile: fileIo.File, entry: IsoArchiveHelperAudioEntry, outputPath: string,
|
|
|
|
|
+ progressState?: IsoExtractProgressState): boolean {
|
|
|
|
|
+ if (!entry.startLsn || !entry.lengthLsn || !entry.areaType) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ reportIsoExtractProgress(progressState, entry.name, 'extract', true);
|
|
|
|
|
+
|
|
|
|
|
+ const areaInfo: SacdAreaExtractInfo | null = resolveSacdAreaExtractInfo(isoFile, entry.areaType);
|
|
|
|
|
+ if (!areaInfo) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const channelCount: number = entry.channelCount && entry.channelCount > 0 ? entry.channelCount : areaInfo.channelCount;
|
|
|
|
|
+ const frameFormat: number = entry.frameFormat !== undefined ? entry.frameFormat : areaInfo.frameFormat;
|
|
|
|
|
+ if (frameFormat === SACD_FRAME_FORMAT_DST) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let outputFile: fileIo.File | undefined = undefined;
|
|
|
|
|
+ try {
|
|
|
|
|
+ outputFile = fileIo.openSync(outputPath,
|
|
|
|
|
+ fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
|
|
|
|
|
+ const writer: DsfWriterState = createDsfWriter(outputFile, channelCount);
|
|
|
|
|
+
|
|
|
|
|
+ // 先写占位头,导出完成后再回填真实大小和样本数。
|
|
|
|
|
+ writeDsfHeader(writer);
|
|
|
|
|
+
|
|
|
|
|
+ const assembler: SacdFrameAssembler = {
|
|
|
|
|
+ started: false,
|
|
|
|
|
+ size: 0,
|
|
|
|
|
+ channelCount,
|
|
|
|
|
+ dstEncoded: false,
|
|
|
|
|
+ sectorCount: 0,
|
|
|
|
|
+ buffer: new Uint8Array(SACD_MAX_FRAME_BUFFER_SIZE)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const sectorsPerBatch: number = 128;
|
|
|
|
|
+ let sectorIndex: number = 0;
|
|
|
|
|
+ while (sectorIndex < entry.lengthLsn) {
|
|
|
|
|
+ const remainingSectors: number = entry.lengthLsn - sectorIndex;
|
|
|
|
|
+ const currentBatchSectors: number = remainingSectors > sectorsPerBatch ? sectorsPerBatch : remainingSectors;
|
|
|
|
|
+ const sectorBatch: Uint8Array = readSectors(isoFile, entry.startLsn + sectorIndex, currentBatchSectors);
|
|
|
|
|
+ if (sectorBatch.length < currentBatchSectors * ISO_SECTOR_SIZE) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (let batchIndex: number = 0; batchIndex < currentBatchSectors; batchIndex++) {
|
|
|
|
|
+ const start: number = batchIndex * ISO_SECTOR_SIZE;
|
|
|
|
|
+ const end: number = start + ISO_SECTOR_SIZE;
|
|
|
|
|
+ const sectorBuffer: Uint8Array = sectorBatch.subarray(start, end);
|
|
|
|
|
+ const lastSector: boolean = sectorIndex + batchIndex === entry.lengthLsn - 1;
|
|
|
|
|
+ const processed: boolean = processSacdSector(sectorBuffer, channelCount, assembler, writer, lastSector);
|
|
|
|
|
+ if (!processed) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ sectorIndex += currentBatchSectors;
|
|
|
|
|
+ advanceIsoExtractProgress(progressState, currentBatchSectors * ISO_SECTOR_SIZE, entry.name, 'extract');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ finalizeDsfWriter(writer);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (outputFile) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ fileIo.closeSync(outputFile);
|
|
|
|
|
+ } catch (_error) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resolveSacdAreaExtractInfo(isoFile: fileIo.File, areaType: string): SacdAreaExtractInfo | null {
|
|
|
|
|
+ const masterToc: SacdMasterTocInfo | null = parseSacdMasterToc(isoFile);
|
|
|
|
|
+ if (!masterToc) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let areaStartLsn: number = 0;
|
|
|
|
|
+ let areaTocSize: number = 0;
|
|
|
|
|
+ if (areaType === 'MCH') {
|
|
|
|
|
+ areaStartLsn = masterToc.area2Toc1Start;
|
|
|
|
|
+ areaTocSize = masterToc.area2TocSize;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ areaStartLsn = masterToc.area1Toc1Start;
|
|
|
|
|
+ areaTocSize = masterToc.area1TocSize;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (areaStartLsn <= 0 || areaTocSize <= 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const areaData: Uint8Array = readBytes(isoFile, areaStartLsn * ISO_SECTOR_SIZE, areaTocSize * ISO_SECTOR_SIZE);
|
|
|
|
|
+ if (areaData.length < ISO_SECTOR_SIZE) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ const areaId: string = readAscii(areaData, 0, 8);
|
|
|
|
|
+ if (areaId !== 'TWOCHTOC' && areaId !== 'MULCHTOC') {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const channelCount: number = areaData[32];
|
|
|
|
|
+ const frameFormat: number = areaData[21] & 0x0f;
|
|
|
|
|
+ if (channelCount <= 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ areaStartLsn,
|
|
|
|
|
+ areaTocSize,
|
|
|
|
|
+ channelCount,
|
|
|
|
|
+ frameFormat
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createDsfWriter(file: fileIo.File, channelCount: number): DsfWriterState {
|
|
|
|
|
+ const channelBuffers: Uint8Array[] = [];
|
|
|
|
|
+ for (let channelIndex: number = 0; channelIndex < channelCount; channelIndex++) {
|
|
|
|
|
+ channelBuffers.push(new Uint8Array(new ArrayBuffer(SACD_BLOCK_SIZE_PER_CHANNEL)));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ file,
|
|
|
|
|
+ channelCount,
|
|
|
|
|
+ channelType: mapDsfChannelType(channelCount),
|
|
|
|
|
+ channelBuffers,
|
|
|
|
|
+ blockFill: 0,
|
|
|
|
|
+ audioDataSize: 0,
|
|
|
|
|
+ sampleBytes: 0,
|
|
|
|
|
+ outputOffset: DSF_HEADER_SIZE
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function writeDsfHeader(writer: DsfWriterState): void {
|
|
|
|
|
+ const headerBuffer: ArrayBuffer = new ArrayBuffer(DSF_HEADER_SIZE);
|
|
|
|
|
+ const bytes: Uint8Array = new Uint8Array(headerBuffer);
|
|
|
|
|
+
|
|
|
|
|
+ writeAscii(bytes, 0, 'DSD ');
|
|
|
|
|
+ writeUInt64LE(bytes, 4, 28);
|
|
|
|
|
+ writeUInt64LE(bytes, 12, DSF_HEADER_SIZE + writer.audioDataSize);
|
|
|
|
|
+ writeUInt64LE(bytes, 20, 0);
|
|
|
|
|
+
|
|
|
|
|
+ writeAscii(bytes, 28, 'fmt ');
|
|
|
|
|
+ writeUInt64LE(bytes, 32, 52);
|
|
|
|
|
+ writeUInt32LE(bytes, 40, 1);
|
|
|
|
|
+ writeUInt32LE(bytes, 44, 0);
|
|
|
|
|
+ writeUInt32LE(bytes, 48, writer.channelType);
|
|
|
|
|
+ writeUInt32LE(bytes, 52, writer.channelCount);
|
|
|
|
|
+ writeUInt32LE(bytes, 56, SACD_SAMPLING_FREQUENCY);
|
|
|
|
|
+ writeUInt32LE(bytes, 60, SACD_BITS_PER_SAMPLE);
|
|
|
|
|
+ writeUInt64LE(bytes, 64, Math.floor(writer.sampleBytes / writer.channelCount) * 8);
|
|
|
|
|
+ writeUInt32LE(bytes, 72, SACD_BLOCK_SIZE_PER_CHANNEL);
|
|
|
|
|
+ writeUInt32LE(bytes, 76, 0);
|
|
|
|
|
+
|
|
|
|
|
+ writeAscii(bytes, 80, 'data');
|
|
|
|
|
+ writeUInt64LE(bytes, 84, 12 + writer.audioDataSize);
|
|
|
|
|
+
|
|
|
|
|
+ fileIo.writeSync(writer.file.fd, headerBuffer, { offset: 0, length: DSF_HEADER_SIZE });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function finalizeDsfWriter(writer: DsfWriterState): void {
|
|
|
|
|
+ if (writer.blockFill > 0) {
|
|
|
|
|
+ flushDsfBlocks(writer, writer.blockFill);
|
|
|
|
|
+ }
|
|
|
|
|
+ writeDsfHeader(writer);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function flushDsfBlocks(writer: DsfWriterState, actualBytesPerChannel: number): void {
|
|
|
|
|
+ for (let channelIndex: number = 0; channelIndex < writer.channelCount; channelIndex++) {
|
|
|
|
|
+ const channelBuffer: Uint8Array = writer.channelBuffers[channelIndex];
|
|
|
|
|
+ if (actualBytesPerChannel < SACD_BLOCK_SIZE_PER_CHANNEL) {
|
|
|
|
|
+ channelBuffer.fill(0, actualBytesPerChannel);
|
|
|
|
|
+ }
|
|
|
|
|
+ fileIo.writeSync(writer.file.fd, channelBuffer.buffer, {
|
|
|
|
|
+ offset: writer.outputOffset,
|
|
|
|
|
+ length: SACD_BLOCK_SIZE_PER_CHANNEL
|
|
|
|
|
+ });
|
|
|
|
|
+ writer.outputOffset += SACD_BLOCK_SIZE_PER_CHANNEL;
|
|
|
|
|
+ }
|
|
|
|
|
+ writer.sampleBytes += actualBytesPerChannel * writer.channelCount;
|
|
|
|
|
+ writer.audioDataSize += SACD_BLOCK_SIZE_PER_CHANNEL * writer.channelCount;
|
|
|
|
|
+ writer.blockFill = 0;
|
|
|
|
|
+ for (let channelIndex: number = 0; channelIndex < writer.channelCount; channelIndex++) {
|
|
|
|
|
+ writer.channelBuffers[channelIndex].fill(0);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function writeSacdFrameToDsf(writer: DsfWriterState, frameData: Uint8Array, channelCount: number): boolean {
|
|
|
|
|
+ if (channelCount !== writer.channelCount || channelCount <= 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ let offset: number = 0;
|
|
|
|
|
+ while (offset < frameData.length) {
|
|
|
|
|
+ if (writer.blockFill >= SACD_BLOCK_SIZE_PER_CHANNEL) {
|
|
|
|
|
+ flushDsfBlocks(writer, SACD_BLOCK_SIZE_PER_CHANNEL);
|
|
|
|
|
+ }
|
|
|
|
|
+ for (let channelIndex: number = 0; channelIndex < writer.channelCount; channelIndex++) {
|
|
|
|
|
+ if (offset >= frameData.length) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ writer.channelBuffers[channelIndex][writer.blockFill] = reverseBits(frameData[offset]);
|
|
|
|
|
+ offset++;
|
|
|
|
|
+ }
|
|
|
|
|
+ writer.blockFill++;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function processSacdSector(sectorBuffer: Uint8Array, areaChannelCount: number, assembler: SacdFrameAssembler,
|
|
|
|
|
+ writer: DsfWriterState, lastSector: boolean): boolean {
|
|
|
|
|
+ let offset: number = 0;
|
|
|
|
|
+ const sectorHeader: number = sectorBuffer[offset];
|
|
|
|
|
+ offset++;
|
|
|
|
|
+
|
|
|
|
|
+ const dstEncoded: boolean = (sectorHeader & 0x01) !== 0;
|
|
|
|
|
+ const frameInfoCount: number = (sectorHeader >> 2) & 0x07;
|
|
|
|
|
+ const packetInfoCount: number = (sectorHeader >> 5) & 0x07;
|
|
|
|
|
+ if (packetInfoCount > SACD_MAX_PACKET_COUNT || frameInfoCount > SACD_MAX_PACKET_COUNT) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const packets: SacdPacketInfo[] = [];
|
|
|
|
|
+ for (let packetIndex: number = 0; packetIndex < packetInfoCount; packetIndex++) {
|
|
|
|
|
+ if (offset + 1 >= sectorBuffer.length) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const byte0: number = sectorBuffer[offset];
|
|
|
|
|
+ const byte1: number = sectorBuffer[offset + 1];
|
|
|
|
|
+ packets.push({
|
|
|
|
|
+ frameStart: ((byte0 >> 7) & 0x01) === 1,
|
|
|
|
|
+ dataType: (byte0 >> 3) & 0x07,
|
|
|
|
|
+ packetLength: ((byte0 & 0x07) << 8) | byte1
|
|
|
|
|
+ });
|
|
|
|
|
+ offset += 2;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const frameInfos: SacdFrameInfo[] = [];
|
|
|
|
|
+ for (let frameIndex: number = 0; frameIndex < frameInfoCount; frameIndex++) {
|
|
|
|
|
+ if (offset + 2 >= sectorBuffer.length) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dstEncoded) {
|
|
|
|
|
+ if (offset + 3 >= sectorBuffer.length) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const extraByte: number = sectorBuffer[offset + 3];
|
|
|
|
|
+ frameInfos.push({
|
|
|
|
|
+ sectorCount: (extraByte >> 2) & 0x1f,
|
|
|
|
|
+ channelBit2: ((extraByte >> 1) & 0x01) === 1,
|
|
|
|
|
+ channelBit3: (extraByte & 0x01) === 1
|
|
|
|
|
+ });
|
|
|
|
|
+ offset += 4;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ frameInfos.push({
|
|
|
|
|
+ sectorCount: 0,
|
|
|
|
|
+ channelBit2: false,
|
|
|
|
|
+ channelBit3: false
|
|
|
|
|
+ });
|
|
|
|
|
+ offset += 3;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let frameInfoIndex: number = 0;
|
|
|
|
|
+ for (let packetIndex: number = 0; packetIndex < packets.length; packetIndex++) {
|
|
|
|
|
+ const packet: SacdPacketInfo = packets[packetIndex];
|
|
|
|
|
+ if (offset + packet.packetLength > sectorBuffer.length) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (packet.dataType === SACD_PACKET_TYPE_AUDIO) {
|
|
|
|
|
+ if (packet.frameStart) {
|
|
|
|
|
+ if (!finalizeSacdFrame(assembler, writer)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (assembler.started && assembler.size > 0) {
|
|
|
|
|
+ resetSacdFrameAssembler(assembler);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const frameInfo: SacdFrameInfo | undefined = frameInfoIndex < frameInfos.length ? frameInfos[frameInfoIndex] : undefined;
|
|
|
|
|
+ assembler.started = true;
|
|
|
|
|
+ assembler.size = 0;
|
|
|
|
|
+ assembler.dstEncoded = dstEncoded;
|
|
|
|
|
+ assembler.sectorCount = frameInfo ? frameInfo.sectorCount : 0;
|
|
|
|
|
+ assembler.channelCount = resolveSacdFrameChannelCount(frameInfo, areaChannelCount);
|
|
|
|
|
+ frameInfoIndex++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (assembler.started) {
|
|
|
|
|
+ if (assembler.dstEncoded) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!appendSacdFrameBytes(assembler, sectorBuffer.subarray(offset, offset + packet.packetLength))) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ offset += packet.packetLength;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (lastSector) {
|
|
|
|
|
+ const finalized: boolean = finalizeSacdFrame(assembler, writer);
|
|
|
|
|
+ resetSacdFrameAssembler(assembler);
|
|
|
|
|
+ return finalized;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function finalizeSacdFrame(assembler: SacdFrameAssembler, writer: DsfWriterState): boolean {
|
|
|
|
|
+ if (!isSacdFrameReady(assembler)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (assembler.dstEncoded || assembler.channelCount <= 0 || assembler.size % assembler.channelCount !== 0) {
|
|
|
|
|
+ resetSacdFrameAssembler(assembler);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const frameData: Uint8Array = assembler.buffer.subarray(0, assembler.size);
|
|
|
|
|
+ const written: boolean = writeSacdFrameToDsf(writer, frameData, assembler.channelCount);
|
|
|
|
|
+ resetSacdFrameAssembler(assembler);
|
|
|
|
|
+ return written;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function isSacdFrameReady(assembler: SacdFrameAssembler): boolean {
|
|
|
|
|
+ if (!assembler.started || assembler.size <= 0) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (assembler.dstEncoded) {
|
|
|
|
|
+ return assembler.sectorCount === 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ return assembler.size % SACD_FRAME_SIZE_64 === 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function appendSacdFrameBytes(assembler: SacdFrameAssembler, source: Uint8Array): boolean {
|
|
|
|
|
+ if (assembler.size + source.length > assembler.buffer.length) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ assembler.buffer.set(source, assembler.size);
|
|
|
|
|
+ assembler.size += source.length;
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resetSacdFrameAssembler(assembler: SacdFrameAssembler): void {
|
|
|
|
|
+ assembler.started = false;
|
|
|
|
|
+ assembler.size = 0;
|
|
|
|
|
+ assembler.dstEncoded = false;
|
|
|
|
|
+ assembler.sectorCount = 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resolveSacdFrameChannelCount(frameInfo: SacdFrameInfo | undefined, defaultChannelCount: number): number {
|
|
|
|
|
+ if (!frameInfo) {
|
|
|
|
|
+ return defaultChannelCount > 0 ? defaultChannelCount : 2;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (frameInfo.channelBit2 === true && frameInfo.channelBit3 !== true) {
|
|
|
|
|
+ return 6;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (frameInfo.channelBit2 !== true && frameInfo.channelBit3 === true) {
|
|
|
|
|
+ return 5;
|
|
|
|
|
+ }
|
|
|
|
|
+ return defaultChannelCount > 0 ? defaultChannelCount : 2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function mapDsfChannelType(channelCount: number): number {
|
|
|
|
|
+ switch (channelCount) {
|
|
|
|
|
+ case 1:
|
|
|
|
|
+ return DSF_CHANNEL_TYPE_MONO;
|
|
|
|
|
+ case 2:
|
|
|
|
|
+ return DSF_CHANNEL_TYPE_STEREO;
|
|
|
|
|
+ case 3:
|
|
|
|
|
+ return DSF_CHANNEL_TYPE_3_CHANNELS;
|
|
|
|
|
+ case 4:
|
|
|
|
|
+ return DSF_CHANNEL_TYPE_4_CHANNELS;
|
|
|
|
|
+ case 5:
|
|
|
|
|
+ return DSF_CHANNEL_TYPE_5_CHANNELS;
|
|
|
|
|
+ case 6:
|
|
|
|
|
+ return DSF_CHANNEL_TYPE_5_1_CHANNELS;
|
|
|
|
|
+ default:
|
|
|
|
|
+ return DSF_CHANNEL_TYPE_STEREO;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createBitReverseTable(): Uint8Array {
|
|
|
|
|
+ const table: Uint8Array = new Uint8Array(256);
|
|
|
|
|
+ for (let value: number = 0; value < 256; value++) {
|
|
|
|
|
+ let source: number = value;
|
|
|
|
|
+ let reversed: number = 0;
|
|
|
|
|
+ for (let bitIndex: number = 0; bitIndex < 8; bitIndex++) {
|
|
|
|
|
+ reversed = (reversed << 1) | (source & 0x01);
|
|
|
|
|
+ source >>= 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ table[value] = reversed;
|
|
|
|
|
+ }
|
|
|
|
|
+ return table;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function reverseBits(value: number): number {
|
|
|
|
|
+ return BIT_REVERSE_TABLE[value & 0xff];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readBytes(file: fileIo.File, offset: number, length: number): Uint8Array {
|
|
|
|
|
+ if (length <= 0) {
|
|
|
|
|
+ return new Uint8Array(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ const buffer: ArrayBuffer = new ArrayBuffer(length);
|
|
|
|
|
+ const bytesRead: number = fileIo.readSync(file.fd, buffer, { offset, length });
|
|
|
|
|
+ if (bytesRead <= 0) {
|
|
|
|
|
+ return new Uint8Array(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ return new Uint8Array(buffer, 0, bytesRead);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readAscii(buffer: Uint8Array, offset: number, length: number): string {
|
|
|
|
|
+ let result: string = '';
|
|
|
|
|
+ const maxLength: number = offset + length;
|
|
|
|
|
+ for (let index: number = offset; index < maxLength && index < buffer.length; index++) {
|
|
|
|
|
+ const value: number = buffer[index];
|
|
|
|
|
+ if (value === 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ result += String.fromCharCode(value);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function writeAscii(buffer: Uint8Array, offset: number, text: string): void {
|
|
|
|
|
+ for (let index: number = 0; index < text.length && offset + index < buffer.length; index++) {
|
|
|
|
|
+ buffer[offset + index] = text.charCodeAt(index);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readUInt16BE(buffer: Uint8Array, offset: number): number {
|
|
|
|
|
+ return (buffer[offset] << 8) | buffer[offset + 1];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readUInt32BE(buffer: Uint8Array, offset: number): number {
|
|
|
|
|
+ return buffer[offset] * 16777216 + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + buffer[offset + 3];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readUInt32LE(buffer: Uint8Array, offset: number): number {
|
|
|
|
|
+ return buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16) + buffer[offset + 3] * 16777216;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function writeUInt32LE(buffer: Uint8Array, offset: number, value: number): void {
|
|
|
|
|
+ buffer[offset] = value & 0xff;
|
|
|
|
|
+ buffer[offset + 1] = (value >> 8) & 0xff;
|
|
|
|
|
+ buffer[offset + 2] = (value >> 16) & 0xff;
|
|
|
|
|
+ buffer[offset + 3] = (value >> 24) & 0xff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function writeUInt64LE(buffer: Uint8Array, offset: number, value: number): void {
|
|
|
|
|
+ let current: number = value;
|
|
|
|
|
+ for (let index: number = 0; index < 8; index++) {
|
|
|
|
|
+ buffer[offset + index] = current & 0xff;
|
|
|
|
|
+ current = Math.floor(current / 256);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|