#include "../../external/fs/fs.c" #include "../../miniaudio.c" #if defined(MA_CLI_ENABLE_LIBVORBIS) #include "../../extras/decoders/libvorbis/miniaudio_libvorbis.c" #endif #if defined(MA_CLI_ENABLE_LIBOPUS) #include "../../extras/decoders/libopus/miniaudio_libopus.c" #endif static const char* pHelpOverview = "USAGE: \n" " miniaudio [version | -v | --version] [help | -h | --help] [] \n" " [] : [] : ... \n" " \n" " The output of one command can be used as the input to another. Commands are\n" " executed left to right, and are separated with ':'. \n" " \n" " miniaudio help commands List all available commands. \n" " miniaudio help Print help for a specific command. \n" " \n" "EXAMPLES: \n" " miniaudio play sound.mp3 \n" " Play a sound from a file. \n" " \n" " miniaudio record output.wav \n" " Record to a file. \n" " \n" " miniaudio record : play \n" " Record and play back. \n" " \n" "OPTIONS: \n" " --backend [backend1,backend2,...] \n" " Sets the backend prioritization for playback and capture. Backends \n" " should be comma separated and not have spaces. Available backends: \n" " \n" #if defined(MA_HAS_WASAPI) " wasapi \n" #endif #if defined(MA_HAS_DSOUND) " directsound \n" #endif #if defined(MA_HAS_WINMM) " winmm \n" #endif #if defined(MA_HAS_COREAUDIO) " coreaudio \n" #endif #if defined(MA_HAS_PIPEWIRE) " pipewire \n" #endif #if defined(MA_HAS_PULSEAUDIO) " pulseaudio \n" #endif #if defined(MA_HAS_JACK) " jack \n" #endif #if defined(MA_HAS_ALSA) " alsa \n" #endif #if defined(MA_HAS_SNDIO) " sndio \n" #endif #if defined(MA_HAS_AUDIO4) " audio4 \n" #endif #if defined(MA_HAS_OSS) " oss \n" #endif #if defined(MA_HAS_AAUDIO) " aaudio \n" #endif #if defined(MA_HAS_OPENSL) " opensl \n" #endif #if defined(MA_HAS_WEBAUDIO) " webaudio \n" #endif #if defined(MA_HAS_DREAMCAST) " dreamcast \n" #endif #if defined(MA_HAS_XAUDIO) " xaudio \n" #endif #if defined(MA_HAS_VITA) " vita \n" #endif #if defined(MA_HAS_NULL) " null \n" #endif #if defined(MA_HAS_SDL2) " sdl2 \n" #endif " \n" " Example: --backend pulseaudio,pipewire \n" " \n" "ALLOWABLE SAMPLE FORMATS: \n" " u8 8-bit unsigned integer \n" " s16 16-bit signed integer \n" " s24 24-bit signed integer (tightly packed) \n" " s32 32-bit signed integer \n" " f32 32-bit floating point \n" ; #define MA_CLI_HELP_PLAY \ "USAGE: \n"\ " play \n"\ " \n"\ "DESCRIPTION: \n"\ " Plays a sound. If no inputs are specified, the sound will be read from the \n"\ " output of the previous command. \n"\ " \n"\ " Only a single play command is allowed, and there are no outputs which means\n"\ " it would typically always be the last command. \n"\ " \n"\ "EXAMPLES: \n"\ " miniaudio play sound.mp3 \n"\ " Play a sound from a file. \n"\ " \n"\ " miniaudio decode sound.mp3 : play \n"\ " Play a sound from a file. \n"\ " \n"\ " miniaudio waveform : play \n"\ " Play a sine wave. \n"\ " \n"\ " miniaudio record : play \n"\ " Capture from microphone and play back. \n"\ " \n"\ "OPTIONS: \n"\ " -f, --format [default: system default] \n"\ " The device sample format. \n"\ " \n"\ " -c, --channels [default: system default] \n"\ " The device channel count. \n"\ " \n"\ " -r, --rate [default: system default] \n"\ " The device sample rate. \n"\ #define MA_CLI_HELP_RECORD \ "USAGE: \n"\ " record \n"\ " \n"\ "DESCRIPTION: \n"\ " Records from a microphone. \n"\ " \n"\ "EXAMPLES: \n"\ " miniaudio record sound.wav \n"\ " Record a sound an output to a file. \n"\ " \n"\ " miniaudio record : encode sound.wav \n"\ " Record a sound an output to a file. \n"\ " \n"\ " miniaudio record : play \n"\ " Capture from microphone and play back. \n"\ " \n"\ "OPTIONS: \n"\ " -f, --format [default: system default] \n"\ " The device sample format. \n"\ " \n"\ " -c, --channels [default: system default] \n"\ " The device channel count. \n"\ " \n"\ " -r, --rate [default: system default] \n"\ " The device sample rate. \n"\ #define MA_CLI_HELP_DECODE \ "USAGE: \n"\ " decode : \n"\ " \n"\ "DESCRIPTION: \n"\ " Decodes a file. You would typically route the output to the input of \n"\ " another command. \n"\ " \n"\ "EXAMPLES: \n"\ " miniaudio decode sound.mp3 : play \n"\ " Decode a file and play it. \n"\ " \n"\ " miniaudio decode sound.mp3 : encode sound.wav \n"\ " Decode and re-encode a file. \n"\ " \n"\ "OPTIONS: \n"\ " -f, --format [default: file default] \n"\ " The output sample format. \n"\ " \n"\ " -c, --channels [default: file default] \n"\ " The output channel count. \n"\ " \n"\ " -r, --rate [default: file default] \n"\ " The output sample rate. \n"\ #define MA_CLI_HELP_ENCODE \ "USAGE: \n"\ " : encode \n"\ " \n"\ "DESCRIPTION: \n"\ " Encodes a file. You would typically route the output of another command to \n"\ " the input of this command. \n"\ " \n"\ "EXAMPLES: \n"\ " miniaudio record : encode sound.wav \n"\ " Capture audio from a microphone and then output to a file. \n"\ " \n"\ " miniaudio decode sound.mp3 : encode sound.wav \n"\ " Decode and re-encode a file. \n"\ " \n"\ "OPTIONS: \n"\ " -f, --format [default: file default] \n"\ " The output sample format. \n"\ " \n"\ " -c, --channels [default: file default] \n"\ " The output channel count. \n"\ " \n"\ " -r, --rate [default: file default] \n"\ " The output sample rate. \n"\ #define MA_CLI_HELP_WAVEFORM \ "USAGE: \n"\ " waveform \n"\ " \n"\ "DESCRIPTION: \n"\ " Generates a sine wave. \n"\ " \n"\ " This does not accept any inputs. \n"\ " \n"\ "OPTIONS: \n"\ " -f, --format [default: f32] \n"\ " The output sample format. \n"\ " \n"\ " -c, --channels [default: 1] \n"\ " The output channel count. \n"\ " \n"\ " -r, --rate [default: 48000] \n"\ " The output sample rate. \n"\ " \n"\ " --type [default: sine] \n"\ " The waveform type. Allowable values: \n"\ " sine \n"\ " square \n"\ " triangle \n"\ " sawtooth \n"\ " \n"\ " --amplitude [default: 0.1] \n"\ " Controls the volume of the output. A value of 1 will be quite harsh so \n"\ " take care when setting this. \n"\ " \n"\ " --frequency [default: 220] \n"\ " The frequency of the generated sine wave. Larger values will be higher \n"\ " pitched. \n"\ #include #define MA_CLI_DEFAULT_PERIOD_SIZE_IN_FRAMES 512 /* These should be in priority order. */ #if defined(MA_CLI_ENABLE_LIBVORBIS) #define MA_CLI_DECODING_BACKEND_LIBVORBIS ma_decoding_backend_libvorbis, #else #define MA_CLI_DECODING_BACKEND_LIBVORBIS #endif #if defined(MA_CLI_ENABLE_LIBOPUS) #define MA_CLI_DECODING_BACKEND_LIBOPUS ma_decoding_backend_libopus, #else #define MA_CLI_DECODING_BACKEND_LIBOPUS #endif #define MA_CLI_DECODING_BACKENDS \ MA_CLI_DECODING_BACKEND_LIBVORBIS \ MA_CLI_DECODING_BACKEND_LIBOPUS \ ma_decoding_backend_wav, \ ma_decoding_backend_flac, \ ma_decoding_backend_mp3 /* <-- MP3 seems to be worst/slowest at detecting whether or not it's a valid file so we'll put this last. */ static fs_file* MA_CLI_STDIN; static fs_file* MA_CLI_STDOUT; static fs_file* MA_CLI_STDERR; static ma_result ma_result_from_fs(fs_result result) { return (ma_result)result; } /* BEG ma_cli_args */ typedef struct { const char* pToken; size_t tokenLen; struct { int iarg; int argc; char** argv; } private; } ma_cli_args; ma_cli_args ma_cli_args_first(int argc, char** argv); ma_bool32 ma_cli_args_at_end(ma_cli_args* pArgs); ma_bool32 ma_cli_args_next(ma_cli_args* pArgs); ma_cli_args ma_cli_args_first(int argc, char** argv) { ma_cli_args args; MA_ZERO_OBJECT(&args); args.private.iarg = 0; args.private.argc = argc; args.private.argv = argv; ma_cli_args_next(&args); return args; } ma_bool32 ma_cli_args_at_end(ma_cli_args* pArgs) { if (pArgs == NULL) { return MA_TRUE; } return pArgs->private.iarg >= pArgs->private.argc; } ma_bool32 ma_cli_args_next(ma_cli_args* pArgs) { if (pArgs == NULL) { return MA_FALSE; } if (ma_cli_args_at_end(pArgs)) { return MA_FALSE; } /* Some setup for the first iteration. */ if (pArgs->pToken == NULL && pArgs->private.iarg == 0 && pArgs->private.argc > 0) { pArgs->pToken = pArgs->private.argv[0]; pArgs->tokenLen = 0; } if (pArgs->pToken == NULL) { return MA_FALSE; } pArgs->pToken += pArgs->tokenLen; if (pArgs->pToken[0] == '\0') { /* Reached the end of the token. Move to the next argument. */ pArgs->private.iarg += 1; if (ma_cli_args_at_end(pArgs)) { return MA_FALSE; } pArgs->pToken = pArgs->private.argv[pArgs->private.iarg]; } /* For now we are just treating each whole argument as a single token, but later on I want to make it so each argument can be split up into parts so we can avoid excessive space separations. This can come later once things has stabilized a bit. */ pArgs->tokenLen = strlen(pArgs->pToken); return MA_TRUE; } ma_bool32 ma_cli_args_equal(ma_cli_args* pArgs, const char* pName) { return fs_strncmp(pName, pArgs->pToken, pArgs->tokenLen) == 0; } /* END ma_cli_args */ static void ma_cli_process_node_graph(ma_node_graph* pNodeGraph); #include "miniaudio_cli_node.c" typedef struct { ma_cli_node_vtable* pVTable; ma_uint32 inputCount; ma_uint32 outputCount; /* Set this to ~0 if the output count is variable, such as a splitter. */ const char* pName; const char* pSummary; const char* pHelp; } ma_cli_command; static ma_cli_command pCommands[] = { { &ma_gNodeVTable_Play, 1, 0, "play", "Plays a sound.", MA_CLI_HELP_PLAY }, { &ma_gNodeVTable_Record, 0, 1, "record", "Records from a microphone.", MA_CLI_HELP_RECORD }, { &ma_gNodeVTable_Decode, 0, 1, "decode", "Decode a sound.", MA_CLI_HELP_DECODE }, { &ma_gNodeVTable_Encode, 1, 0, "encode", "Encode a sound.", MA_CLI_HELP_ENCODE }, { &ma_gNodeVTable_Waveform, 0, 1, "waveform", "Generates a waveform tone.", MA_CLI_HELP_WAVEFORM } }; /* BEG ma_cli_node_list */ typedef struct { ma_cli_node* items[255]; ma_uint32 count; } ma_cli_node_list; MA_API ma_cli_node_list* ma_cli_node_list_push(ma_cli_node_list* pList, ma_cli_node* pNode) { if (pList == NULL) { pList = (ma_cli_node_list*)ma_calloc(sizeof(ma_cli_node_list), NULL); if (pList == NULL) { return NULL; } } if (pList->count >= ma_countof(pList->items)) { return NULL; /* Ran out of space. */ /* TODO: Just dynamically expand this. */ } pList->items[pList->count] = pNode; pList->count += 1; return pList; } MA_API ma_uint32 ma_cli_node_list_count(ma_cli_node_list* pList) { if (pList == NULL) { return 0; } return pList->count; } MA_API ma_cli_node* ma_cli_node_list_get(ma_cli_node_list* pList, ma_uint32 index) { if (pList == NULL) { return NULL; } return pList->items[index]; } /* END ma_cli_node_list */ /* Global program state. */ static ma_cli_node_list* g_pNodes = NULL; static ma_cli_play* g_ppPlayNodes[16]; static ma_uint32 g_playNodeCount = 0; static ma_cli_record* g_ppRecordNodes[16]; static ma_uint32 g_recordNodeCount = 0; static ma_device_backend_config g_backends[32]; static ma_uint32 g_backendCount = 0; static ma_result ma_cli_track_play_node(ma_cli_play* pPlay) { if (g_playNodeCount >= ma_countof(g_ppPlayNodes)) { fs_file_writef(MA_CLI_STDERR, "Too many play nodes. You can have a maximum of %d.\n", (int)ma_countof(g_ppPlayNodes)); return MA_INVALID_OPERATION; } g_ppPlayNodes[g_playNodeCount] = pPlay; g_playNodeCount += 1; return MA_SUCCESS; } static ma_uint32 ma_cli_get_play_node_count(void) { return g_playNodeCount; } static ma_cli_play* ma_cli_get_play_node(ma_uint32 index) { return g_ppPlayNodes[index]; } static ma_result ma_cli_track_record_node(ma_cli_record* pRecord) { if (g_recordNodeCount >= ma_countof(g_ppRecordNodes)) { fs_file_writef(MA_CLI_STDERR, "Too many record nodes. You can have a maximum of %d.\n", (int)ma_countof(g_ppRecordNodes)); return MA_INVALID_OPERATION; } g_ppRecordNodes[g_recordNodeCount] = pRecord; g_recordNodeCount += 1; return MA_SUCCESS; } static ma_uint32 ma_cli_get_record_node_count(void) { return g_recordNodeCount; } static ma_cli_record* ma_cli_get_record_node(ma_uint32 index) { return g_ppRecordNodes[index]; } static ma_result ma_cli_track_node(ma_cli_node* pNode) { g_pNodes = ma_cli_node_list_push(g_pNodes, pNode); if (g_pNodes == NULL) { return MA_OUT_OF_MEMORY; } if (pNode->pVTable == &ma_gNodeVTable_Play) { return ma_cli_track_play_node((ma_cli_play*)pNode); } if (pNode->pVTable == &ma_gNodeVTable_Record) { return ma_cli_track_record_node((ma_cli_record*)pNode); } return MA_SUCCESS; } static ma_uint32 ma_cli_get_node_count(void) { return ma_cli_node_list_count(g_pNodes); } static ma_cli_node* ma_cli_get_node(ma_uint32 index) { return ma_cli_node_list_get(g_pNodes, index); } static ma_cli_node* ma_cli_parse_command(ma_cli_args* pArgs, ma_cli_node* pPreviousNode); static void ma_cli_version(void) { /* The CLI version is just the version of miniaudio itself. */ fs_file_writef(MA_CLI_STDOUT, "miniaudio v%d.%d.%d\n", MA_VERSION_MAJOR, MA_VERSION_MINOR, MA_VERSION_REVISION); } static void ma_cli_help_overview(void) { ma_cli_version(); fs_file_writef(MA_CLI_STDOUT, "\n%s\n", pHelpOverview); /* Now print our decoding backends. */ fs_file_writef(MA_CLI_STDOUT, "AVAILABLE DECODING BACKENDS:\n"); { size_t iDecodingBackend; ma_decoding_backend_vtable* pDecodingBackends[] = { MA_CLI_DECODING_BACKENDS }; for (iDecodingBackend = 0; iDecodingBackend < ma_countof(pDecodingBackends); iDecodingBackend += 1) { if (pDecodingBackends[iDecodingBackend] != NULL && pDecodingBackends[iDecodingBackend]->onInfo != NULL) { ma_decoding_backend_info backendInfo; pDecodingBackends[iDecodingBackend]->onInfo(NULL, &backendInfo); fs_file_writef(MA_CLI_STDOUT, " %s (via %s)\n", backendInfo.pName, backendInfo.pLibraryName); } } } } static void ma_cli_help_command(const char* pOpName) { size_t iCommand; for (iCommand = 0; iCommand < ma_countof(pCommands); iCommand += 1) { if (strcmp(pCommands[iCommand].pName, pOpName) == 0) { fs_file_writef(MA_CLI_STDOUT, "%s\n", pCommands[iCommand].pHelp); break; } } } static void ma_cli_help_commands(void) { size_t iCommand; for (iCommand = 0; iCommand < ma_countof(pCommands); iCommand += 1) { fs_file_writef(MA_CLI_STDOUT, "%-20s%s\n", pCommands[iCommand].pName, pCommands[iCommand].pSummary); } } static void ma_cli_help(int argc, char** argv) { if (argc > 2) { int iarg; for (iarg = 2; iarg < argc; iarg += 1) { if (strcmp(argv[iarg], "commands") == 0) { ma_cli_help_commands(); } else { ma_cli_help_command(argv[iarg]); } } } else { ma_cli_help_overview(); } } typedef struct ma_cli_backend_name_map { const char* pName; ma_device_backend_vtable* pVTable; } ma_cli_backend_name_map; static ma_device_backend_vtable* ma_cli_backend_vtable_from_string(const char* pName, size_t nameLen) { size_t i; ma_cli_backend_name_map backends[] = { #if defined(MA_HAS_WASAPI) { "wasapi", ma_device_backend_wasapi }, #endif #if defined(MA_HAS_DSOUND) { "directsound", ma_device_backend_dsound }, #endif #if defined(MA_HAS_WINMM) { "winmm", ma_device_backend_winmm }, #endif #if defined(MA_HAS_COREAUDIO) { "coreaudio", ma_device_backend_coreaudio }, #endif #if defined(MA_HAS_PIPEWIRE) { "pipewire", ma_device_backend_pipewire }, #endif #if defined(MA_HAS_PULSEAUDIO) { "pulseaudio", ma_device_backend_pulseaudio }, #endif #if defined(MA_HAS_JACK) { "jack", ma_device_backend_jack }, #endif #if defined(MA_HAS_ALSA) { "alsa", ma_device_backend_alsa }, #endif #if defined(MA_HAS_SNDIO) { "sndio", ma_device_backend_sndio }, #endif #if defined(MA_HAS_AUDIO4) { "audio4", ma_device_backend_audio4 }, #endif #if defined(MA_HAS_OSS) { "oss", ma_device_backend_oss }, #endif #if defined(MA_HAS_AAUDIO) { "aaudio", ma_device_backend_aaudio }, #endif #if defined(MA_HAS_OPENSL) { "opensl", ma_device_backend_opensl }, #endif #if defined(MA_HAS_WEBAUDIO) { "webaudio", ma_device_backend_webaudio }, #endif #if defined(MA_HAS_DREAMCAST) { "dreamcast", ma_device_backend_dreamcast }, #endif #if defined(MA_HAS_XAUDIO) { "xaudio", ma_device_backend_xaudio }, #endif #if defined(MA_HAS_VITA) { "vita", ma_device_backend_vita }, #endif #if defined(MA_HAS_NULL) { "null", ma_device_backend_null } #endif }; for (i = 0; i < sizeof(backends)/sizeof(backends[0]); i += 1) { if (fs_strncmp(backends[i].pName, pName, nameLen) == 0 && backends[i].pName[nameLen] == '\0') { return backends[i].pVTable; } } return NULL; } static ma_bool32 ma_cli_parse_backends(const char* pList) { const char* pStart; const char* pEnd; g_backendCount = 0; pStart = pList; for (;;) { size_t nameLen; ma_device_backend_vtable* pVTable; pEnd = pStart; while (*pEnd != ',' && *pEnd != '\0') { pEnd += 1; } nameLen = (size_t)(pEnd - pStart); if (nameLen > 0) { if (g_backendCount >= ma_countof(g_backends)) { fs_file_writef(MA_CLI_STDERR, "Too many backends specified. Maximum is %d.\n", (int)ma_countof(g_backends)); return MA_FALSE; } pVTable = ma_cli_backend_vtable_from_string(pStart, nameLen); if (pVTable == NULL) { fs_file_writef(MA_CLI_STDERR, "Unknown or unsupported backend \"%.*s\".\n", (int)nameLen, pStart); return MA_FALSE; } g_backends[g_backendCount] = ma_device_backend_config_init(pVTable, NULL); g_backendCount += 1; } if (*pEnd == '\0') { break; } pStart = pEnd + 1; } return MA_TRUE; } static ma_format ma_cli_format_from_string(const char* pFormat, size_t formatLen) { if (fs_strncmp("f32", pFormat, formatLen) == 0) { return ma_format_f32; } if (fs_strncmp("s16", pFormat, formatLen) == 0) { return ma_format_s16; } if (fs_strncmp("s32", pFormat, formatLen) == 0) { return ma_format_s32; } if (fs_strncmp("s24", pFormat, formatLen) == 0) { return ma_format_s24; } if (fs_strncmp("u8", pFormat, formatLen) == 0) { return ma_format_u8; } return ma_format_unknown; } static ma_cli_node* ma_cli_create_node(void* pConfig) { ma_result result; ma_cli_node* pNode; pNode = ma_cli_node_create(pConfig); if (pNode == NULL) { return NULL; } result = ma_cli_track_node(pNode); if (result != MA_SUCCESS) { ma_cli_node_delete(pNode); return NULL; } return pNode; } static void ma_cli_delete_node(ma_cli_node* pNode) { ma_cli_node_delete(pNode); } static ma_bool32 ma_cli_try_parse_format(ma_cli_args* pArgs, ma_format* pFormat) { if (ma_cli_args_equal(pArgs, "-f") || ma_cli_args_equal(pArgs, "--format")) { if (ma_cli_args_next(pArgs)) { *pFormat = ma_cli_format_from_string(pArgs->pToken, pArgs->tokenLen); if (*pFormat == ma_format_unknown) { fs_file_writef(MA_CLI_STDERR, "Unknown format \"%.*s\".\n", (int)pArgs->tokenLen, pArgs->pToken); return MA_FALSE; } } else { fs_file_writef(MA_CLI_STDERR, "Expecting format after \"%.*s\".\n", (int)pArgs->tokenLen, pArgs->pToken); return MA_FALSE; } } else { /* Not a format switch. This is not an error - it's just ignored. */ } return MA_TRUE; } static ma_bool32 ma_cli_try_parse_channels(ma_cli_args* pArgs, ma_uint32* pChannels) { if (ma_cli_args_equal(pArgs, "-c") || ma_cli_args_equal(pArgs, "--channels")) { if (ma_cli_args_next(pArgs)) { char temp[64]; ma_strncpy_s(temp, sizeof(temp), pArgs->pToken, pArgs->tokenLen); *pChannels = atoi(temp); } else { fs_file_writef(MA_CLI_STDERR, "Expecting channel count after \"%.*s\".\n", (int)pArgs->tokenLen, pArgs->pToken); return MA_FALSE; } } else { /* Not a format switch. This is not an error - it's just ignored. */ } return MA_TRUE; } static ma_bool32 ma_cli_try_parse_rate(ma_cli_args* pArgs, ma_uint32* pSampleRate) { if (ma_cli_args_equal(pArgs, "-r") || ma_cli_args_equal(pArgs, "--rate")) { if (ma_cli_args_next(pArgs)) { char temp[64]; ma_strncpy_s(temp, sizeof(temp), pArgs->pToken, pArgs->tokenLen); *pSampleRate = atoi(temp); } else { fs_file_writef(MA_CLI_STDERR, "Expecting sample rate after \"%.*s\".\n", (int)pArgs->tokenLen, pArgs->pToken); return MA_FALSE; } } else { /* Not a format switch. This is not an error - it's just ignored. */ } return MA_TRUE; } static ma_cli_node* ma_cli_parse_command(ma_cli_args* pArgs, ma_cli_node* pPreviousNode) { ma_cli_command* pCommand = NULL; size_t iCommand; ma_cli_node_config nodeConfig; ma_cli_node* pNode = NULL; const char* pCommandName; size_t commandNameLen; ma_cli_args firstArgs = *pArgs; ma_cli_node_list* pInputNodes = NULL; ma_cli_node_list* pOutputNodes = NULL; ma_bool32 areArgNodesInputs = MA_TRUE; ma_uint32 iNode; const char* pEncoderFilePath = NULL; size_t encoderFilePathLen = 0; pCommandName = pArgs->pToken; commandNameLen = pArgs->tokenLen; for (iCommand = 0; iCommand < ma_countof(pCommands); iCommand += 1) { if (ma_cli_args_equal(pArgs, pCommands[iCommand].pName)) { pCommand = &pCommands[iCommand]; } } if (pCommand == NULL) { fs_file_writef(MA_CLI_STDOUT, "Unknown command: \"%.*s\".\n", (int)pArgs->tokenLen, pArgs->pToken); return NULL; } if (pPreviousNode != NULL) { pInputNodes = ma_cli_node_list_push(pInputNodes, pPreviousNode); } if (pCommand->inputCount == 0 || pCommand->outputCount > 1) { areArgNodesInputs = MA_FALSE; } nodeConfig = ma_cli_node_config_init_default(pCommand->pVTable); nodeConfig.pBackends = g_backends; nodeConfig.backendCount = g_backendCount; /* The current argument will be sitting on the command name. Skip past it. */ ma_cli_args_next(pArgs); /* Parse arguments. */ for (;;) { ma_bool32 hasArgBeenParsed = MA_FALSE; if (ma_cli_args_at_end(pArgs) || ma_cli_args_equal(pArgs, ":") || ma_cli_args_equal(pArgs, "]")) { break; } if (!ma_cli_try_parse_format (pArgs, &nodeConfig.format )) { return NULL; } if (!ma_cli_try_parse_channels(pArgs, &nodeConfig.channels )) { return NULL; } if (!ma_cli_try_parse_rate (pArgs, &nodeConfig.sampleRate)) { return NULL; } if (pArgs->tokenLen >= 2 && pArgs->pToken[0] == '-') { if (pCommand->pVTable->onArg != NULL) { if (!pCommand->pVTable->onArg(pArgs, &nodeConfig)) { fs_file_writef(MA_CLI_STDERR, "Unknown argument '%.*s' for command '%.*s'.\n", (int)pArgs->tokenLen, pArgs->pToken, (int)commandNameLen, pCommandName); return NULL; } } } else { /* It's not a conventional `--arg` or `-arg` style argument. */ if (ma_cli_args_equal(pArgs, "[")) { /* It's either an input node or an output node. When it's an input node we want to parse it here so that this node can possibly infer it's format/channels/rate. If it's an output node, it needs to be parsed in a second pass so that they can infer this nodes format/channels/rate, which is only fully known after the node has been initialized. */ if (areArgNodesInputs) { if (ma_cli_args_next(pArgs)) { ma_node* pInputNode = ma_cli_parse_command(pArgs, NULL); if (pInputNode == NULL) { return NULL; /* ma_cli_parse_command() will have posted any error messages. */ } pInputNodes = ma_cli_node_list_push(pInputNodes, pInputNode); } else { fs_file_writef(MA_CLI_STDERR, "Expecting command after \"[\".\n"); return NULL; } } else { /* It's an output node. This needs to be parsed in a second pass. Just skip over this one for now. */ int depth = 0; for (;;) { if (ma_cli_args_at_end(pArgs) || ma_cli_args_equal(pArgs, ":") || ma_cli_args_equal(pArgs, "]")) { break; } /* */ if (ma_cli_args_equal(pArgs, "[")) { depth += 1; } else if (ma_cli_args_equal(pArgs, "]")) { depth -= 1; } else { /* It's an argument inside []. Skip it for now. */ } if (depth == 0) { break; } ma_cli_args_next(pArgs); } } } else { /* Could be a file path. If the node if input-only we wire up a decode node. If it's an output-only node, we wire up a decode node. */ /* Only encode and decode nodes have special handling of file paths. */ if (pCommand->pVTable == &ma_gNodeVTable_Decode || pCommand->pVTable == &ma_gNodeVTable_Encode) { nodeConfig.pFilePath = pArgs->pToken; nodeConfig.filePathLen = pArgs->tokenLen; } else { if (pCommand->outputCount == 0) { /* Need to wire up a decoder as an input node. */ ma_cli_node* pDecodeNode = NULL; ma_cli_node_config decodeConfig; decodeConfig = ma_cli_node_config_init_default(&ma_gNodeVTable_Decode); decodeConfig.pFilePath = pArgs->pToken; decodeConfig.filePathLen = pArgs->tokenLen; pDecodeNode = ma_cli_create_node(&decodeConfig); if (pDecodeNode == NULL) { fs_file_writef(MA_CLI_STDERR, "Failed to create decoder for '%.*s'.\n", (int)decodeConfig.filePathLen, decodeConfig.pFilePath); return NULL; } pInputNodes = ma_cli_node_list_push(pInputNodes, pDecodeNode); if (pInputNodes == NULL) { fs_file_writef(MA_CLI_STDERR, "Failed to push child node.\n"); return NULL; } } else if (pCommand->inputCount == 0) { /* Need to wire up an encoder as an output node, but it needs to be delay till after this node has been initialized. */ pEncoderFilePath = pArgs->pToken; encoderFilePathLen = pArgs->tokenLen; } } } } ma_cli_args_next(pArgs); } /* If the node takes inputs, infer the format/channels/rate from the first input, if there is one. */ if (pInputNodes != NULL && pInputNodes->count > 0) { /* TODO: Format. This requires support from the node graph itself. */ if (nodeConfig.channels == 0) { nodeConfig.channels = ma_cli_node_get_output_channels(pInputNodes->items[0], 0); } if (nodeConfig.sampleRate == 0) { nodeConfig.sampleRate = ma_cli_node_get_output_sample_rate(pInputNodes->items[0]); } } /* We should now have enough information to create the node. Inputs and outputs will be attached next. */ pNode = ma_cli_create_node(&nodeConfig); if (pNode == NULL) { fs_file_writef(MA_CLI_STDERR, "Failed to create '%.*s' node.\n", (int)commandNameLen, pCommandName); return NULL; } /* We now need to parse parameters for output nodes (those in square brackets). */ if (!areArgNodesInputs) { *pArgs = firstArgs; for (;;) { if (ma_cli_args_at_end(pArgs) || ma_cli_args_equal(pArgs, ":") || ma_cli_args_equal(pArgs, "]")) { break; } if (ma_cli_args_equal(pArgs, "[")) { if (ma_cli_args_next(pArgs)) { ma_node* pOutputNode = ma_cli_parse_command(pArgs, pNode); /* TODO: Do something with the output node? */ (void)pOutputNode; } else { fs_file_writef(MA_CLI_STDERR, "Expecting command after \"[\".\n"); return NULL; } } ma_cli_args_next(pArgs); } } /* Wire up all the inputs. */ if (pInputNodes != NULL) { for (iNode = 0; iNode < pInputNodes->count; iNode += 1) { ma_cli_node* pInputNode = pInputNodes->items[iNode]; ma_uint32 outputChannels; ma_uint32 outputSampleRate; ma_uint32 inputChannels; ma_uint32 inputSampleRate; outputChannels = ma_cli_node_get_output_channels(pInputNode, 0); outputSampleRate = ma_cli_node_get_output_sample_rate(pInputNode); inputChannels = ma_cli_node_get_input_channels(pNode, 0); inputSampleRate = ma_cli_node_get_output_sample_rate(pNode); /* *Output* sample rate is correct here. */ if (outputChannels != inputChannels || outputSampleRate != inputSampleRate) { /* Converter necessary. */ ma_cli_converter_config converterConfig; ma_cli_node* pConverterNode; converterConfig = ma_cli_converter_config_init(inputChannels, outputChannels, inputSampleRate, outputSampleRate); pConverterNode = ma_cli_create_node(&converterConfig); if (pConverterNode == NULL) { fs_file_writef(MA_CLI_STDERR, "Failed to create converter.\n"); return NULL; } /* Now the converter needs to be wired up and swapped out in the child list. */ ma_cli_node_attach_output(pInputNode, 0, pConverterNode, 0); /* Swap the child for the converter in the child list. This ensures the converter node is attached as the input to the main node. */ pInputNodes->items[iNode] = pConverterNode; pInputNode = pInputNodes->items[iNode]; } else { /* Converter not necessary. */ } ma_cli_node_attach_output(pInputNode, 0, pNode, 0); } } /* If we need an encoder, wire that up now. The encoder node will become the return value, and it will not allow outputs beyond it. */ if (pEncoderFilePath != NULL) { ma_cli_node_config encodeConfig; ma_node* pEncodeNode; encodeConfig = ma_cli_node_config_init_default(&ma_gNodeVTable_Encode); encodeConfig.pFilePath = pEncoderFilePath; encodeConfig.filePathLen = encoderFilePathLen; pEncodeNode = ma_cli_create_node(&encodeConfig); if (pEncodeNode != NULL) { fs_file_writef(MA_CLI_STDERR, "Failed to create encoder for '%.*s'.\n", (int)encoderFilePathLen, pEncoderFilePath); return NULL; /* Failed to create encode node. */ } ma_cli_node_attach_output(pNode, 0, pEncodeNode, 0); pNode = pEncodeNode; } return pNode; } #if defined(_WIN32) /* TODO: Win32 SIGINT handler. */ static volatile LONG g_shouldQuit = 0; static BOOL WINAPI ma_cli_handle_sigint(DWORD type) { switch (type) { case CTRL_C_EVENT: case CTRL_BREAK_EVENT: { InterlockedExchange(&g_shouldQuit, 1); return TRUE; } default: return FALSE; } } static ma_bool32 ma_cli_wants_to_quit(void) { return InterlockedCompareExchange(&g_shouldQuit, 0, 0) != 0; } #else static volatile sig_atomic_t g_shouldQuit = 0; static void ma_cli_handle_sigint(int signum) { (void)signum; g_shouldQuit = 1; } static ma_bool32 ma_cli_wants_to_quit(void) { return g_shouldQuit != 0; } #endif static void ma_cli_process_node_graph(ma_node_graph* pNodeGraph) { float temp[4096]; /* TODO: Make this a heap allocation for robustness. */ ma_uint32 frameCount = ma_node_graph_get_processing_size_in_frames(pNodeGraph); ma_uint64 framesRead; ma_result result; MA_ASSERT(frameCount <= ma_countof(temp)/ma_node_graph_get_channels(pNodeGraph)); result = ma_node_graph_read_pcm_frames(pNodeGraph, temp, frameCount, &framesRead); if (result != MA_SUCCESS || framesRead == 0) { g_shouldQuit = 1; } } int main(int argc, char** argv) { ma_result result; int iarg; int iFirstNodeArg; ma_cli_args args; ma_cli_node* pLastNode = NULL; ma_uint32 iNode; ma_uint32 iPlayNode; ma_uint32 iRecordNode; ma_cli_node* pEndpointNode = NULL; ma_node_graph_config nodeGraphConfig; ma_node_graph nodeGraph; /* We want to handle SIGINT so we can do cleanup and close encoders. This is relevant for when one of the input sources is a microphone which has no end point. */ #if defined(_WIN32) { if (!SetConsoleCtrlHandler(ma_cli_handle_sigint, TRUE)) { printf("SetConsoleCtrlHandler() failed."); return 1; } } #else { struct sigaction sa; sigemptyset(&sa.sa_mask); sa.sa_handler = ma_cli_handle_sigint; sa.sa_flags = 0; if (sigaction(SIGINT, &sa, NULL) != 0) { printf("sigaction() failed."); return 1; } } #endif fs_file_open(NULL, FS_STDIN, FS_READ, &MA_CLI_STDIN); fs_file_open(NULL, FS_STDOUT, FS_WRITE, &MA_CLI_STDOUT); fs_file_open(NULL, FS_STDOUT, FS_WRITE, &MA_CLI_STDERR); if (argc < 2) { ma_cli_help_overview(); return 1; } if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) { ma_cli_help(argc, argv); return 0; } if (strcmp(argv[1], "version") == 0 || strcmp(argv[1], "-v") == 0 || strcmp(argv[1], "--version") == 0) { ma_cli_version(); return 0; } /* Parse global options before building the command graph. */ iFirstNodeArg = 1; for (iarg = 1; iarg < argc; iarg += 1) { if (strcmp(argv[iarg], "--backend") == 0) { iarg += 1; if (iarg >= argc) { fs_file_writef(MA_CLI_STDERR, "Expecting backend list after \"--backend\".\n"); return 1; } if (!ma_cli_parse_backends(argv[iarg])) { return 1; } iFirstNodeArg = iarg + 1; /* +1 because the first argument is "--backend" and the second is the backend list. */ } } /* Build the graph from the arguments. */ for (args = ma_cli_args_first(argc - iFirstNodeArg, argv + iFirstNodeArg); !ma_cli_args_at_end(&args); ma_cli_args_next(&args)) { ma_cli_node* pNode = NULL; pNode = ma_cli_parse_command(&args, pLastNode); if (pNode == NULL) { return 1; /* Parsing failed. A relevant message will have already been printed so no need to print a message here. */ } /* The returned node needs to become the input of the next node. */ pLastNode = pNode; /*printf("arg: %.*s\n", (int)args.tokenLen, args.pToken);*/ } /* In order for our nodes to actually get processed we need to make sure everything is connected to the endpoint. To do this we can just create a single endpoint node, with one input for each unique channel count. We just look at nodes that do not have their outputs connected to anything. */ { ma_cli_endpoint_config endpointConfig; ma_uint32 inputChannels[MA_MAX_NODE_INPUT_COUNT]; ma_uint32 iInputChannels; ma_uint32 iOutput; MA_ZERO_MEMORY(inputChannels, sizeof(inputChannels)); endpointConfig = ma_cli_endpoint_config_init(); endpointConfig.inputCount = 0; /* Will be updated below. */ endpointConfig.pInputChannelCounts = inputChannels; /* Determine the unique channel counts and input count. */ for (iNode = 0; iNode < ma_cli_get_node_count(); iNode += 1) { ma_cli_node* pNode = ma_cli_get_node(iNode); for (iOutput = 0; iOutput < ma_cli_node_get_output_count(pNode); iOutput += 1) { if (pNode->pOutputs[iOutput].pInputNode == NULL) { ma_uint32 channels = ma_cli_node_get_output_channels(pNode, iOutput); for (iInputChannels = 0; iInputChannels < endpointConfig.inputCount; iInputChannels += 1) { if (inputChannels[iInputChannels] == channels) { break; } } if (iInputChannels == endpointConfig.inputCount) { inputChannels[iInputChannels] = channels; endpointConfig.inputCount += 1; } } } } pEndpointNode = ma_cli_create_node(&endpointConfig); if (pEndpointNode == NULL) { fs_file_writef(MA_CLI_STDERR, "Failed to create endpoint node.\n"); return 1; } /* Wire up the inputs. */ for (iNode = 0; iNode < ma_cli_get_node_count(); iNode += 1) { ma_cli_node* pNode = ma_cli_get_node(iNode); if (pNode != pEndpointNode) { for (iOutput = 0; iOutput < ma_cli_node_get_output_count(pNode); iOutput += 1) { if (pNode->pOutputs[iOutput].pInputNode == NULL) { ma_uint32 channels = ma_cli_node_get_output_channels(pNode, iOutput); for (iInputChannels = 0; iInputChannels < endpointConfig.inputCount; iInputChannels += 1) { if (inputChannels[iInputChannels] == channels) { break; } } if (iInputChannels > ma_cli_node_get_input_count(pEndpointNode)) { fs_file_writef(MA_CLI_STDERR, "Could not attach node to endpoint.\n"); return 1; } ma_cli_node_attach_output(pNode, iOutput, pEndpointNode, iInputChannels); } } } } } /* Now we need to build the miniaudio node graph. First we need a node graph. */ nodeGraphConfig = ma_node_graph_config_init(ma_cli_node_get_output_channels(pEndpointNode, 0)); if (ma_cli_get_play_node_count() > 0) { nodeGraphConfig.processingSizeInFrames = ma_device_get_period_size_in_frames(ma_cli_play_get_device(ma_cli_get_play_node(0))); } else { nodeGraphConfig.processingSizeInFrames = MA_CLI_DEFAULT_PERIOD_SIZE_IN_FRAMES; } result = ma_node_graph_init(&nodeGraphConfig, NULL, &nodeGraph); if (result != MA_SUCCESS) { fs_file_writef(MA_CLI_STDERR, "Failed to initialize node graph.\n"); return 1; } /* Now that we have the graph we can initialize the base nodes. */ for (iNode = 0; iNode < ma_cli_get_node_count(); iNode += 1) { result = ma_cli_node_init_base_node(ma_cli_get_node(iNode), &nodeGraph); if (result != MA_SUCCESS) { fs_file_writef(MA_CLI_STDERR, "Failed to initialize base node.\n"); return 1; } } /* Finally we can wire up the inputs and outputs. After this the node graph has been set up and we can start processing. */ for (iNode = 0; iNode < ma_cli_get_node_count(); iNode += 1) { ma_uint32 iOutput; ma_cli_node* pNode; pNode = ma_cli_get_node(iNode); MA_ASSERT(pNode != NULL); for (iOutput = 0; iOutput < ma_cli_node_get_output_count(pNode); iOutput += 1) { if (pNode->pOutputs[iOutput].pInputNode != NULL) { /* <-- Can be null for the endpoint. Not an error. */ ma_node_attach_output_bus(&pNode->baseNode, iOutput, &pNode->pOutputs[iOutput].pInputNode->baseNode, pNode->pOutputs[iOutput].inputNodeInputIndex); } } } /* Our endpoint needs to be attached to the miniaudio endpoint. */ ma_node_attach_output_bus(&pEndpointNode->baseNode, 0, ma_node_graph_get_endpoint(&nodeGraph), 0); /* Now we need to process. Any playback and capture devices need to be started. */ for (iRecordNode = 0; iRecordNode < ma_cli_get_record_node_count(); iRecordNode += 1) { ma_cli_record* pRecord = ma_cli_get_record_node(iRecordNode); MA_ASSERT(pRecord != NULL); result = ma_device_start(ma_cli_record_get_device(pRecord)); if (result != MA_SUCCESS) { fs_file_writef(MA_CLI_STDERR, "Failed to start device.\n"); return 1; } } for (iPlayNode = 0; iPlayNode < ma_cli_get_play_node_count(); iPlayNode += 1) { ma_cli_play* pPlay = ma_cli_get_play_node(iPlayNode); MA_ASSERT(pPlay != NULL); result = ma_device_start(ma_cli_play_get_device(pPlay)); if (result != MA_SUCCESS) { fs_file_writef(MA_CLI_STDERR, "Failed to start device.\n"); return 1; } } /* If we have a play or record node, the first one needs to be responsible for doing the node graph processing. */ if (ma_cli_get_record_node_count() > 0) { ma_cli_record_set_node_graph(ma_cli_get_record_node(0), &nodeGraph); } else if (ma_cli_get_play_node_count() > 0) { ma_cli_play_set_node_graph(ma_cli_get_play_node(0), &nodeGraph); } /* We need to process in a different way depending on whether or not we are outputting to a device. */ while (!ma_cli_wants_to_quit()) { /* If we have any devices we need to step them. */ for (iRecordNode = 0; iRecordNode < ma_cli_get_record_node_count(); iRecordNode += 1) { ma_cli_record* pRecord = ma_cli_get_record_node(iRecordNode); MA_ASSERT(pRecord != NULL); #if 1 while (ma_device_step(ma_cli_record_get_device(pRecord), MA_BLOCKING_MODE_BLOCKING) == MA_SUCCESS) { /* If the device has been processed, get out. Otherwise keep waiting. */ if (pRecord->pNodeGraph != NULL || pRecord->cursor > 0) { break; } } #else ma_device_step(ma_cli_record_get_device(pRecord), MA_BLOCKING_MODE_BLOCKING); #endif } /* Process the node graph, but only if there are no play nodes. When a play node is present, node processing will be done by the first play node. */ if (ma_cli_get_play_node_count() == 0 && ma_cli_get_record_node_count() == 0) { ma_cli_process_node_graph(&nodeGraph); } /* Playback devices. */ for (iPlayNode = 0; iPlayNode < ma_cli_get_play_node_count(); iPlayNode += 1) { ma_cli_play* pPlay = ma_cli_get_play_node(iPlayNode); MA_ASSERT(pPlay != NULL); #if 1 while (ma_device_step(ma_cli_play_get_device(pPlay), MA_BLOCKING_MODE_BLOCKING) == MA_SUCCESS) { /* If the device has been processed, get out. Otherwise keep waiting. */ if (pPlay->pNodeGraph != NULL || pPlay->cursor > 0) { break; } } #else ma_device_step(ma_cli_play_get_device(pPlay), MA_BLOCKING_MODE_BLOCKING); #endif } } /* Delete every node in the graph. Needed to ensure encoders are uninitialized in particular. */ for (iNode = 0; iNode < ma_cli_get_node_count(); iNode += 1) { ma_cli_delete_node(ma_cli_get_node(iNode)); } return 0; }