/* Copyright (c) 2020 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "audiolab/AudioRecipe.h" #include "audiosynth/sfxrSynth.h" #include "jsonserialization/JSONDeserializationContext.h" #include "nativeaudio/AudioOut.h" #include "pcmaudio/WAVAudioIO.h" #include "utilities/IOUtilities.h" #include "vorbisaudioio/VorbisAudioIO.h" #include #include #include #if defined(WIN32) #include #define msleep(ms) Sleep(ms) #else #define msleep(ms) usleep((ms) * 1000) #endif static const char * filePathForResource(ResourceManager * resourceManager, const char * resourceName) { const char * filePath = NULL; filePath = ResourceManager_resolveFilePath(resourceManager, resourceName); if (filePath == NULL) { filePath = resourceName; } return filePath; } static void * loadRecipeResource(Atom resourceName, void * context) { ResourceManager * resourceManager = context; const char * resourceFilePath = filePathForResource(resourceManager, resourceName); JSONDeserializationContext * deserializationContext = JSONDeserializationContext_createWithFile(resourceFilePath); if (deserializationContext->status != SERIALIZATION_ERROR_OK) { fprintf(stderr, "Error: Couldn't load \"%s\" as a json resource (error %d)\n", resourceFilePath, deserializationContext->status); return NULL; } AudioRecipe * recipe = AudioRecipe_deserialize(deserializationContext); if (recipe == NULL) { fprintf(stderr, "Error: Couldn't load \"%s\" as a recipe file (error %d)\n", resourceFilePath, deserializationContext->status); JSONDeserializationContext_dispose(deserializationContext); return NULL; } JSONDeserializationContext_dispose(deserializationContext); ResourceManager_addSearchPath(resourceManager, getDirectory(resourceFilePath)); return recipe; } static void unloadRecipeResource(void * resource, void * context) { AudioRecipe_dispose(resource); } static void * loadRecipeResourceResult(Atom resourceName, void * context) { ResourceManager * resourceManager = context; AudioRecipe * recipe = ResourceManager_referenceResource(resourceManager, ATOM("audiolab_recipe"), resourceName); if (recipe == NULL) { return NULL; } PCMAudio * result = AudioRecipe_evaluate(recipe, resourceManager); ResourceManager_releaseResource(resourceManager, recipe); return result; } static void * loadWAVResource(Atom resourceName, void * context) { return WAVAudioIO_readWAVFile(filePathForResource(context, resourceName)); } static void unloadPCMAudio(void * resource, void * context) { PCMAudio_dispose(resource); } static void * loadBFXRSoundResource(Atom resourceName, void * context) { struct bfxrTrackList trackList; if (!readBFXRSoundFile(resourceName, &trackList)) { return NULL; } struct bfxrTrackList * outTrackList = malloc(sizeof(*outTrackList)); *outTrackList = trackList; return outTrackList; } static void unloadBFXRSoundResource(void * resource, void * context) { struct bfxrTrackList * trackList = context; freeBFXRTrackList(trackList); free(trackList); } static void * loadSfxrXSoundResource(Atom resourceName, void * context) { struct sfxrParams params; sfxr_ResetParams(¶ms); if (!readSfxrXSoundFile(resourceName, ¶ms)) { return NULL; } struct sfxrParams * outParams = malloc(sizeof(*outParams)); *outParams = params; return outParams; } static void unloadSFXRSoundResource(void * resource, void * context) { free(context); } static void * loadBFXRSoundResourceResult(Atom resourceName, void * context) { ResourceManager * resourceManager = context; struct bfxrTrackList * trackList = ResourceManager_referenceResource(resourceManager, ATOM("bfxrsound"), resourceName); if (trackList == NULL) { return NULL; } PCMAudio * result = createBFXRSoundOutputFromTrackList(trackList); ResourceManager_releaseResource(resourceManager, trackList); return result; } static void * loadSfxrXSoundResourceResult(Atom resourceName, void * context) { ResourceManager * resourceManager = context; struct sfxrParams * params = ResourceManager_referenceResource(resourceManager, ATOM("sfxrXsound"), resourceName); if (params == NULL) { return NULL; } PCMAudio * result = createSFXRSoundOutputFromParams(params); ResourceManager_releaseResource(resourceManager, params); return result; } static void audioOutCallback(void * outSamples, unsigned int frameCount, void * context) { static unsigned int frameIndex; PCMAudio * audio = context; unsigned int sourceFrameCount = audio->frameCount; if (frameCount + frameIndex > sourceFrameCount) { frameCount = sourceFrameCount - frameIndex; } memcpy(outSamples, audio->samples + frameIndex * audio->channelCount * audio->bytesPerSample, frameCount * audio->channelCount * audio->bytesPerSample); frameIndex += frameCount; } static void printRecipePrereqs(AudioRecipe * recipe, ResourceManager * resourceManager) { for (unsigned int commandIndex = 0; commandIndex < recipe->commandCount; commandIndex++) { if (recipe->commands[commandIndex].type == COMMAND_MIX) { printf("%s ", filePathForResource(resourceManager, recipe->commands[commandIndex].data.mix.sourceName)); switch (recipe->commands[commandIndex].data.mix.sourceType) { case SOURCE_WAV_FILE: case SOURCE_OGG_FILE: case SOURCE_BFXR_SYNTH: case SOURCE_SFXRX_SYNTH: break; case SOURCE_RECIPE: { AudioRecipe * subrecipe = ResourceManager_referenceResource(resourceManager, ATOM("audiolab_recipe"), recipe->commands[commandIndex].data.mix.sourceName); if (subrecipe == NULL) { exit(EXIT_FAILURE); } printRecipePrereqs(subrecipe, resourceManager); ResourceManager_releaseResource(resourceManager, subrecipe); } } } } } static void printUsage(void) { fputs("Usage: audiorecipe [-o outputFile] [--format {wav|ogg}] [--play] recipe_file\n", stderr); fputs(" If outputFile is \"-\", or is unspecified and audiorecipe has not\n", stderr); fputs(" been invoked from a TTY, the output will be written to stdout.\n", stderr); } int main(int argc, const char ** argv) { const char * recipeFile = NULL, * outputFile = NULL; bool playOutput = false, isTTY = isatty(STDOUT_FILENO); AudioRecipe_outputFormat outputFormat = OUTPUT_FORMAT_UNSPECIFIED; bool printPrereqs = false; bool printOutputs = false; for (int argIndex = 1; argIndex < argc; argIndex++) { if (!strcmp(argv[argIndex], "--help")) { printUsage(); return EXIT_SUCCESS; } if (!strcmp(argv[argIndex], "-o")) { if (argIndex >= argc - 1) { fprintf(stderr, "Error: -o specified at the end of args; must be followed by a file path, or \"-\"\n"); printUsage(); return EXIT_FAILURE; } outputFile = argv[++argIndex]; } else if (!strcmp(argv[argIndex], "--format")) { if (argIndex >= argc - 1) { fprintf(stderr, "Error: --format specified at the end of args; must be followed by \"wav\" or \"ogg\"\n"); printUsage(); return EXIT_FAILURE; } if (outputFormat != OUTPUT_FORMAT_UNSPECIFIED) { fprintf(stderr, "Warning: Multiple --format arguments specified; using latest one\n"); } if (!strcmp(argv[argIndex + 1], "wav")) { outputFormat = OUTPUT_FORMAT_WAV; } else if (!strcmp(argv[argIndex + 1], "ogg")) { outputFormat = OUTPUT_FORMAT_OGG; } else { fprintf(stderr, "Error: Format \"%s\" unrecognized; must be \"wav\" or \"ogg\"\n", argv[argIndex + 1]); printUsage(); return EXIT_FAILURE; } ++argIndex; } else if (!strcmp(argv[argIndex], "--play")) { playOutput = true; } else if (!strcmp(argv[argIndex], "--prereqs")) { if (argIndex != 1) { fprintf(stderr, "Error: --prereqs specified after other arguments\n"); return EXIT_FAILURE; } printPrereqs = true; } else if (!strcmp(argv[argIndex], "--outputs")) { if (argIndex != 1) { fprintf(stderr, "Error: --outputs specified after other arguments\n"); return EXIT_FAILURE; } printOutputs = true; } else { if (recipeFile != NULL) { fprintf(stderr, "Error: Unrecognized option \"%s\"; was more than one recipe file specified? First recipe file argument appeared to be \"%s\"\n", argv[argIndex], outputFile); printUsage(); return EXIT_FAILURE; } recipeFile = argv[argIndex]; } } if (recipeFile == NULL) { fprintf(stderr, "Error: Recipe file must be specified\n"); printUsage(); return EXIT_FAILURE; } ResourceManager * resourceManager = ResourceManager_create(NULL); ResourceManager_addTypeHandler(resourceManager, ATOM("audiolab_recipe"), loadRecipeResource, unloadRecipeResource, PURGE_NEVER, resourceManager); ResourceManager_addTypeHandler(resourceManager, ATOM("wav_file"), loadWAVResource, unloadPCMAudio, PURGE_NEVER, resourceManager); ResourceManager_addTypeHandler(resourceManager, ATOM("audiolab_recipe_result"), loadRecipeResourceResult, unloadPCMAudio, PURGE_NEVER, resourceManager); ResourceManager_addTypeHandler(resourceManager, ATOM("bfxrsound"), loadBFXRSoundResource, unloadBFXRSoundResource, PURGE_NEVER, resourceManager); ResourceManager_addTypeHandler(resourceManager, ATOM("bfxrsound_result"), loadBFXRSoundResourceResult, unloadPCMAudio, PURGE_NEVER, resourceManager); ResourceManager_addTypeHandler(resourceManager, ATOM("sfxrXsound"), loadSfxrXSoundResource, unloadSFXRSoundResource, PURGE_NEVER, resourceManager); ResourceManager_addTypeHandler(resourceManager, ATOM("sfxrXsound_result"), loadSfxrXSoundResourceResult, unloadPCMAudio, PURGE_NEVER, resourceManager); AudioRecipe * recipe = ResourceManager_referenceResource(resourceManager, ATOM("audiolab_recipe"), recipeFile); if (recipe == NULL) { return EXIT_FAILURE; } if (outputFile == NULL) { outputFile = recipe->outputFile; } if (outputFormat == OUTPUT_FORMAT_UNSPECIFIED) { outputFormat = recipe->outputFormat; } if (outputFile == NULL && isTTY && !playOutput) { fprintf(stderr, "Error: outputFile is unspecified, and stdout appears to be a TTY; if you're sure you want raw audio data in your terminal output, specify `-o -` to write to stdout, or specify a path to write to a file.\n"); printUsage(); return EXIT_FAILURE; } if (outputFile != NULL && !strcmp(outputFile, "-")) { outputFile = NULL; } if (printOutputs) { if (outputFile != NULL) { write(STDOUT_FILENO, outputFile, strlen(outputFile)); } } else if (printPrereqs) { printRecipePrereqs(recipe, resourceManager); } if (printPrereqs || printOutputs) { if (isatty(STDOUT_FILENO)) { putchar('\n'); } return EXIT_SUCCESS; } if (outputFile != NULL) { if (outputFormat == OUTPUT_FORMAT_UNSPECIFIED) { const char * extension = getFileExtension(outputFile); if (!strcmp(extension, "wav")) { outputFormat = OUTPUT_FORMAT_WAV; } else if (!strcmp(extension, "ogg")) { outputFormat = OUTPUT_FORMAT_OGG; } } } if (outputFormat == OUTPUT_FORMAT_UNSPECIFIED && (!isTTY || !playOutput)) { fprintf(stderr, "Warning: Couldn't determine output format from file extension or --format argument; defaulting to wav\n"); outputFormat = OUTPUT_FORMAT_WAV; } PCMAudio * output = ResourceManager_referenceResource(resourceManager, ATOM("audiolab_recipe_result"), recipeFile); if (output == NULL) { return EXIT_FAILURE; } if (outputFile != NULL || !isTTY || !playOutput) { switch (outputFormat) { case OUTPUT_FORMAT_UNSPECIFIED: break; case OUTPUT_FORMAT_WAV: if (!WAVAudioIO_writeWAVFile(output, outputFile) && !playOutput) { return EXIT_FAILURE; } break; case OUTPUT_FORMAT_OGG: if (!VorbisAudioIO_writeOggVorbisFile(output, outputFile, VORBIS_ENCODE_QUALITY_DEFAULT) && !playOutput) { return EXIT_FAILURE; } break; } } if (isTTY && playOutput) { AudioOut_init(); AudioOut_setTransportFormat((AudioOut_sampleFormat) {output->channelCount, output->sampleRate, output->bytesPerSample, output->bytesPerSample == 32}); AudioOut_startOutput(audioOutCallback, output); msleep(output->frameCount * 1000 / output->sampleRate); AudioOut_stopOutput(); } return EXIT_SUCCESS; }