From 9ec75bd24fcb43541728bece3b0c72ebc071421d Mon Sep 17 00:00:00 2001 From: William Toohey Date: Thu, 23 Mar 2017 22:36:32 +1000 Subject: [PATCH] Bout time I made a repo for this --- .gitignore | 5 ++ 2dx.h | 19 ++++++ 2dxBuild.c | 88 +++++++++++++++++++++++++ 2dxDump.c | 55 ++++++++++++++++ 2dxMerge.c | 84 ++++++++++++++++++++++++ 2dxWav.c | 170 ++++++++++++++++++++++++++++++++++++++++++++++++ 2dxWav.h | 9 +++ 2dxWavConvert.c | 20 ++++++ Makefile | 26 ++++++++ README.md | 44 +++++++++++++ shared.c | 35 ++++++++++ shared.h | 10 +++ 12 files changed, 565 insertions(+) create mode 100644 .gitignore create mode 100644 2dx.h create mode 100644 2dxBuild.c create mode 100644 2dxDump.c create mode 100644 2dxMerge.c create mode 100644 2dxWav.c create mode 100644 2dxWav.h create mode 100644 2dxWavConvert.c create mode 100644 Makefile create mode 100644 README.md create mode 100644 shared.c create mode 100644 shared.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5d8ba3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +sox/ +*.dll +*.wav +*.exe +*.o diff --git a/2dx.h b/2dx.h new file mode 100644 index 0000000..9ebac4a --- /dev/null +++ b/2dx.h @@ -0,0 +1,19 @@ +#include + +typedef struct { + char name[16]; + uint32_t headerSize; // this + offsets table + uint32_t fileCount; + char unknown[48]; // contains rest of title, some random flags +} fileHeader_t; + +typedef struct{ + char dx[4]; // should be "2DX9"; + uint32_t headerSize; // always 24, includes dx chars + uint32_t wavSize; + int16_t unk1; // always 0x3231 + int16_t trackId; // always -1 for previews, 0-7 for song + effected versions, 9 to 11 used for a few effects + int16_t unk2; // all 64, except song selection change 'click' is 40 + int16_t attenuation; // 0-127 for varying quietness + int32_t loopPoint; // sample to loop at * 4 +} dxHeader_t; \ No newline at end of file diff --git a/2dxBuild.c b/2dxBuild.c new file mode 100644 index 0000000..c3bdbf1 --- /dev/null +++ b/2dxBuild.c @@ -0,0 +1,88 @@ +#include +#include +#include + +#include "2dx.h" +#include "shared.h" + +int count_wav(void) { + int ret = 0; + char filename[256]; + while(snprintf(filename, 256, "%d.wav", ret), file_exists(filename)) + ret++; + return ret; +} + +void build_2dx(char* path) { + int wavCount = count_wav(); + printf("Writing %d wavs to %s\n",wavCount, path); + + FILE* outFile = fopen(path, "wb"); + + if(!outFile) { + printf("Could not open %s\n", path); + return; + } + + fileHeader_t fileHeader; + dxHeader_t dxHeader; + char wavPath[256]; + + // +1 to ignore null terminator, clobbers headerSize but we write that next + strncpy(fileHeader.name, path, sizeof(fileHeader.name)+1); + fileHeader.headerSize = sizeof(fileHeader) + sizeof(uint32_t)*wavCount; + fileHeader.fileCount = wavCount; + memset(fileHeader.unknown, 0, sizeof(fileHeader.unknown)); + fwrite(&fileHeader, sizeof(fileHeader), 1, outFile); + + // file offsets + uint32_t offset = fileHeader.headerSize; + for(int i = 0; i < wavCount; i++) { + snprintf(wavPath, 256, "%d.wav", i); + FILE* wav = fopen(wavPath, "rb"); + + fseek(wav, 0L, SEEK_END); + size_t wavSize = ftell(wav); + rewind(wav); + + fwrite(&offset, sizeof(uint32_t), 1, outFile); + offset += wavSize + sizeof(dxHeader_t); + fclose(wav); + } + // the actual wavs + for(int i = 0; i < wavCount; i++) { + snprintf(wavPath, 256, "%d.wav", i); + FILE* wav = fopen(wavPath, "rb"); + + fseek(wav, 0L, SEEK_END); + size_t wavSize = ftell(wav); + rewind(wav); + + memcpy(dxHeader.dx, "2DX9", 4); + dxHeader.headerSize = 24; + dxHeader.wavSize = wavSize; + dxHeader.unk1 = 0x3231; + // these match preview files for convenience + dxHeader.trackId = -1; // I may regret this + dxHeader.unk2 = 64; + dxHeader.attenuation = 1; + dxHeader.loopPoint = 0; + fwrite(&dxHeader, sizeof(dxHeader), 1, outFile); + transfer_file(wav, outFile, wavSize); + + fclose(wav); + } + + fclose(outFile); + printf("Done!\n"); +} + +int main(int argc, char** argv) { + if(argc != 2) { + printf("Usage: 2dxbuild output\n"); + return 1; + } + + build_2dx(argv[1]); + return 0; +} \ No newline at end of file diff --git a/2dxDump.c b/2dxDump.c new file mode 100644 index 0000000..cf13254 --- /dev/null +++ b/2dxDump.c @@ -0,0 +1,55 @@ +#include +#include +#include + +#include "2dx.h" +#include "shared.h" + +void extract_2dx(char* path) { + FILE* f = fopen(path, "rb"); + + if(!f) { + printf("Could not open %s, skipping\n", path); + return; + } + + fileHeader_t fileHeader; + uint32_t *fileOffsets; + dxHeader_t dxHeader; + char outPath[256]; + FILE* outFile; + + fread(&fileHeader, sizeof(fileHeader), 1, f); + //printf("2dx contains %d file(s)\n", fileHeader.fileCount); + fileOffsets = malloc(sizeof(uint32_t) * fileHeader.fileCount); + fread(fileOffsets, sizeof(uint32_t), fileHeader.fileCount, f); + + for(int i = 0; i < fileHeader.fileCount; i++) { + fseek(f, fileOffsets[i], SEEK_SET); + + // TODO verify 2DX9 + fread(&dxHeader, sizeof(dxHeader), 1, f); + snprintf(outPath, 256, "%d.wav", i); + outFile = fopen(outPath, "wb"); + // seek to RIFF start + fseek(f, fileOffsets[i]+dxHeader.headerSize, SEEK_SET); + fprintf(stderr, "Extracting %s...\n", outPath); + transfer_file(f, outFile, dxHeader.wavSize); + fclose(outFile); + } + + free(fileOffsets); + //printf("Done!\n"); +} + +int main(int argc, char** argv) { + if(argc < 2) { + printf("Usage: 2dxdump file1 [file2 ...]\n"); + return 1; + } + + for(int i = 1; i < argc; i++) { + extract_2dx(argv[i]); + } + return 0; +} \ No newline at end of file diff --git a/2dxMerge.c b/2dxMerge.c new file mode 100644 index 0000000..2e272f7 --- /dev/null +++ b/2dxMerge.c @@ -0,0 +1,84 @@ +#include +#include +#include + +#include "2dx.h" +#include "shared.h" + +void update_2dx(char* inPath, char* outPath) { + size_t wavSize; + fileHeader_t fileHeader; + uint32_t *fileOffsets; + uint32_t *newOffsets; + dxHeader_t dxHeader; + char wavPath[256]; + + FILE* inFile = fopen(inPath, "rb"); + if(!inFile) { + printf("Could not open %s for reading\n", inPath); + return; + } + + FILE* outFile = fopen(outPath, "wb"); + if(!outFile) { + printf("Could not open %s for writing\n", outPath); + return; + } + + fread(&fileHeader, sizeof(fileHeader), 1, inFile); + fileOffsets = malloc(sizeof(uint32_t) * fileHeader.fileCount); + newOffsets = malloc(sizeof(uint32_t) * fileHeader.fileCount); + fread(fileOffsets, sizeof(uint32_t), fileHeader.fileCount, inFile); + memcpy(newOffsets, fileOffsets, sizeof(uint32_t) * fileHeader.fileCount); + + // update offsets for new wavs, start at 1 cause it has same offset + for(int i = 1; i < fileHeader.fileCount; i++) { + snprintf(wavPath, 256, "%d.wav", i-1); + // we have a new wav! + if((wavSize = file_size(wavPath)) == 0) { + // calculate size from old offsets + wavSize = fileOffsets[i] - fileOffsets[i-1] - sizeof(dxHeader); + } + newOffsets[i] = newOffsets[i-1] + sizeof(dxHeader) + wavSize; + } + + rewind(inFile); + // unchanged + fwrite(&fileHeader, sizeof(fileHeader), 1, outFile); + // new offsets + fwrite(newOffsets, sizeof(uint32_t), fileHeader.fileCount, outFile); + // new wavs + for(int i = 0; i < fileHeader.fileCount; i++) { + // load existing header + fseek(inFile, fileOffsets[i], SEEK_SET); + fread(&dxHeader, sizeof(dxHeader), 1, inFile); + + snprintf(wavPath, 256, "%d.wav", i); + // new wav to insert + if((wavSize = file_size(wavPath))) { + printf("Updating %s\n", wavPath); + dxHeader.wavSize = wavSize; + FILE* newWav = fopen(wavPath, "rb"); + fwrite(&dxHeader, sizeof(dxHeader), 1, outFile); + transfer_file(newWav, outFile, wavSize); + fclose(newWav); + } else { + fwrite(&dxHeader, sizeof(dxHeader), 1, outFile); + transfer_file(inFile, outFile, dxHeader.wavSize); + } + } + + fclose(outFile); + fclose(inFile); + printf("Done!\n"); +} + +int main(int argc, char** argv) { + if(argc != 3) { + printf("Usage: 2dxupdate input output\n"); + return 1; + } + + update_2dx(argv[1], argv[2]); + return 0; +} \ No newline at end of file diff --git a/2dxWav.c b/2dxWav.c new file mode 100644 index 0000000..c98c40f --- /dev/null +++ b/2dxWav.c @@ -0,0 +1,170 @@ +#include "sox/sox.h" +#include "2dxWav.h" +#include +#include +#include + +// the assert in assert.h produces a crash, this just returns +#define assert(cond) if(!(cond)) {printf("Something went wrong: " #cond "\n"); return 1;} + +/* Private data for .wav file, taken from wav.c so we can change + certain attributes to the 2dx specific format */ +typedef struct { + uint64_t numSamples; + size_t dataLength; + unsigned short formatTag; + unsigned short samplesPerBlock; + unsigned short blockAlign; + size_t dataStart; + char * comment; + int ignoreSize; + + unsigned short nCoefs; /* ADPCM: number of coef sets */ + short *lsx_ms_adpcm_i_coefs; /* ADPCM: coef sets */ + unsigned char *packet; /* Temporary buffer for packets */ + short *samples; /* interleaved samples buffer */ + short *samplePtr; /* Pointer to current sample */ + short *sampleTop; /* End of samples-buffer */ + unsigned short blockSamplesRemaining;/* Samples remaining per channel */ + int state[16]; /* step-size info for *ADPCM writes */ + + /* there are more things in here but they're related to GSM encoding */ +} priv_t; + +/* For debugging */ +void printwav(priv_t* wav) { + printf("\t numSamples: %d\n" , wav->numSamples ); + printf("\t dataLength: %zu\n" , wav->dataLength ); + printf("\t formatTag: %hu\n" , wav->formatTag ); + printf("\t samplesPerBlock: %hu\n" , wav->samplesPerBlock ); + printf("\t blockAlign: %hu\n" , wav->blockAlign ); + printf("\t dataStart: %zu\n" , wav->dataStart ); + printf("\t ignoreSize: %d\n" , wav->ignoreSize ); + printf("\t nCoefs: %hu\n" , wav->nCoefs ); + printf("\t lsx_ms_adpcm_i_coefs: %d\n" , wav->lsx_ms_adpcm_i_coefs ); + printf("\t packet: %d\n:" , wav->packet ); + printf("\t samples: %d\n" , wav->samples ); + printf("\t samplePtr: %d\n" , wav->samplePtr ); + printf("\t sampleTop: %d\n" , wav->sampleTop ); + printf("\t blockSamplesRemaining: %d\n" , wav->blockSamplesRemaining ); +} + +int convert_wav(char* inFile, char* outWav, int trimPreview) { + static sox_format_t *in, *out; + sox_effects_chain_t *chain; + sox_effect_t *e; + sox_signalinfo_t interm_signal; + char *args[10]; + + assert(sox_init() == SOX_SUCCESS); + assert(in = sox_open_read(inFile, NULL, NULL, NULL)); + + // 2dx specific format + sox_signalinfo_t out_signal = { + .rate = 44100, + .channels = 2, // have to be 2 for later hacks to work + .precision = 0, + .length = 0, + .mult = NULL + }; + + sox_encodinginfo_t out_encoding; + memset(&out_encoding, 0, sizeof(out_encoding)); + out_encoding.encoding = SOX_ENCODING_MS_ADPCM; + out_encoding.bits_per_sample = 4; + + assert(out = sox_open_write(outWav, &out_signal, &out_encoding, "wav", NULL, NULL)); + // Forcibly set the values we want + priv_t *wav = (priv_t *) out->priv; + wav->samplesPerBlock = 244; + wav->blockAlign = 256; + // taken from wav.c, with locked 2 channels + // The buffers become the wrong size, but we use smaller buffers so it's ok + size_t sbsize = 2 * wav->samplesPerBlock; + wav->sampleTop = wav->samples + sbsize; + + // we'll need these later to fix the headers + uint16_t blockAlign = wav->blockAlign; + uint16_t samplesPerBlock = wav->samplesPerBlock; + uint32_t avgBytes = (double)blockAlign*out_signal.rate / (double)samplesPerBlock + 0.5; + + // Debugging + /*printf("\n\nOutput data:\n"); + printwav(wav); + + wav = (priv_t *) in->priv; + printf("\n\nInput data:\n"); + printwav(wav);*/ + + chain = sox_create_effects_chain(&in->encoding, &out->encoding); + interm_signal = in->signal; /* NB: deep copy */ + e = sox_create_effect(sox_find_effect("input")); + args[0] = (char *)in; + assert(sox_effect_options(e, 1, args) == SOX_SUCCESS); + assert(sox_add_effect(chain, e, &in->signal, &in->signal) == SOX_SUCCESS); + free(e); + + if (in->signal.rate != out->signal.rate) { + e = sox_create_effect(sox_find_effect("rate")); + assert(sox_effect_options(e, 0, NULL) == SOX_SUCCESS); + assert(sox_add_effect(chain, e, &interm_signal, &out->signal) == SOX_SUCCESS); + free(e); + } + + if (in->signal.channels != out->signal.channels) { + e = sox_create_effect(sox_find_effect("channels")); + assert(sox_effect_options(e, 0, NULL) == SOX_SUCCESS); + assert(sox_add_effect(chain, e, &interm_signal, &out->signal) == SOX_SUCCESS); + free(e); + } + + // Only use 10 seconds of audio for previews + if(trimPreview) { + e = sox_create_effect(sox_find_effect("trim")); + args[0] = "0"; + args[1] = "10"; + assert(sox_effect_options(e, 1, args) == SOX_SUCCESS); + assert(sox_add_effect(chain, e, &interm_signal, &in->signal) == SOX_SUCCESS); + free(e); + } + + e = sox_create_effect(sox_find_effect("output")); + args[0] = (char *)out; + assert(sox_effect_options(e, 1, args) == SOX_SUCCESS); + assert(sox_add_effect(chain, e, &in->signal, &in->signal) == SOX_SUCCESS); + free(e); + + /* Flow samples through the effects processing chain until EOF is reached */ + sox_flow_effects(chain, NULL, NULL); + /* All done; tidy up: */ + sox_delete_effects_chain(chain); + sox_close(out); + sox_close(in); + sox_quit(); + + // Fix the header, since sox wrote it before we changed our block stuff + FILE* outFile = fopen(outWav, "r+b"); + // AvgBytesPerSec + fseek(outFile, 28, SEEK_SET); + fwrite(&avgBytes, sizeof(uint32_t), 1, outFile); + fwrite(&blockAlign, sizeof(uint16_t), 1, outFile); + // samples per block + fseek(outFile, 38, SEEK_SET); + fwrite(&samplesPerBlock, sizeof(uint16_t), 1, outFile); + + // RIFF size + // minus 4 byte "RIFF" and 4 bytes for this number + size_t wavSize = file_size(outWav) - 8; + fseek(outFile, 4, SEEK_SET); + fwrite(&wavSize, sizeof(uint32_t), 1, outFile); + + //// data size + // minus the RIFF header size + wavSize -= 82; + fseek(outFile, 86, SEEK_SET); + fwrite(&wavSize, sizeof(uint32_t), 1, outFile); + + fclose(outFile); + + return 0; +} \ No newline at end of file diff --git a/2dxWav.h b/2dxWav.h new file mode 100644 index 0000000..644b6d1 --- /dev/null +++ b/2dxWav.h @@ -0,0 +1,9 @@ +#ifndef _2DX_WAV_H +#define _2DX_WAV_H + +#include "shared.h" + +// Converts a wav to a file +int convert_wav(char* inFile, char* outWav, int trimPreview); + +#endif \ No newline at end of file diff --git a/2dxWavConvert.c b/2dxWavConvert.c new file mode 100644 index 0000000..8416735 --- /dev/null +++ b/2dxWavConvert.c @@ -0,0 +1,20 @@ +#include +#include +#include + +#include "shared.h" +#include "2dxWav.h" + +int main(int argc, char * argv[]) +{ + if(argc != 3) { + printf("2dxWavConvert infile outwav\n"); + return 1; + } + + if(convert_wav(argv[1], argv[2], 0)) { + printf("Conversion failed!\n"); + return 1; + } + return 0; +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cb03037 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +CC=gcc +CFLAGS=-std=c99 -Wall -pedantic + +all: 2dxWavConvert 2dxDump 2dxBuild 2dxMerge + +shared.o: shared.c shared.h + $(CC) $(CFLAGS) -c shared.c -o shared.o + +2dxWav.o: 2dxWav.c 2dxWav.h shared.o + $(CC) $(CFLAGS) -c 2dxWav.c -o 2dxWav.o + +2dxDump: 2dxDump.c 2dx.h shared.o + $(CC) $(CFLAGS) 2dxDump.c shared.o -o 2dxDump + +2dxBuild: 2dxBuild.c 2dx.h shared.o + $(CC) $(CFLAGS) 2dxBuild.c shared.o -o 2dxBuild + +2dxMerge: 2dxMerge.c 2dx.h shared.o + $(CC) $(CFLAGS) 2dxMerge.c shared.o -o 2dxMerge + +2dxWavConvert: 2dxWavConvert.c shared.o 2dxWav.o + $(CC) $(CFLAGS) 2dxWavConvert.c shared.o 2dxWav.o -o 2dxWavConvert libsox-3.dll + +.PHONY: clean +clean: + rm -f *.o 2dxDump 2dxBuild 2dxMerge \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..eab6374 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +#2dxTools + +A set of tools for working with 2dx audio containers. + +###2dxDump + +`2dxDump infile.2dx` + +Takes a 2dx and dumps all its wavs. Names them sequentially with no leading zeros. + +###2dxBuild + +`2dxBuild outfile.2dx` + +Builds a 2dx with default paramaters for loop point/volume etc. + +###2dxMerge + +`2dxMerge infile.2dx outfile.2dx` + +For each file in `infile.2dx`, if there is a `.wav` present in the directory, +it will replace it in the new file. Useful for extracting a single audio file, +editing it, then adding it back. + +###2dxWavConvert + +`2dxWavConvert infile outfile.wav [preview]` + +Takes any file that `sox` supports (mp3, flac, wav, ogg, etc) and converts it to +the specific format required for 2dx files (MS-ADPCM wav with a block size of 256). +If the third argument is "preview", the file is clipped to exactly 10 seconds +to comply with preview wav requirements. + +##Tools to come: + +2dxTransfer - will work like 2dxMerge, but will take two input files, an output, +and a list of tracks to transfer. Metadata (such as loop points) will also be +transferred. + +Enhancements to 2dxDump and 2dxBuild to generate and load xml files so track +parameters can be modified. + +If you have a burning need for one of these unfinished tools, please get in +contact. I appreciate motivation. \ No newline at end of file diff --git a/shared.c b/shared.c new file mode 100644 index 0000000..b9c7dee --- /dev/null +++ b/shared.c @@ -0,0 +1,35 @@ +#include "shared.h" +#include + +// TODO: buffering for speed +void transfer_file(FILE* in, FILE* out, size_t bytes) { + char byte; + for(size_t i = 0; i < bytes; i++) { + if(!fread(&byte, 1, 1, in)) { + printf("Could not read all bytes from input!\n"); + exit(1); + } + fwrite(&byte, 1, 1, out); + } +} + +// returns 0 if file is empty or noexistant, filesize otherwise +size_t file_size(const char* path) { + FILE* f = fopen(path, "rb"); + if(!f) + return 0; + fseek(f, 0L, SEEK_END); + size_t size = ftell(f); + fclose(f); + return size; +} + +// platform agnostic and proves we can read a file too +int file_exists(const char *path) { + FILE *file; + if ((file = fopen(path, "rb"))) { + fclose(file); + return 1; + } + return 0; +} \ No newline at end of file diff --git a/shared.h b/shared.h new file mode 100644 index 0000000..3fc57f0 --- /dev/null +++ b/shared.h @@ -0,0 +1,10 @@ +#ifndef _SHARED_H +#define _SHARED_H + +#include + +void transfer_file(FILE* in, FILE* out, size_t bytes); +size_t file_size(const char* path); +int file_exists(const char *path); + +#endif \ No newline at end of file