commit 9ec75bd24fcb43541728bece3b0c72ebc071421d Author: William Toohey Date: Thu Mar 23 22:36:32 2017 +1000 Bout time I made a repo for this 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