Skip to content

Commit fcea281

Browse files
committed
Refactor logging, loop metadata, and conversion flow
1 parent 0fb52d1 commit fcea281

19 files changed

+792
-597
lines changed

README.md

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,15 @@
1616
</p>
1717

1818
<!-- Pre-Headline -->
19-
<p style="font-size: 16px; color: yellow; text-align: center;">Boost your productivity and streamline your workflow today!</p>
19+
<p style="font-size: 16px; color: yellow; text-align: center;">Easiest, fastest and most reliable way to convert audio files available</p>
2020

21-
## Why I built Ez Game Audio Converter
2221

23-
I started this project after wasting many hours finding, organizing and converting assets for my game project. When you are using mostly free assets from different sources with different formats, bitrates and naming conventions, it can be a real time sink. If you spend all day learning how audio files work, you just spent all day not working on your project. You shouldn't have to be a audio engineer or terminal wizard just to have audio files in the right format/codec/bitrate.
24-
25-
The Problem: Game devs and anyone else who needs to convert audio files for whatever reason, need to know a lot about digital audio before they can even get started. This is a huge barrier to entry for new game devs and a time sink for experienced ones.
26-
27-
The Solution: A simple, easy to use tool that does all the heavy lifting for you. You don't need to know anything about audio files to use this tool. Just point it at your files and let it do the work. It will automatically select the best codec and bitrate for you. It will even handle loop tags for you. It's the easiest, fastest and most reliable solution available. And it's FREE!
28-
29-
Please leave feedback on Itch.io or Github.
3022

3123
<!--
3224
## Introduction
3325
3426
EZ-Game-Audio-Converter streamlines the process of batch audio conversion. Tailored specifically for game developers, this tool ensures great audio quality and small file sizes without the need for extensive knowledge. With almost no setup and multi-threaded conversion, it's the easiest, fastest and most reliable solution available. Plus, now with support for loop tags! -->
3527

36-
<!-- ## Not just for game devs anymore.
37-
38-
Now with FULL support for Apple iTunes metadata even when converting to and from non-M4A formats. This means you can convert your iTunes library to OGG or FLAC and keep all your metadata. This includes loop tags. Not many tools that do it all, also do this.
39-
40-
A big feature request has been to add CD ripping support. That this feature is now available! You can now rip your CDs to any format you like. Just select the CD drive as your source and the destination folder as your output. You can even rip multiple CDs at once. This feature is still in beta so please report any issues you find. -->
41-
4228
## Features
4329

4430
- 💻 **User-Friendly Interface:** Designed with simplicity as the main goal, eliminating any learning curve.
@@ -51,7 +37,7 @@ EZ-Game-Audio-Converter streamlines the process of batch audio conversion. Tailo
5137
- 🤖 **Intelligent File Handling:** Automatically resolves duplicate file names with different file extensions. Selects the best input file format.
5238
- 📝 **Meta Data Support:** All meta data, including iTunes data and Apple music will be transferred to the new file. Will transfer all basic meta data to and from all formats that support it.
5339
- 🔁 **Loop Tag Support:** All loop meta data will be transferred to new OGG or FLAC files. When changing sample rate, loop timings will be adjusted automatically. Cannot write loop tags TO M4A, only FROM.
54-
- 🎼 **Opus AND Vorbis Support for Ogg:** Use Opus when you can and Vorbis when you have to.
40+
- 🎼 **Opus AND Vorbis Support for Ogg:** Use Opus when you can and Vorbis for compatibility.
5541

5642
## Installation
5743

src/__tests__/convertFiles.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,7 @@ describe('convertFiles', () => {
253253
console.log("Test 'should handle worker errors' completed");
254254

255255
expect(console.error).toHaveBeenCalledWith(
256-
expect.stringContaining('Worker had an error'),
257-
expect.any(String),
258-
expect.any(String)
256+
expect.stringContaining('Worker had an error')
259257
);
260258
}, 30000);
261259

@@ -405,8 +403,7 @@ describe('convertFiles', () => {
405403
);
406404

407405
expect(console.error).toHaveBeenCalledWith(
408-
expect.stringContaining('Error creating worker'),
409-
expect.any(Object)
406+
expect.stringContaining('Error creating worker')
410407
);
411408
}, 30000);
412409

