diff --git a/examples/node_graph.c b/examples/node_graph.c new file mode 100644 index 00000000..44984c84 --- /dev/null +++ b/examples/node_graph.c @@ -0,0 +1,250 @@ +/* +This example shows how to use the node graph system. + +The node graph system can be used for doing complex mixing and effect processing. The idea is that +you have a number of nodes that are connected to each other to form a graph. At the end of the +graph is an endpoint which all nodes eventually connect to. + +A node is used to do some kind of processing on zero or more input streams and produce one or more +output streams. Each node can have a number of inputs and outputs. Each of these is called a bus in +miniaudio. Some nodes, particularly data source nodes, have no inputs and instead generate their +outputs dynamically. All nodes will have at least one output or else it'll be disconnected from the +graph and will never get processed. Each output bus of a node will be connected to an input bus of +another node, but they don't all need to connect to the same input node. For example, a splitter +node has 1 input bus and 2 output buses and is used to duplicate a signal. You could then branch +off and have one output bus connected to one input node and the other connected to a different +input node, and then have two different effects process for each of the duplicated branches. + +Any number of output buses can be connected to an input bus in which case the output buses will be +mixed before processing by the input node. This is how you would achieve the mixing part of the +node graph. + +This example will be using the following node graph set up: + + ``` + >>>>>>>>>>>>>>>>>>>>>> Data flows left to right >>>>>>>>>>>>>>>>>>>>>> + + +---------------+ +-----------------+ + | Data Source 1 =----+ +----------+ +----= Low Pass Filter =----+ + +---------------+ | | =----+ +-----------------+ | +----------+ + +----= Splitter | +----= ENDPOINT | + +---------------+ | | =----+ +-----------------+ | +----------+ + | Data Source 2 =----+ +----------+ +----= Echo / Delay =----+ + +---------------+ +-----------------+ + ``` + +This does not represent a realistic real-world scenario, but it demonstrates how to make use of +mixing, multiple outputs and multiple effects. + +The data source nodes are connected to the input of the splitter. They'll be mixed before being +processed by the splitter. The splitter has two output buses. In the graph above, one bus will be +routed to a low pass filter, whereas the other bus will be routed to an echo effect. Then, the +outputs of these two effects will be connected to the input bus of the endpoint. Because both of +the outputs are connected to the same input bus, they'll be mixed at that point. + +The two data sources at the start of the graph have no inputs. They'll instead generate their +output by reading from a data source. The data source in this case will be one `ma_decoder` for +each input file specified on the command line. + +You can also control the volume of an output bus. In this example, we set the volumes of the low +pass and echo effects so that one of them becomes more obvious than the other. + +When you want to read from the graph, you simply call `ma_node_graph_read_pcm_frames()`. +*/ +#define MINIAUDIO_IMPLEMENTATION +#include "../miniaudio.h" + +/* Data Format */ +#define FORMAT ma_format_f32 /* Must always be f32. */ +#define CHANNELS 2 +#define SAMPLE_RATE 48000 + +/* Effect Properties */ +#define LPF_BIAS 0.9f /* Higher values means more bias towards the low pass filter (the low pass filter will be more audible). Lower values means more bias towards the echo. Must be between 0 and 1. */ +#define LPF_CUTOFF_FACTOR 80 /* High values = more filter. */ +#define LPF_ORDER 8 +#define DELAY_IN_SECONDS 0.2f +#define DECAY 0.5f /* Volume falloff for each echo. */ + +typedef struct +{ + ma_data_source_node node; /* If you make this the first member, you can pass a pointer to this struct into any `ma_node_*` API and it will "Just Work". */ + ma_decoder decoder; +} sound_node; + +static ma_node_graph g_nodeGraph; +static ma_lpf_node g_lpfNode; +static ma_delay_node g_delayNode; +static ma_splitter_node g_splitterNode; +static sound_node* g_pSoundNodes; +static int g_soundNodeCount; + +void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) +{ + MA_ASSERT(pDevice->playback.channels == CHANNELS); + + /* + Hearing the output of the node graph is as easy as reading straight into the output buffer. You just need to + make sure you use a consistent data format or else you'll need to do your own conversion. + */ + ma_node_graph_read_pcm_frames(&g_nodeGraph, pOutput, frameCount, NULL); + + (void)pInput; /* Unused. */ +} + +int main(int argc, char** argv) +{ + int iarg; + ma_result result; + + /* We'll set up our nodes starting from the end and working our way back to the start. We'll need to set up the graph first. */ + { + ma_node_graph_config nodeGraphConfig = ma_node_graph_config_init(CHANNELS); + + result = ma_node_graph_init(&nodeGraphConfig, NULL, &g_nodeGraph); + if (result != MA_SUCCESS) { + printf("ERROR: Failed to initialize node graph."); + return -1; + } + } + + + /* Low Pass Filter. */ + { + ma_lpf_node_config lpfNodeConfig = ma_lpf_node_config_init(CHANNELS, SAMPLE_RATE, SAMPLE_RATE / LPF_CUTOFF_FACTOR, LPF_ORDER); + + result = ma_lpf_node_init(&g_nodeGraph, &lpfNodeConfig, NULL, &g_lpfNode); + if (result != MA_SUCCESS) { + printf("ERROR: Failed to initialize low pass filter node."); + return -1; + } + + /* Connect the output bus of the low pass filter node to the input bus of the endpoint. */ + ma_node_attach_output_bus(&g_lpfNode, 0, ma_node_graph_get_endpoint(&g_nodeGraph), 0); + + /* Set the volume of the low pass filter to make it more of less impactful. */ + ma_node_set_output_bus_volume(&g_lpfNode, 0, LPF_BIAS); + } + + + /* Echo / Delay. */ + { + ma_delay_node_config delayNodeConfig = ma_delay_node_config_init(CHANNELS, SAMPLE_RATE, (ma_uint32)(SAMPLE_RATE * DELAY_IN_SECONDS), DECAY); + + result = ma_delay_node_init(&g_nodeGraph, &delayNodeConfig, NULL, &g_delayNode); + if (result != MA_SUCCESS) { + printf("ERROR: Failed to initialize delay node."); + return -1; + } + + /* Connect the output bus fo the delay node to the input bus of the endpoint. */ + ma_node_attach_output_bus(&g_delayNode, 0, ma_node_graph_get_endpoint(&g_nodeGraph), 0); + + /* Set the volume of the delay filter to make it more of less impactful. */ + ma_node_set_output_bus_volume(&g_delayNode, 0, 1 - LPF_BIAS); + } + + + /* Splitter. */ + { + ma_splitter_node_config splitterNodeConfig = ma_splitter_node_config_init(CHANNELS); + + result = ma_splitter_node_init(&g_nodeGraph, &splitterNodeConfig, NULL, &g_splitterNode); + if (result != MA_SUCCESS) { + printf("ERROR: Failed to initialize splitter node."); + return -1; + } + + /* Connect output bus 0 to the input bus of the low pass filter node, and output bus 1 to the input bus of the delay node. */ + ma_node_attach_output_bus(&g_splitterNode, 0, &g_lpfNode, 0); + ma_node_attach_output_bus(&g_splitterNode, 1, &g_delayNode, 0); + } + + + /* Data sources. Ignore any that cannot be loaded. */ + g_pSoundNodes = (sound_node*)ma_malloc(sizeof(*g_pSoundNodes) * argc-1, NULL); + if (g_pSoundNodes == NULL) { + printf("Failed to allocate memory for sounds."); + return -1; + } + + g_soundNodeCount = 0; + for (iarg = 1; iarg < argc; iarg += 1) { + ma_decoder_config decoderConfig = ma_decoder_config_init(FORMAT, CHANNELS, SAMPLE_RATE); + + result = ma_decoder_init_file(argv[iarg], &decoderConfig, &g_pSoundNodes[g_soundNodeCount].decoder); + if (result == MA_SUCCESS) { + ma_data_source_node_config dataSourceNodeConfig = ma_data_source_node_config_init(&g_pSoundNodes[g_soundNodeCount].decoder); + + result = ma_data_source_node_init(&g_nodeGraph, &dataSourceNodeConfig, NULL, &g_pSoundNodes[g_soundNodeCount].node); + if (result == MA_SUCCESS) { + /* The data source node has been created successfully. Attach it to the splitter. */ + ma_node_attach_output_bus(&g_pSoundNodes[g_soundNodeCount].node, 0, &g_splitterNode, 0); + g_soundNodeCount += 1; + } else { + printf("WARNING: Failed to init data source node for sound \"%s\". Ignoring.", argv[iarg]); + ma_decoder_uninit(&g_pSoundNodes[g_soundNodeCount].decoder); + } + } else { + printf("WARNING: Failed to load sound \"%s\". Ignoring.", argv[iarg]); + } + } + + /* Everything has been initialized successfully so now we can set up a playback device so we can listen to the result. */ + { + ma_device_config deviceConfig; + ma_device device; + + deviceConfig = ma_device_config_init(ma_device_type_playback); + deviceConfig.playback.format = FORMAT; + deviceConfig.playback.channels = CHANNELS; + deviceConfig.sampleRate = SAMPLE_RATE; + deviceConfig.dataCallback = data_callback; + deviceConfig.pUserData = NULL; + + result = ma_device_init(NULL, &deviceConfig, &device); + if (result != MA_SUCCESS) { + printf("ERROR: Failed to initialize device."); + goto cleanup_graph; + } + + result = ma_device_start(&device); + if (result != MA_SUCCESS) { + ma_device_uninit(&device); + goto cleanup_graph; + } + + printf("Press Enter to quit...\n"); + getchar(); + + /* We're done. Clean up the device. */ + ma_device_uninit(&device); + } + + +cleanup_graph: + { + /* It's good practice to tear down the graph from the lowest level nodes first. */ + int iSound; + + /* Sounds. */ + for (iSound = 0; iSound < g_soundNodeCount; iSound += 1) { + ma_data_source_node_uninit(&g_pSoundNodes[iSound].node, NULL); + ma_decoder_uninit(&g_pSoundNodes[iSound].decoder); + } + + /* Splitter. */ + ma_splitter_node_uninit(&g_splitterNode, NULL); + + /* Echo / Delay */ + ma_delay_node_uninit(&g_delayNode, NULL); + + /* Low Pass Filter */ + ma_lpf_node_uninit(&g_lpfNode, NULL); + + /* Node Graph */ + ma_node_graph_uninit(&g_nodeGraph, NULL); + } + + return 0; +}