From 254c06c74a151ad503d184872cda413ace57d99d Mon Sep 17 00:00:00 2001 From: Stefano Brilli Date: Tue, 31 Aug 2021 19:19:54 +0200 Subject: [PATCH] Skip compression of atrac3 wave files --- src/redux/actions.ts | 14 +++---- src/services/audio-export.ts | 28 +++++++++++--- src/utils.ts | 75 ++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 13 deletions(-) diff --git a/src/redux/actions.ts b/src/redux/actions.ts index ae8e389..7e38a41 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -436,10 +436,9 @@ async function getTrackNameFromMediaTags(file: File, titleFormat: TitleFormatTyp } } -export function convertAndUpload(files: File[], format: UploadFormat, titleFormat: TitleFormatType) { +export function convertAndUpload(files: File[], requestedFormat: UploadFormat, titleFormat: TitleFormatType) { return async function(dispatch: AppDispatch, getState: () => RootState) { const { audioExportService, netmdService } = serviceRegistry; - const wireformat = WireformatDict[format]; await netmdService?.stop(); dispatch(batchActions([uploadDialogActions.setVisible(true), uploadDialogActions.setCancelUpload(false)])); @@ -484,7 +483,7 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma }; let conversionIterator = async function*(files: File[]) { - let converted: Promise<{ file: File; data: ArrayBuffer }>[] = []; + let converted: Promise<{ file: File; data: ArrayBuffer; format: Wireformat }>[] = []; let i = 0; function convertNext() { @@ -504,11 +503,12 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma converted.push( new Promise(async (resolve, reject) => { let data: ArrayBuffer; + let format: Wireformat; try { await audioExportService!.prepare(f); - data = await audioExportService!.export({ format }); + ({ data, format } = await audioExportService!.export({ requestedFormat })); convertNext(); - resolve({ file: f, data: data }); + resolve({ file: f, data: data, format: format }); } catch (err) { error = err; errorMessage = `${f.name}: Unsupported or unrecognized format`; @@ -539,7 +539,7 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma break; } - const { file, data } = item; + const { file, data, format } = item; let title = file.name; try { @@ -563,7 +563,7 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma updateTrack(); updateProgressCallback({ written: 0, encrypted: 0, total: 100 }); try { - await netmdService?.upload(halfWidthTitle, fullWidthTitle, data, wireformat, updateProgressCallback); + await netmdService?.upload(halfWidthTitle, fullWidthTitle, data, format, updateProgressCallback); } catch (err) { error = err; errorMessage = `${file.name}: Error uploading to device. There might not be enough space left.`; diff --git a/src/services/audio-export.ts b/src/services/audio-export.ts index b0bfced..ba0b00a 100644 --- a/src/services/audio-export.ts +++ b/src/services/audio-export.ts @@ -1,6 +1,8 @@ import { createWorker, setLogging } from '@ffmpeg/ffmpeg'; import { AtracdencProcess } from './atracdenc-worker'; -import { getPublicPathFor } from '../utils'; +import { getAtrac3Info, getPublicPathFor } from '../utils'; +import { Wireformat } from 'netmd-js'; +import { WireformatDict } from '../redux/actions'; const AtracdencWorker = require('worker-loader!./atracdenc-worker'); // eslint-disable-line import/no-webpack-loader-syntax interface LogPayload { @@ -10,7 +12,7 @@ interface LogPayload { export interface AudioExportService { init(): Promise; - export(params: { format: string }): Promise; + export(params: { requestedFormat: 'SP' | 'LP2' | 'LP4' }): Promise<{ data: ArrayBuffer; format: Wireformat }>; info(): Promise<{ format: string | null; input: string | null }>; prepare(file: File): Promise; } @@ -21,12 +23,14 @@ export class FFMpegAudioExportService implements AudioExportService { public loglines: { action: string; message: string }[] = []; public inFileName: string = ``; public outFileNameNoExt: string = ``; + public inFile?: File; async init() { setLogging(true); } async prepare(file: File) { + this.inFile = file; this.loglines = []; this.ffmpegProcess = createWorker({ logger: (payload: LogPayload) => { @@ -79,33 +83,45 @@ export class FFMpegAudioExportService implements AudioExportService { return { format, input }; } - async export({ format }: { format: string }) { + async export({ requestedFormat }: { requestedFormat: 'SP' | 'LP2' | 'LP4' | 'LP105' }) { let result: ArrayBuffer; - if (format === `SP`) { + let format: Wireformat; + const atrac3Info = await getAtrac3Info(this.inFile!); + if (atrac3Info) { + format = WireformatDict[atrac3Info.mode]; + result = (await this.inFile!.arrayBuffer()).slice(atrac3Info.dataOffset); + } else if (requestedFormat === `SP`) { const outFileName = `${this.outFileNameNoExt}.raw`; await this.ffmpegProcess.transcode(this.inFileName, outFileName, '-ac 2 -ar 44100 -f s16be'); let { data } = await this.ffmpegProcess.read(outFileName); result = data.buffer; + format = Wireformat.pcm; } else { const outFileName = `${this.outFileNameNoExt}.wav`; await this.ffmpegProcess.transcode(this.inFileName, outFileName, '-f wav -ar 44100 -ac 2'); let { data } = await this.ffmpegProcess.read(outFileName); let bitrate: string = `0`; - switch (format) { + switch (requestedFormat) { case `LP2`: bitrate = `128`; + format = Wireformat.lp2; break; case `LP105`: bitrate = `102`; + format = Wireformat.l105kbps; break; case `LP4`: bitrate = `64`; + format = Wireformat.lp4; break; } result = await this.atracdencProcess!.encode(data.buffer, bitrate); } this.ffmpegProcess.worker.terminate(); this.atracdencProcess!.terminate(); - return result; + return { + data: result, + format, + }; } } diff --git a/src/utils.ts b/src/utils.ts index d858944..54a764a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -391,4 +391,79 @@ export function askNotificationPermission(): Promise { } } +export async function getAtrac3Info(file: File) { + // see: http://soundfile.sapp.org/doc/WaveFormat/ + // and: https://www.fatalerrors.org/a/detailed-explanation-of-wav-file-format.html + + const fileData = await file.arrayBuffer(); + if (fileData.byteLength < 44) { + return null; + } + + const riffDescriptor = new Uint32Array(fileData.slice(0, 12)); + if (riffDescriptor[0] !== 0x46464952 || riffDescriptor[2] !== 0x45564157) { + // 'RIFF' && 'WAVE' + return null; + } + + // WAVE format + const waveDescriptor = new Uint32Array(fileData.slice(12, 20)); + if (waveDescriptor[0] !== 0x20746d66) { + return false; + } + + const audioFormatAndChanneld = new Uint16Array(fileData.slice(20, 24)); + if (audioFormatAndChanneld[0] !== 0x270 || audioFormatAndChanneld[1] !== 2) { + // 'atrac3' && 2 channels + return null; + } + + const sampleRateAndByteRate = new Uint32Array(fileData.slice(24, 32)); + if (sampleRateAndByteRate[0] !== 44100) { + // Sample rate + return null; + } + const byteRate = sampleRateAndByteRate[1]; + + let mode: 'LP2' | 'LP105' | 'LP4' | null = null; + if (byteRate > 16000) { + mode = 'LP2'; + } else if (byteRate > 13000) { + mode = 'LP105'; + } else if (byteRate > 8000) { + mode = 'LP4'; + } else { + mode = null; + } + + if (mode === null) { + return null; + } + + const waveBlockEndOffset = new Uint16Array(fileData.slice(36, 38)); + + let dataOffset = -1; + + const nextBlockStartOffset = waveBlockEndOffset[0] + 38; + const nextBlockEndOffset = nextBlockStartOffset + 8; + const nextBlock = new Uint32Array(fileData.slice(nextBlockStartOffset, nextBlockEndOffset)); + if (nextBlock[0] === 0x61746164) { + // data + dataOffset = nextBlockEndOffset; + } else if (nextBlock[0] === 0x74636166) { + // fact + const dataBlockLength = 8; + dataOffset = nextBlockEndOffset + nextBlock[1] + dataBlockLength; + } + + if (dataOffset === -1) { + return null; + } + + return { + mode, + dataOffset, + }; +} + declare let process: any;