src/__tests__/converterWorker.test.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe('converterWorker.js', () => {
9999
loopStart: null,
100100
loopLength: null,
101101
});
102-
metadataService.formatLoopData.mockReturnValue('');
102+
metadataService.formatLoopData.mockReturnValue([]);
103103
});
104104

105105
afterEach(() => {
@@ -321,9 +321,16 @@ describe('converterWorker.js', () => {
321321
loopStart: 123,
322322
loopLength: 456,
323323
});
324-
metadataService.formatLoopData.mockReturnValue(
325-
' -metadata LOOPSTART=123 -metadata loopstart=123 -metadata LOOPLENGTH=456 -metadata looplength=456'
326-
);
324+
metadataService.formatLoopData.mockReturnValue([
325+
'-metadata',
326+
'LOOPSTART=123',
327+
'-metadata',
328+
'loopstart=123',
329+
'-metadata',
330+
'LOOPLENGTH=456',
331+
'-metadata',
332+
'looplength=456',
333+
]);
327334

328335
await converterWorker({
329336
file: {

src/__tests__/converterWorker.test.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ describe('converterWorker.js', () => {
142142
loopStart: NaN,
143143
loopLength: NaN,
144144
});
145-
formatLoopDataMock.mockReturnValue('');
145+
formatLoopDataMock.mockReturnValue([]);
146146
});
147147

148148
afterEach(() => {
@@ -288,6 +288,25 @@ describe('converterWorker.js', () => {
288288
});
289289
}, 10000);
290290

291+
it('does not re-apply metadata args when preserving metadata for AIFF', async () => {
292+
fs.existsSync.mockReturnValue(true);
293+
294+
await converterWorker({
295+
file: {
296+
inputFile: 'in.mp3',
297+
outputFile: 'out.aiff',
298+
outputFormat: 'aiff',
299+
},
300+
settings: { oggCodec: 'vorbis' },
301+
});
302+
303+
const args = spawnMock.mock.calls[0]?.[1] as string[];
304+
305+
expect(args).toContain('-write_id3v2');
306+
expect(args).toContain('1');
307+
expect(args).not.toContain('title=Test');
308+
}, 10000);
309+
291310
it('handles directory creation error gracefully', async () => {
292311
process.env.NODE_ENV = 'test';
293312

@@ -485,16 +504,17 @@ describe('converterWorker.js', () => {
485504
consoleLogSpy.mockRestore();
486505
}, 10000);
487506

488-
it('blocks outputs marked as "Skipped!"', async () => {
507+
it('blocks output files with embedded quotes', async () => {
489508
fs.existsSync.mockReturnValue(true);
490509
// Mutate live workerData binding
491510
workerThreads.workerData.file = {
492511
inputFile: 'in.wav',
493-
outputFile: 'out Skipped!.mp3',
512+
outputFile: 'out "Skipped!".mp3',
494513
outputFormat: 'mp3',
495514
};
515+
workerThreads.workerData.settings = { oggCodec: 'vorbis' };
496516

497-
await expect(runConversion()).rejects.toThrow(/Skipped!/);
517+
await expect(runConversion()).rejects.toThrow(/quotes/i);
498518
}, 10000);
499519

500520
describe('self-overwrite defense in depth', () => {

src/__tests__/getUserInput.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
2+
import { resolve } from 'path';
23

34
// ESM mocks must be declared BEFORE dynamic imports
45
jest.unstable_mockModule('fs', () => ({
@@ -52,8 +53,8 @@ describe('getUserInput', () => {
5253

5354
const result = await getUserInput(settings);
5455

55-
expect(result.inputFilePath).toBe('/input/path');
56-
expect(result.outputFilePath).toBe('/output/path');
56+
expect(result.inputFilePath).toBe(resolve('/input/path'));
57+
expect(result.outputFilePath).toBe(resolve('/output/path'));
5758
expect(result.inputFormats).toEqual(['mp3', 'wav']);
5859
expect(result.outputFormats).toEqual(['ogg']);
5960
});

src/__tests__/metadataFormatting.integration.test.ts

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ describe('formatMetaDataArgs - Real Output Tests', () => {
166166

167167
const result = formatMetaDataArgs(metadata);
168168

169-
// Backslashes should be escaped
170-
expect(result.metaDataArgs).toContain('title=Path\\\\To\\\\File');
169+
// With spawn args array (shell: false), backslashes are preserved as-is
170+
expect(result.metaDataArgs).toContain('title=Path\\To\\File');
171171
});
172172

173173
it('should escape double quotes', () => {
@@ -195,7 +195,8 @@ describe('formatMetaDataArgs - Real Output Tests', () => {
195195

196196
const result = formatMetaDataArgs(metadata);
197197

198-
expect(result.metaDataArgs).toContain('title=Song \\"With\\" Quotes');
198+
// With spawn args array (shell: false), quotes are preserved as-is
199+
expect(result.metaDataArgs).toContain('title=Song "With" Quotes');
199200
});
200201

201202
it('should convert Windows newlines to escaped \\n', () => {
@@ -223,7 +224,8 @@ describe('formatMetaDataArgs - Real Output Tests', () => {
223224

224225
const result = formatMetaDataArgs(metadata);
225226

226-
expect(result.metaDataArgs).toContain('comment=Line 1\\nLine 2\\nLine 3');
227+
// With spawn args array (shell: false), newlines are normalized to \n and preserved
228+
expect(result.metaDataArgs).toContain('comment=Line 1\nLine 2\nLine 3');
227229
});
228230

229231
it('should convert Unix newlines to escaped \\n', () => {
@@ -251,9 +253,8 @@ describe('formatMetaDataArgs - Real Output Tests', () => {
251253

252254
const result = formatMetaDataArgs(metadata);
253255

254-
expect(result.metaDataArgs).toContain(
255-
'lyrics=Verse 1\\nChorus\\nVerse 2'
256-
);
256+
// With spawn args array (shell: false), newlines are preserved as-is
257+
expect(result.metaDataArgs).toContain('lyrics=Verse 1\nChorus\nVerse 2');
257258
});
258259

259260
it('should convert old Mac newlines (CR only) to escaped \\n', () => {
@@ -281,7 +282,8 @@ describe('formatMetaDataArgs - Real Output Tests', () => {
281282

282283
const result = formatMetaDataArgs(metadata);
283284

284-
expect(result.metaDataArgs).toContain('comment=Old\\nMac\\nStyle');
285+
// With spawn args array (shell: false), CR is normalized to LF
286+
expect(result.metaDataArgs).toContain('comment=Old\nMac\nStyle');
285287
});
286288

287289
it('should remove null bytes', () => {
@@ -337,9 +339,9 @@ describe('formatMetaDataArgs - Real Output Tests', () => {
337339

338340
const result = formatMetaDataArgs(metadata);
339341

340-
// Should escape backslashes, quotes, and newlines
342+
// With spawn args array (shell: false), chars are preserved; newlines normalized
341343
expect(result.metaDataArgs).toContain(
342-
'comment=Path: C:\\\\Music\\\\\\"Best\\" Songs\\nLine 2'
344+
'comment=Path: C:\\Music\\"Best" Songs\nLine 2'
343345
);
344346
});
345347
});
@@ -1021,53 +1023,103 @@ describe('convertLoopPoints - Real Output Tests', () => {
10211023
});
10221024

10231025
describe('formatLoopData - Real Output Tests', () => {
1024-
it('should format loop data for ffmpeg', () => {
1026+
it('should format loop data for ffmpeg as args array', () => {
10251027
const result = formatLoopData(12345, 67890);
10261028

1027-
expect(result).toContain('-metadata LOOPSTART=12345');
1028-
expect(result).toContain('-metadata LOOPLENGTH=67890');
1029-
expect(result).toContain('-metadata loopstart=12345');
1030-
expect(result).toContain('-metadata looplength=67890');
1029+
expect(result).toEqual([
1030+
'-metadata',
1031+
'LOOPSTART=12345',
1032+
'-metadata',
1033+
'LOOPLENGTH=67890',
1034+
'-metadata',
1035+
'loopstart=12345',
1036+
'-metadata',
1037+
'looplength=67890',
1038+
]);
10311039
});
10321040

1033-
it('should return empty string for NaN loop start', () => {
1041+
it('should return empty array for NaN loop start', () => {
10341042
const result = formatLoopData(NaN, 67890);
10351043

1036-
expect(result).toBe('');
1044+
expect(result).toEqual([]);
10371045
});
10381046

1039-
it('should return empty string for NaN loop length', () => {
1047+
it('should return empty array for NaN loop length', () => {
10401048
const result = formatLoopData(12345, NaN);
10411049

1042-
expect(result).toBe('');
1050+
expect(result).toEqual([]);
10431051
});
10441052

1045-
it('should return empty string for both NaN', () => {
1053+
it('should return empty array for both NaN', () => {
10461054
const result = formatLoopData(NaN, NaN);
10471055

1048-
expect(result).toBe('');
1056+
expect(result).toEqual([]);
10491057
});
10501058

10511059
it('should handle zero values (valid loop points)', () => {
10521060
const result = formatLoopData(0, 100000);
10531061

1054-
expect(result).toContain('-metadata LOOPSTART=0');
1055-
expect(result).toContain('-metadata LOOPLENGTH=100000');
1062+
expect(result).toEqual([
1063+
'-metadata',
1064+
'LOOPSTART=0',
1065+
'-metadata',
1066+
'LOOPLENGTH=100000',
1067+
'-metadata',
1068+
'loopstart=0',
1069+
'-metadata',
1070+
'looplength=100000',
1071+
]);
10561072
});
10571073

1058-
it('should produce valid ffmpeg argument string', () => {
1074+
it('should produce valid ffmpeg argument array', () => {
10591075
const result = formatLoopData(48000, 96000);
10601076

1061-
// Should be usable directly in ffmpeg command
1062-
// Format: " -metadata LOOPSTART=X -metadata LOOPLENGTH=Y -metadata loopstart=X -metadata looplength=Y"
1063-
expect(result).toMatch(
1064-
/^\s*-metadata LOOPSTART=\d+ -metadata LOOPLENGTH=\d+ -metadata loopstart=\d+ -metadata looplength=\d+$/
1077+
// Should be usable directly spread into ffmpeg args
1078+
expect(result).toEqual(
1079+
expect.arrayContaining(['-metadata', 'LOOPSTART=48000'])
10651080
);
1081+
expect(result).toEqual(
1082+
expect.arrayContaining(['-metadata', 'LOOPLENGTH=96000'])
1083+
);
1084+
expect(result.length).toBe(8);
10661085
});
10671086
});
10681087

10691088
// Additional preservation-focused scenarios requested post-review
10701089
describe('formatMetaDataArgs - Preservation scenarios', () => {
1090+
it('should prefer non-replacement-character performer value when duplicates conflict', () => {
1091+
const metadata: AudioMetadata = {
1092+
streams: [
1093+
{
1094+
index: 0,
1095+
codec_name: 'aiff',
1096+
codec_type: 'audio',
1097+
channels: 2,
1098+
sample_rate: '44100',
1099+
tags: {
1100+
Performer: '�B',
1101+
},
1102+
},
1103+
],
1104+
format: {
1105+
filename: 'performer.aiff',
1106+
format_name: 'aiff',
1107+
duration: '180',
1108+
size: '1234',
1109+
bit_rate: '256000',
1110+
tags: {
1111+
PERFORMER: 'µB',
1112+
},
1113+
},
1114+
};
1115+
1116+
const { metaDataArgs } = formatMetaDataArgs(metadata);
1117+
const joined = metaDataArgs.join(' ');
1118+
1119+
expect(joined).toContain('performer=µB');
1120+
expect(joined).not.toContain('performer=�B');
1121+
});
1122+
10711123
it('should round-trip extra/unknown tags unchanged', () => {
10721124
const metadata: AudioMetadata = {
10731125
streams: [

0 commit comments

Comments
 (0)