mirror of
https://github.com/mackron/miniaudio.git
synced 2026-06-29 17:42:42 +02:00
1440 lines
57 KiB
C
1440 lines
57 KiB
C
#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] [<global args>] \n"
|
|
" <command> [<args>] : <command> [<args>] : ... \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 <command> 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 <input> \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 <output> \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 <input file> : <output command> \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"\
|
|
" <input command> : encode <output file> \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 <output> \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 <signal.h>
|
|
|
|
#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;
|
|
}
|