├── .gitignore ├── 2dx.h ├── 2dxBuild.c ├── 2dxDump.c ├── 2dxMerge.c ├── 2dxWav.c ├── 2dxWav.h ├── 2dxWavConvert.c ├── Makefile ├── README.md ├── shared.c └── shared.h /.gitignore: -------------------------------------------------------------------------------- 1 | sox/ 2 | *.dll 3 | *.wav 4 | *.exe 5 | *.o 6 | -------------------------------------------------------------------------------- /2dx.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef struct { 4 | char name[16]; 5 | uint32_t headerSize; // this + offsets table 6 | uint32_t fileCount; 7 | char unknown[48]; // contains rest of title, some random flags 8 | } fileHeader_t; 9 | 10 | typedef struct{ 11 | char dx[4]; // should be "2DX9"; 12 | uint32_t headerSize; // always 24, includes dx chars 13 | uint32_t wavSize; 14 | int16_t unk1; // always 0x3231 15 | int16_t trackId; // always -1 for previews, 0-7 for song + effected versions, 9 to 11 used for a few effects 16 | int16_t unk2; // all 64, except song selection change 'click' is 40 17 | int16_t attenuation; // 0-127 for varying quietness 18 | int32_t loopPoint; // sample to loop at * 4 19 | } dxHeader_t; -------------------------------------------------------------------------------- /2dxBuild.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "2dx.h" 6 | #include "shared.h" 7 | 8 | int count_wav(void) { 9 | int ret = 0; 10 | char filename[256]; 11 | while(snprintf(filename, 256, "%d.wav", ret), file_exists(filename)) 12 | ret++; 13 | return ret; 14 | } 15 | 16 | void build_2dx(char* path) { 17 | int wavCount = count_wav(); 18 | printf("Writing %d wavs to %s\n",wavCount, path); 19 | 20 | FILE* outFile = fopen(path, "wb"); 21 | 22 | if(!outFile) { 23 | printf("Could not open %s\n", path); 24 | return; 25 | } 26 | 27 | fileHeader_t fileHeader; 28 | dxHeader_t dxHeader; 29 | char wavPath[256]; 30 | 31 | // +1 to ignore null terminator, clobbers headerSize but we write that next 32 | strncpy(fileHeader.name, path, sizeof(fileHeader.name)+1); 33 | fileHeader.headerSize = sizeof(fileHeader) + sizeof(uint32_t)*wavCount; 34 | fileHeader.fileCount = wavCount; 35 | memset(fileHeader.unknown, 0, sizeof(fileHeader.unknown)); 36 | fwrite(&fileHeader, sizeof(fileHeader), 1, outFile); 37 | 38 | // file offsets 39 | uint32_t offset = fileHeader.headerSize; 40 | for(int i = 0; i < wavCount; i++) { 41 | snprintf(wavPath, 256, "%d.wav", i); 42 | FILE* wav = fopen(wavPath, "rb"); 43 | 44 | fseek(wav, 0L, SEEK_END); 45 | size_t wavSize = ftell(wav); 46 | rewind(wav); 47 | 48 | fwrite(&offset, sizeof(uint32_t), 1, outFile); 49 | offset += wavSize + sizeof(dxHeader_t); 50 | fclose(wav); 51 | } 52 | // the actual wavs 53 | for(int i = 0; i < wavCount; i++) { 54 | snprintf(wavPath, 256, "%d.wav", i); 55 | FILE* wav = fopen(wavPath, "rb"); 56 | 57 | fseek(wav, 0L, SEEK_END); 58 | size_t wavSize = ftell(wav); 59 | rewind(wav); 60 | 61 | memcpy(dxHeader.dx, "2DX9", 4); 62 | dxHeader.headerSize = 24; 63 | dxHeader.wavSize = wavSize; 64 | dxHeader.unk1 = 0x3231; 65 | // these match preview files for convenience 66 | dxHeader.trackId = -1; // I may regret this 67 | dxHeader.unk2 = 64; 68 | dxHeader.attenuation = 1; 69 | dxHeader.loopPoint = 0; 70 | fwrite(&dxHeader, sizeof(dxHeader), 1, outFile); 71 | transfer_file(wav, outFile, wavSize); 72 | 73 | fclose(wav); 74 | } 75 | 76 | fclose(outFile); 77 | printf("Done!\n"); 78 | } 79 | 80 | int main(int argc, char** argv) { 81 | if(argc != 2) { 82 | printf("Usage: 2dxbuild output\n"); 83 | return 1; 84 | } 85 | 86 | build_2dx(argv[1]); 87 | return 0; 88 | } -------------------------------------------------------------------------------- /2dxDump.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "2dx.h" 6 | #include "shared.h" 7 | 8 | void extract_2dx(char* path) { 9 | FILE* f = fopen(path, "rb"); 10 | 11 | if(!f) { 12 | printf("Could not open %s, skipping\n", path); 13 | return; 14 | } 15 | 16 | fileHeader_t fileHeader; 17 | uint32_t *fileOffsets; 18 | dxHeader_t dxHeader; 19 | char outPath[256]; 20 | FILE* outFile; 21 | 22 | fread(&fileHeader, sizeof(fileHeader), 1, f); 23 | //printf("2dx contains %d file(s)\n", fileHeader.fileCount); 24 | fileOffsets = malloc(sizeof(uint32_t) * fileHeader.fileCount); 25 | fread(fileOffsets, sizeof(uint32_t), fileHeader.fileCount, f); 26 | 27 | for(int i = 0; i < fileHeader.fileCount; i++) { 28 | fseek(f, fileOffsets[i], SEEK_SET); 29 | 30 | // TODO verify 2DX9 31 | fread(&dxHeader, sizeof(dxHeader), 1, f); 32 | snprintf(outPath, 256, "%d.wav", i); 33 | outFile = fopen(outPath, "wb"); 34 | // seek to RIFF start 35 | fseek(f, fileOffsets[i]+dxHeader.headerSize, SEEK_SET); 36 | fprintf(stderr, "Extracting %s...\n", outPath); 37 | transfer_file(f, outFile, dxHeader.wavSize); 38 | fclose(outFile); 39 | } 40 | fclose(f); 41 | free(fileOffsets); 42 | //printf("Done!\n"); 43 | } 44 | 45 | int main(int argc, char** argv) { 46 | if(argc < 2) { 47 | printf("Usage: 2dxdump file1 [file2 ...]\n"); 48 | return 1; 49 | } 50 | 51 | for(int i = 1; i < argc; i++) { 52 | extract_2dx(argv[i]); 53 | } 54 | return 0; 55 | } 56 | -------------------------------------------------------------------------------- /2dxMerge.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "2dx.h" 6 | #include "shared.h" 7 | 8 | void update_2dx(char* inPath, char* outPath) { 9 | size_t wavSize; 10 | fileHeader_t fileHeader; 11 | uint32_t *fileOffsets; 12 | uint32_t *newOffsets; 13 | dxHeader_t dxHeader; 14 | char wavPath[256]; 15 | 16 | FILE* inFile = fopen(inPath, "rb"); 17 | if(!inFile) { 18 | printf("Could not open %s for reading\n", inPath); 19 | return; 20 | } 21 | 22 | FILE* outFile = fopen(outPath, "wb"); 23 | if(!outFile) { 24 | printf("Could not open %s for writing\n", outPath); 25 | return; 26 | } 27 | 28 | fread(&fileHeader, sizeof(fileHeader), 1, inFile); 29 | fileOffsets = malloc(sizeof(uint32_t) * fileHeader.fileCount); 30 | newOffsets = malloc(sizeof(uint32_t) * fileHeader.fileCount); 31 | fread(fileOffsets, sizeof(uint32_t), fileHeader.fileCount, inFile); 32 | memcpy(newOffsets, fileOffsets, sizeof(uint32_t) * fileHeader.fileCount); 33 | 34 | // update offsets for new wavs, start at 1 cause it has same offset 35 | for(int i = 1; i < fileHeader.fileCount; i++) { 36 | snprintf(wavPath, 256, "%d.wav", i-1); 37 | // we have a new wav! 38 | if((wavSize = file_size(wavPath)) == 0) { 39 | // calculate size from old offsets 40 | wavSize = fileOffsets[i] - fileOffsets[i-1] - sizeof(dxHeader); 41 | } 42 | newOffsets[i] = newOffsets[i-1] + sizeof(dxHeader) + wavSize; 43 | } 44 | 45 | rewind(inFile); 46 | // unchanged 47 | fwrite(&fileHeader, sizeof(fileHeader), 1, outFile); 48 | // new offsets 49 | fwrite(newOffsets, sizeof(uint32_t), fileHeader.fileCount, outFile); 50 | // new wavs 51 | for(int i = 0; i < fileHeader.fileCount; i++) { 52 | // load existing header 53 | fseek(inFile, fileOffsets[i], SEEK_SET); 54 | fread(&dxHeader, sizeof(dxHeader), 1, inFile); 55 | 56 | snprintf(wavPath, 256, "%d.wav", i); 57 | // new wav to insert 58 | if((wavSize = file_size(wavPath))) { 59 | printf("Updating %s\n", wavPath); 60 | dxHeader.wavSize = wavSize; 61 | FILE* newWav = fopen(wavPath, "rb"); 62 | fwrite(&dxHeader, sizeof(dxHeader), 1, outFile); 63 | transfer_file(newWav, outFile, wavSize); 64 | fclose(newWav); 65 | } else { 66 | fwrite(&dxHeader, sizeof(dxHeader), 1, outFile); 67 | transfer_file(inFile, outFile, dxHeader.wavSize); 68 | } 69 | } 70 | 71 | fclose(outFile); 72 | fclose(inFile); 73 | printf("Done!\n"); 74 | } 75 | 76 | int main(int argc, char** argv) { 77 | if(argc != 3) { 78 | printf("Usage: 2dxupdate input output\n"); 79 | return 1; 80 | } 81 | 82 | update_2dx(argv[1], argv[2]); 83 | return 0; 84 | } -------------------------------------------------------------------------------- /2dxWav.c: -------------------------------------------------------------------------------- 1 | #include "sox/sox.h" 2 | #include "2dxWav.h" 3 | #include 4 | #include 5 | #include 6 | 7 | // the assert in assert.h produces a crash, this just returns 8 | #define assert(cond) if(!(cond)) {printf("Something went wrong: " #cond "\n"); return 1;} 9 | 10 | /* Private data for .wav file, taken from wav.c so we can change 11 | certain attributes to the 2dx specific format */ 12 | typedef struct { 13 | uint64_t numSamples; 14 | size_t dataLength; 15 | unsigned short formatTag; 16 | unsigned short samplesPerBlock; 17 | unsigned short blockAlign; 18 | size_t dataStart; 19 | char * comment; 20 | int ignoreSize; 21 | 22 | unsigned short nCoefs; /* ADPCM: number of coef sets */ 23 | short *lsx_ms_adpcm_i_coefs; /* ADPCM: coef sets */ 24 | unsigned char *packet; /* Temporary buffer for packets */ 25 | short *samples; /* interleaved samples buffer */ 26 | short *samplePtr; /* Pointer to current sample */ 27 | short *sampleTop; /* End of samples-buffer */ 28 | unsigned short blockSamplesRemaining;/* Samples remaining per channel */ 29 | int state[16]; /* step-size info for *ADPCM writes */ 30 | 31 | /* there are more things in here but they're related to GSM encoding */ 32 | } priv_t; 33 | 34 | /* For debugging */ 35 | void printwav(priv_t* wav) { 36 | printf("\t numSamples: %d\n" , wav->numSamples ); 37 | printf("\t dataLength: %zu\n" , wav->dataLength ); 38 | printf("\t formatTag: %hu\n" , wav->formatTag ); 39 | printf("\t samplesPerBlock: %hu\n" , wav->samplesPerBlock ); 40 | printf("\t blockAlign: %hu\n" , wav->blockAlign ); 41 | printf("\t dataStart: %zu\n" , wav->dataStart ); 42 | printf("\t ignoreSize: %d\n" , wav->ignoreSize ); 43 | printf("\t nCoefs: %hu\n" , wav->nCoefs ); 44 | printf("\t lsx_ms_adpcm_i_coefs: %d\n" , wav->lsx_ms_adpcm_i_coefs ); 45 | printf("\t packet: %d\n:" , wav->packet ); 46 | printf("\t samples: %d\n" , wav->samples ); 47 | printf("\t samplePtr: %d\n" , wav->samplePtr ); 48 | printf("\t sampleTop: %d\n" , wav->sampleTop ); 49 | printf("\t blockSamplesRemaining: %d\n" , wav->blockSamplesRemaining ); 50 | } 51 | 52 | int convert_wav(char* inFile, char* outWav, int trimPreview) { 53 | static sox_format_t *in, *out; 54 | sox_effects_chain_t *chain; 55 | sox_effect_t *e; 56 | sox_signalinfo_t interm_signal; 57 | char *args[10]; 58 | 59 | assert(sox_init() == SOX_SUCCESS); 60 | assert(in = sox_open_read(inFile, NULL, NULL, NULL)); 61 | 62 | // 2dx specific format 63 | sox_signalinfo_t out_signal = { 64 | .rate = 44100, 65 | .channels = 2, // have to be 2 for later hacks to work 66 | .precision = 0, 67 | .length = 0, 68 | .mult = NULL 69 | }; 70 | 71 | sox_encodinginfo_t out_encoding; 72 | memset(&out_encoding, 0, sizeof(out_encoding)); 73 | out_encoding.encoding = SOX_ENCODING_MS_ADPCM; 74 | out_encoding.bits_per_sample = 4; 75 | 76 | assert(out = sox_open_write(outWav, &out_signal, &out_encoding, "wav", NULL, NULL)); 77 | // Forcibly set the values we want 78 | priv_t *wav = (priv_t *) out->priv; 79 | wav->samplesPerBlock = 244; 80 | wav->blockAlign = 256; 81 | // taken from wav.c, with locked 2 channels 82 | // The buffers become the wrong size, but we use smaller buffers so it's ok 83 | size_t sbsize = 2 * wav->samplesPerBlock; 84 | wav->sampleTop = wav->samples + sbsize; 85 | 86 | // we'll need these later to fix the headers 87 | uint16_t blockAlign = wav->blockAlign; 88 | uint16_t samplesPerBlock = wav->samplesPerBlock; 89 | uint32_t avgBytes = (double)blockAlign*out_signal.rate / (double)samplesPerBlock + 0.5; 90 | 91 | // Debugging 92 | /*printf("\n\nOutput data:\n"); 93 | printwav(wav); 94 | 95 | wav = (priv_t *) in->priv; 96 | printf("\n\nInput data:\n"); 97 | printwav(wav);*/ 98 | 99 | chain = sox_create_effects_chain(&in->encoding, &out->encoding); 100 | interm_signal = in->signal; /* NB: deep copy */ 101 | e = sox_create_effect(sox_find_effect("input")); 102 | args[0] = (char *)in; 103 | assert(sox_effect_options(e, 1, args) == SOX_SUCCESS); 104 | assert(sox_add_effect(chain, e, &in->signal, &in->signal) == SOX_SUCCESS); 105 | free(e); 106 | 107 | if (in->signal.rate != out->signal.rate) { 108 | e = sox_create_effect(sox_find_effect("rate")); 109 | assert(sox_effect_options(e, 0, NULL) == SOX_SUCCESS); 110 | assert(sox_add_effect(chain, e, &interm_signal, &out->signal) == SOX_SUCCESS); 111 | free(e); 112 | } 113 | 114 | // Only use 10 seconds of audio for previews 115 | if(trimPreview) { 116 | e = sox_create_effect(sox_find_effect("trim")); 117 | args[0] = "0"; 118 | args[1] = "10"; 119 | assert(sox_effect_options(e, 2, args) == SOX_SUCCESS); 120 | assert(sox_add_effect(chain, e, &interm_signal, &in->signal) == SOX_SUCCESS); 121 | free(e); 122 | } 123 | 124 | if (in->signal.channels != out->signal.channels) { 125 | e = sox_create_effect(sox_find_effect("channels")); 126 | assert(sox_effect_options(e, 0, NULL) == SOX_SUCCESS); 127 | assert(sox_add_effect(chain, e, &interm_signal, &out->signal) == SOX_SUCCESS); 128 | free(e); 129 | } 130 | 131 | e = sox_create_effect(sox_find_effect("output")); 132 | args[0] = (char *)out; 133 | assert(sox_effect_options(e, 1, args) == SOX_SUCCESS); 134 | assert(sox_add_effect(chain, e, &in->signal, &in->signal) == SOX_SUCCESS); 135 | free(e); 136 | 137 | /* Flow samples through the effects processing chain until EOF is reached */ 138 | sox_flow_effects(chain, NULL, NULL); 139 | /* All done; tidy up: */ 140 | sox_delete_effects_chain(chain); 141 | sox_close(out); 142 | sox_close(in); 143 | sox_quit(); 144 | 145 | // Fix the header, since sox wrote it before we changed our block stuff 146 | FILE* outFile = fopen(outWav, "r+b"); 147 | // AvgBytesPerSec 148 | fseek(outFile, 28, SEEK_SET); 149 | fwrite(&avgBytes, sizeof(uint32_t), 1, outFile); 150 | fwrite(&blockAlign, sizeof(uint16_t), 1, outFile); 151 | // samples per block 152 | fseek(outFile, 38, SEEK_SET); 153 | fwrite(&samplesPerBlock, sizeof(uint16_t), 1, outFile); 154 | 155 | // RIFF size 156 | // minus 4 byte "RIFF" and 4 bytes for this number 157 | size_t wavSize = file_size(outWav) - 8; 158 | fseek(outFile, 4, SEEK_SET); 159 | fwrite(&wavSize, sizeof(uint32_t), 1, outFile); 160 | 161 | //// data size 162 | // minus the RIFF header size 163 | wavSize -= 82; 164 | fseek(outFile, 86, SEEK_SET); 165 | fwrite(&wavSize, sizeof(uint32_t), 1, outFile); 166 | 167 | fclose(outFile); 168 | 169 | return 0; 170 | } -------------------------------------------------------------------------------- /2dxWav.h: -------------------------------------------------------------------------------- 1 | #ifndef _2DX_WAV_H 2 | #define _2DX_WAV_H 3 | 4 | #include "shared.h" 5 | 6 | // Converts a wav to a file 7 | int convert_wav(char* inFile, char* outWav, int trimPreview); 8 | 9 | #endif -------------------------------------------------------------------------------- /2dxWavConvert.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "shared.h" 7 | #include "2dxWav.h" 8 | 9 | int main(int argc, char * argv[]) 10 | { 11 | if(argc != 3 && argc != 4) { 12 | printf("2dxWavConvert infile outwav [preview]\n"); 13 | return 1; 14 | } 15 | int preview = 0; 16 | if(argc == 4 && !strcmp("preview", argv[3])) 17 | preview = 1; 18 | 19 | if(convert_wav(argv[1], argv[2], preview)) { 20 | printf("Conversion failed!\n"); 21 | return 1; 22 | } 23 | return 0; 24 | } 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-std=c99 -Wall -pedantic 3 | 4 | all: 2dxWavConvert 2dxDump 2dxBuild 2dxMerge 5 | 6 | shared.o: shared.c shared.h 7 | $(CC) $(CFLAGS) -c shared.c -o shared.o 8 | 9 | 2dxWav.o: 2dxWav.c 2dxWav.h shared.o 10 | $(CC) $(CFLAGS) -c 2dxWav.c -o 2dxWav.o 11 | 12 | 2dxDump: 2dxDump.c 2dx.h shared.o 13 | $(CC) $(CFLAGS) 2dxDump.c shared.o -o 2dxDump 14 | 15 | 2dxBuild: 2dxBuild.c 2dx.h shared.o 16 | $(CC) $(CFLAGS) 2dxBuild.c shared.o -o 2dxBuild 17 | 18 | 2dxMerge: 2dxMerge.c 2dx.h shared.o 19 | $(CC) $(CFLAGS) 2dxMerge.c shared.o -o 2dxMerge 20 | 21 | 2dxWavConvert: 2dxWavConvert.c shared.o 2dxWav.o 22 | $(CC) $(CFLAGS) 2dxWavConvert.c shared.o 2dxWav.o -o 2dxWavConvert libsox-3.dll 23 | 24 | .PHONY: clean 25 | clean: 26 | rm -f *.o 2dxDump 2dxBuild 2dxMerge -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2dxTools 2 | 3 | A set of tools for working with 2dx audio containers. 4 | 5 | [Download here](https://github.com/mon/2dxTools/releases) 6 | 7 | ### 2dxDump 8 | 9 | `2dxDump infile.2dx` 10 | 11 | Takes a 2dx and dumps all its wavs. Names them sequentially with no leading zeros. 12 | 13 | ### 2dxBuild 14 | 15 | `2dxBuild outfile.2dx` 16 | 17 | Builds a 2dx with default paramaters for loop point/volume etc. 18 | 19 | ### 2dxMerge 20 | 21 | `2dxMerge infile.2dx outfile.2dx` 22 | 23 | For each file in `infile.2dx`, if there is a `.wav` present in the directory, 24 | it will replace it in the new file. Useful for extracting a single audio file, 25 | editing it, then adding it back. 26 | 27 | ### 2dxWavConvert 28 | 29 | `2dxWavConvert infile outfile.wav [preview]` 30 | 31 | Takes any file that `sox` supports (mp3, flac, wav, ogg, etc) and converts it to 32 | the specific format required for 2dx files (MS-ADPCM wav with a block size of 256). 33 | If the third argument is "preview", the file is clipped to exactly 10 seconds 34 | to comply with preview wav requirements. 35 | 36 | ## Example: Custom song preview + game audio 37 | 38 | `2dxWavConvert mySong.ogg 0.wav` 39 | `2dxBuild 1234.2dx` 40 | `2dxWavConvert mySong_preview.ogg 0.wav preview` 41 | `2dxBuild 1234_pre.2dx` 42 | 43 | ## Tools to come: 44 | 45 | 2dxTransfer - will work like 2dxMerge, but will take two input files, an output, 46 | and a list of tracks to transfer. Metadata (such as loop points) will also be 47 | transferred. 48 | 49 | Enhancements to 2dxDump and 2dxBuild to generate and load xml files so track 50 | parameters can be modified. 51 | 52 | If you have a burning need for one of these unfinished tools, please get in 53 | contact. I appreciate motivation. 54 | -------------------------------------------------------------------------------- /shared.c: -------------------------------------------------------------------------------- 1 | #include "shared.h" 2 | #include 3 | 4 | // TODO: buffering for speed 5 | void transfer_file(FILE* in, FILE* out, size_t bytes) { 6 | char byte; 7 | for(size_t i = 0; i < bytes; i++) { 8 | if(!fread(&byte, 1, 1, in)) { 9 | printf("Could not read all bytes from input!\n"); 10 | exit(1); 11 | } 12 | fwrite(&byte, 1, 1, out); 13 | } 14 | } 15 | 16 | // returns 0 if file is empty or noexistant, filesize otherwise 17 | size_t file_size(const char* path) { 18 | FILE* f = fopen(path, "rb"); 19 | if(!f) 20 | return 0; 21 | fseek(f, 0L, SEEK_END); 22 | size_t size = ftell(f); 23 | fclose(f); 24 | return size; 25 | } 26 | 27 | // platform agnostic and proves we can read a file too 28 | int file_exists(const char *path) { 29 | FILE *file; 30 | if ((file = fopen(path, "rb"))) { 31 | fclose(file); 32 | return 1; 33 | } 34 | return 0; 35 | } -------------------------------------------------------------------------------- /shared.h: -------------------------------------------------------------------------------- 1 | #ifndef _SHARED_H 2 | #define _SHARED_H 3 | 4 | #include 5 | 6 | void transfer_file(FILE* in, FILE* out, size_t bytes); 7 | size_t file_size(const char* path); 8 | int file_exists(const char *path); 9 | 10 | #endif --------------------------------------------------------------------------------