aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--include/recorder/audio_input.h66
-rw-r--r--meson.build1
-rw-r--r--src/main.cpp341
-rw-r--r--src/recorder/audio_input.c336
4 files changed, 455 insertions, 289 deletions
diff --git a/include/recorder/audio_input.h b/include/recorder/audio_input.h
new file mode 100644
index 0000000..f6cb87e
--- /dev/null
+++ b/include/recorder/audio_input.h
@@ -0,0 +1,66 @@
+#ifndef GSR_RECORDER_AUDIO_INPUT_H
+#define GSR_RECORDER_AUDIO_INPUT_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "../sound.h"
+
+#define GSR_AUDIO_INPUT_NAME_MAX_SIZE 256
+#define GSR_AUDIO_TRACK_NAME_MAX_SIZE 512
+
+typedef enum {
+ GSR_AUDIO_INPUT_TYPE_DEVICE,
+ GSR_AUDIO_INPUT_TYPE_APPLICATION
+} gsr_audio_input_type;
+
+typedef struct {
+ char name[GSR_AUDIO_INPUT_NAME_MAX_SIZE];
+ gsr_audio_input_type type;
+ bool inverted;
+} gsr_audio_input;
+
+typedef struct {
+ char track_name[GSR_AUDIO_TRACK_NAME_MAX_SIZE];
+ bool has_custom_name;
+ gsr_audio_input *items;
+ size_t num_items;
+ size_t capacity_items;
+} gsr_merged_audio_inputs;
+
+typedef struct {
+ gsr_merged_audio_inputs *items;
+ size_t num_items;
+ size_t capacity_items;
+} gsr_audio_input_tracks;
+
+typedef struct {
+ char name[GSR_AUDIO_INPUT_NAME_MAX_SIZE];
+} gsr_app_audio_name;
+
+typedef struct {
+ gsr_app_audio_name *items;
+ size_t num_items;
+ size_t capacity_items;
+} gsr_app_audio_names;
+
+bool gsr_app_audio_names_add(gsr_app_audio_names *self, const char *name);
+void gsr_app_audio_names_deinit(gsr_app_audio_names *self);
+
+/* Returns a |gsr_error| value. Parses one -a option value, which may contain multiple audio inputs separated by | */
+int gsr_merged_audio_inputs_parse(gsr_merged_audio_inputs *self, const char *str);
+bool gsr_merged_audio_inputs_add(gsr_merged_audio_inputs *self, const gsr_audio_input *audio_input);
+void gsr_merged_audio_inputs_deinit(gsr_merged_audio_inputs *self);
+
+/* Returns a |gsr_error| value. Parses every -a option value and validates that the audio devices exist */
+int gsr_audio_input_tracks_parse(gsr_audio_input_tracks *self, const char **audio_input_args, int num_audio_input_args, const gsr_audio_devices *audio_devices);
+bool gsr_audio_input_tracks_add(gsr_audio_input_tracks *self, const gsr_merged_audio_inputs *merged_audio_inputs);
+void gsr_audio_input_tracks_deinit(gsr_audio_input_tracks *self);
+
+bool gsr_audio_inputs_has_app_audio(const gsr_merged_audio_inputs *self);
+bool gsr_audio_inputs_should_use_amix(const gsr_merged_audio_inputs *self);
+bool gsr_audio_input_tracks_has_app_audio(const gsr_audio_input_tracks *self);
+bool gsr_audio_input_tracks_should_use_amix(const gsr_audio_input_tracks *self);
+/* Returns a |gsr_error| value. Warns about application audio names that don't match any running application */
+int gsr_audio_input_tracks_validate_app_audio(const gsr_audio_input_tracks *self, const gsr_app_audio_names *app_audio_names);
+
+#endif /* GSR_RECORDER_AUDIO_INPUT_H */
diff --git a/meson.build b/meson.build
index af5473d..c751f9e 100644
--- a/meson.build
+++ b/meson.build
@@ -38,6 +38,7 @@ src = [
'src/recorder/windowing.c',
'src/recorder/capture_source.c',
'src/recorder/capture_setup.c',
+ 'src/recorder/audio_input.c',
'src/egl.c',
'src/cuda.c',
'src/window_texture.c',
diff --git a/src/main.cpp b/src/main.cpp
index d64be78..1fb4d38 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -6,6 +6,7 @@ extern "C" {
#include "../include/recorder/windowing.h"
#include "../include/recorder/capture_source.h"
#include "../include/recorder/capture_setup.h"
+#include "../include/recorder/audio_input.h"
#include "../include/recorder/error.h"
#include "../include/capture/nvfbc.h"
#include "../include/capture/xcomposite.h"
@@ -78,23 +79,6 @@ extern "C" {
#define GSR_VERSION "unknown"
#endif
-enum class AudioInputType {
- DEVICE,
- APPLICATION
-};
-
-struct AudioInput {
- std::string name;
- AudioInputType type = AudioInputType::DEVICE;
- bool inverted = false;
-};
-
-struct MergedAudioInputs {
- std::optional<std::string> custom_name;
- std::string track_name;
- std::vector<AudioInput> audio_inputs;
-};
-
// TODO: If options are not supported then they are returned (allocated) in the options. This should be free'd.
// TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0
@@ -213,7 +197,7 @@ static void run_recording_saved_script_async(const char *script_file, const char
struct AudioDeviceData {
SoundDevice sound_device;
- AudioInput audio_input;
+ gsr_audio_input audio_input;
AVFilterContext *src_filter_ctx = nullptr;
AVFrame *frame = nullptr;
std::thread thread; // TODO: Instead of having a thread for each track, have one thread for all threads and read the data with non-blocking read
@@ -541,53 +525,6 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str
return true;
}
-static const gsr_audio_device* get_audio_device_by_name(const gsr_audio_devices *audio_devices, const char *name) {
- for(size_t i = 0; i < audio_devices->num_items; ++i) {
- if(strcmp(audio_devices->items[i].name, name) == 0)
- return &audio_devices->items[i];
- }
- return nullptr;
-}
-
-static bool parse_audio_input_arg_callback(const char *sub, size_t size, void *userdata) {
- MergedAudioInputs *result = (MergedAudioInputs*)userdata;
- if(size == 0)
- return true;
-
- AudioInput audio_input;
- audio_input.name.assign(sub, size);
-
- if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "name:")) {
- result->custom_name = audio_input.name.substr(5);
- } else if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "app:")) {
- audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 4);
- audio_input.type = AudioInputType::APPLICATION;
- audio_input.inverted = false;
- result->audio_inputs.push_back(std::move(audio_input));
- } else if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "app-inverse:")) {
- audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 12);
- audio_input.type = AudioInputType::APPLICATION;
- audio_input.inverted = true;
- result->audio_inputs.push_back(std::move(audio_input));
- } else if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "device:")) {
- audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 7);
- audio_input.type = AudioInputType::DEVICE;
- result->audio_inputs.push_back(std::move(audio_input));
- } else {
- audio_input.type = AudioInputType::DEVICE;
- result->audio_inputs.push_back(std::move(audio_input));
- }
-
- return true;
-}
-
-static MergedAudioInputs parse_audio_input_arg(const char *str) {
- MergedAudioInputs result;
- result.track_name = str;
- gsr_string_split(str, '|', parse_audio_input_arg_callback, &result);
- return result;
-}
-
static int init_filter_graph(AVCodecContext* audio_codec_context, AVFilterGraph** graph, AVFilterContext** sink, std::vector<AVFilterContext*>& src_filter_ctx, size_t num_sources) {
char ch_layout[64];
int err = 0;
@@ -1207,203 +1144,15 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win
_exit(should_stop_error ? 3 : 0);
}
-static void match_app_audio_input_to_available_apps(const std::vector<AudioInput> &requested_audio_inputs, const std::vector<std::string> &app_audio_names) {
- for(const AudioInput &request_audio_input : requested_audio_inputs) {
- if(request_audio_input.type != AudioInputType::APPLICATION || request_audio_input.inverted)
- continue;
-
- bool match = false;
- for(const std::string &app_name : app_audio_names) {
- if(strcasecmp(app_name.c_str(), request_audio_input.name.c_str()) == 0) {
- match = true;
- break;
- }
- }
-
- if(!match) {
- gsr_log(GSR_LOG_LEVEL_WARNING, "no audio application with the name \"%s\" was found, expected one of the following:", request_audio_input.name.c_str());
- for(const std::string &app_name : app_audio_names) {
- fprintf(stderr, " * %s\n", app_name.c_str());
- }
- fprintf(stderr, " assuming this is intentional (if you are trying to record audio for applications that haven't started yet).\n");
- }
- }
-}
-
-struct AudioTrackDescription {
- std::optional<std::string> custom_name;
- std::vector<std::string> devices;
- std::vector<std::string> applications;
- bool app_inverse = false;
-
- std::string to_title() const {
- if(custom_name.has_value()){
- return custom_name.value();
- }
- std::string title;
- if(!devices.empty()) {
- title += "Devices: ";
- for(size_t i = 0; i < devices.size(); ++i) {
- if(i > 0)
- title += ", ";
- title += devices[i];
- }
- }
-
- if(!applications.empty()) {
- if(!title.empty())
- title += ". ";
-
- if(app_inverse)
- title += "All applications except: ";
- else
- title += "Applications: ";
-
- for(size_t i = 0; i < applications.size(); ++i) {
- if(i > 0)
- title += ", ";
- title += applications[i];
- }
- }
- return title;
- }
-};
-
// Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio.
// Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED.
// OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU
-static std::vector<MergedAudioInputs> parse_audio_inputs(const gsr_audio_devices *audio_devices, const Arg *audio_input_arg) {
- std::vector<MergedAudioInputs> requested_audio_inputs;
-
- for(int i = 0; i < audio_input_arg->num_values; ++i) {
- const char *audio_input = audio_input_arg->values[i];
- if(!audio_input || audio_input[0] == '\0')
- continue;
-
- MergedAudioInputs merged_inputs = parse_audio_input_arg(audio_input);
- requested_audio_inputs.push_back(merged_inputs);
- AudioTrackDescription audio_track_description;
- if(merged_inputs.custom_name.has_value()){
- audio_track_description.custom_name = merged_inputs.custom_name;
- }
-
- for(AudioInput &request_audio_input : requested_audio_inputs.back().audio_inputs) {
- if(request_audio_input.type == AudioInputType::APPLICATION) {
- audio_track_description.applications.push_back(request_audio_input.name);
- audio_track_description.app_inverse = request_audio_input.inverted;
- continue;
- }
-
- bool match = false;
-
- if(request_audio_input.name == "default_output") {
- if(audio_devices->default_output[0] == '\0') {
- gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_output was specified but no default audio output is specified in the audio server");
- _exit(2);
- }
- match = true;
- audio_track_description.devices.push_back("Default output");
- } else if(request_audio_input.name == "default_input") {
- if(audio_devices->default_input[0] == '\0') {
- gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_input was specified but no default audio input is specified in the audio server");
- _exit(2);
- }
- match = true;
- audio_track_description.devices.push_back("Default input");
- } else {
- const gsr_audio_device *audio_device = get_audio_device_by_name(audio_devices, request_audio_input.name.c_str());
- if(audio_device) {
- match = true;
- audio_track_description.devices.push_back(audio_device->description);
- }
- }
-
- if(!match) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "Audio device '%s' is not a valid audio device, expected one of:", request_audio_input.name.c_str());
- if(audio_devices->default_output[0] != '\0')
- fprintf(stderr, " default_output (Default output)\n");
- if(audio_devices->default_input[0] != '\0')
- fprintf(stderr, " default_input (Default input)\n");
- for(size_t j = 0; j < audio_devices->num_items; ++j) {
- fprintf(stderr, " %s (%s)\n", audio_devices->items[j].name, audio_devices->items[j].description);
- }
- _exit(50);
- }
- }
-
- requested_audio_inputs.back().track_name = audio_track_description.to_title();
- }
-
- return requested_audio_inputs;
-}
-
-static bool audio_inputs_has_app_audio(const std::vector<AudioInput> &audio_inputs) {
- for(const auto &audio_input : audio_inputs) {
- if(audio_input.type == AudioInputType::APPLICATION)
- return true;
- }
- return false;
-}
-
-static bool merged_audio_inputs_has_app_audio(const std::vector<MergedAudioInputs> &merged_audio_inputs) {
- for(const auto &merged_audio_input : merged_audio_inputs) {
- if(audio_inputs_has_app_audio(merged_audio_input.audio_inputs))
- return true;
- }
- return false;
-}
-
// Should use amix if more than 1 audio device and 0 application audio, merged
-static bool audio_inputs_should_use_amix(const std::vector<AudioInput> &audio_inputs) {
- int num_audio_devices = 0;
- int num_app_audio = 0;
-
- for(const auto &audio_input : audio_inputs) {
- if(audio_input.type == AudioInputType::DEVICE)
- ++num_audio_devices;
- else if(audio_input.type == AudioInputType::APPLICATION)
- ++num_app_audio;
- }
-
- return num_audio_devices > 1 && num_app_audio == 0;
-}
-
-static bool merged_audio_inputs_should_use_amix(const std::vector<MergedAudioInputs> &merged_audio_inputs) {
- for(const auto &merged_audio_input : merged_audio_inputs) {
- if(audio_inputs_should_use_amix(merged_audio_input.audio_inputs))
- return true;
- }
- return false;
-}
-
-static void validate_merged_audio_inputs_app_audio(const std::vector<MergedAudioInputs> &merged_audio_inputs, const std::vector<std::string> &app_audio_names) {
- for(const auto &merged_audio_input : merged_audio_inputs) {
- int num_app_audio = 0;
- int num_app_inverted_audio = 0;
-
- for(const auto &audio_input : merged_audio_input.audio_inputs) {
- if(audio_input.type == AudioInputType::APPLICATION) {
- if(audio_input.inverted)
- ++num_app_inverted_audio;
- else
- ++num_app_audio;
- }
- }
-
- match_app_audio_input_to_available_apps(merged_audio_input.audio_inputs, app_audio_names);
-
- if(num_app_audio > 0 && num_app_inverted_audio > 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "argument -a was provided with both app: and app-inverse:, only one of them can be used for one audio track");
- _exit(2);
- }
- }
-}
-
/* Returns -1 if none is available */
-static std::vector<AudioDeviceData> create_device_audio_inputs(const std::vector<AudioInput> &audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, std::vector<AVFilterContext*> &src_filter_ctx, bool use_amix) {
+static std::vector<AudioDeviceData> create_device_audio_inputs(const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, std::vector<AVFilterContext*> &src_filter_ctx, bool use_amix) {
std::vector<AudioDeviceData> audio_track_audio_devices;
- for(size_t i = 0; i < audio_inputs.size(); ++i) {
- const auto &audio_input = audio_inputs[i];
+ for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) {
+ const gsr_audio_input &audio_input = merged_audio_inputs->items[i];
AVFilterContext *src_ctx = nullptr;
if(use_amix)
src_ctx = src_filter_ctx[i];
@@ -1412,13 +1161,14 @@ static std::vector<AudioDeviceData> create_device_audio_inputs(const std::vector
audio_device.audio_input = audio_input;
audio_device.src_filter_ctx = src_ctx;
- if(audio_input.name.empty()) {
+ if(audio_input.name[0] == '\0') {
audio_device.sound_device.handle = NULL;
audio_device.sound_device.frames = 0;
} else {
- const std::string description = "gsr-" + audio_input.name;
- if(sound_device_get_by_name(&audio_device.sound_device, description.c_str(), audio_input.name.c_str(), description.c_str(), num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get \"%s\" audio device", audio_input.name.c_str());
+ char description[GSR_AUDIO_INPUT_NAME_MAX_SIZE + 8];
+ snprintf(description, sizeof(description), "gsr-%s", audio_input.name);
+ if(sound_device_get_by_name(&audio_device.sound_device, description, audio_input.name, description, num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get \"%s\" audio device", audio_input.name);
_exit(1);
}
}
@@ -1434,7 +1184,7 @@ static std::vector<AudioDeviceData> create_device_audio_inputs(const std::vector
}
#ifdef GSR_APP_AUDIO
-static AudioDeviceData create_application_audio_audio_input(const MergedAudioInputs &merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) {
+static AudioDeviceData create_application_audio_audio_input(const gsr_merged_audio_inputs &merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) {
AudioDeviceData audio_device;
audio_device.frame = create_audio_frame(audio_codec_context);
if(!audio_device.frame)
@@ -1457,16 +1207,18 @@ static AudioDeviceData create_application_audio_audio_input(const MergedAudioInp
}
std::vector<const char*> audio_devices_sources;
- for(const auto &audio_input : merged_audio_inputs.audio_inputs) {
- if(audio_input.type == AudioInputType::DEVICE)
- audio_devices_sources.push_back(audio_input.name.c_str());
+ for(size_t i = 0; i < merged_audio_inputs.num_items; ++i) {
+ const gsr_audio_input &audio_input = merged_audio_inputs.items[i];
+ if(audio_input.type == GSR_AUDIO_INPUT_TYPE_DEVICE)
+ audio_devices_sources.push_back(audio_input.name);
}
bool app_audio_inverted = false;
std::vector<const char*> app_names;
- for(const auto &audio_input : merged_audio_inputs.audio_inputs) {
- if(audio_input.type == AudioInputType::APPLICATION) {
- app_names.push_back(audio_input.name.c_str());
+ for(size_t i = 0; i < merged_audio_inputs.num_items; ++i) {
+ const gsr_audio_input &audio_input = merged_audio_inputs.items[i];
+ if(audio_input.type == GSR_AUDIO_INPUT_TYPE_APPLICATION) {
+ app_names.push_back(audio_input.name);
app_audio_inverted = audio_input.inverted;
}
}
@@ -1520,15 +1272,15 @@ static void av_write_header(AVFormatContext *av_format_context, const char *ffmp
av_dict_free(&options);
}
-static size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const std::vector<MergedAudioInputs> &audio_inputs) {
+static size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs) {
if(replay_buffer_size_secs == -1)
return 0;
int audio_fps = 0;
- if(!audio_inputs.empty())
+ if(audio_inputs->num_items > 0)
audio_fps = GSR_AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec);
- return replay_buffer_size_secs * (fps + audio_fps * audio_inputs.size());
+ return replay_buffer_size_secs * (fps + audio_fps * audio_inputs->num_items);
}
static void set_display_server_environment_variables() {
@@ -1712,11 +1464,15 @@ int main(int argc, char **argv) {
if(audio_input_arg->num_values > 0)
get_pulseaudio_inputs(&audio_devices);
- std::vector<MergedAudioInputs> requested_audio_inputs = parse_audio_inputs(&audio_devices, audio_input_arg);
+ gsr_audio_input_tracks requested_audio_inputs;
+ const int parse_audio_inputs_result = gsr_audio_input_tracks_parse(&requested_audio_inputs, audio_input_arg->values, audio_input_arg->num_values, &audio_devices);
gsr_audio_devices_deinit(&audio_devices);
+ if(parse_audio_inputs_result != GSR_ERROR_OK)
+ _exit(gsr_error_to_exit_code(parse_audio_inputs_result));
- const bool uses_app_audio = merged_audio_inputs_has_app_audio(requested_audio_inputs);
- std::vector<std::string> app_audio_names;
+ const bool uses_app_audio = gsr_audio_input_tracks_has_app_audio(&requested_audio_inputs);
+ gsr_app_audio_names app_audio_names;
+ memset(&app_audio_names, 0, sizeof(app_audio_names));
#ifdef GSR_APP_AUDIO
gsr_pipewire_audio pipewire_audio;
memset(&pipewire_audio, 0, sizeof(pipewire_audio));
@@ -1732,8 +1488,8 @@ int main(int argc, char **argv) {
}
gsr_pipewire_audio_for_each_app(&pipewire_audio, [](const char *app_name, void *userdata) {
- std::vector<std::string> *app_audio_names = (std::vector<std::string>*)userdata;
- app_audio_names->push_back(app_name);
+ gsr_app_audio_names *app_audio_names = (gsr_app_audio_names*)userdata;
+ gsr_app_audio_names_add(app_audio_names, app_name);
return true;
}, &app_audio_names);
}
@@ -1744,7 +1500,10 @@ int main(int argc, char **argv) {
}
#endif
- validate_merged_audio_inputs_app_audio(requested_audio_inputs, app_audio_names);
+ const int validate_app_audio_result = gsr_audio_input_tracks_validate_app_audio(&requested_audio_inputs, &app_audio_names);
+ gsr_app_audio_names_deinit(&app_audio_names);
+ if(validate_app_audio_result != GSR_ERROR_OK)
+ _exit(gsr_error_to_exit_code(validate_app_audio_result));
gsr_windowing windowing;
gsr_windowing_params windowing_params;
@@ -1836,7 +1595,7 @@ int main(int argc, char **argv) {
const bool force_no_audio_offset = arg_parser.settings.is_livestream || arg_parser.settings.is_output_piped || (file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm");
const double target_fps = 1.0 / (double)arg_parser.settings.fps;
- const bool uses_amix = merged_audio_inputs_should_use_amix(requested_audio_inputs);
+ const bool uses_amix = gsr_audio_input_tracks_should_use_amix(&requested_audio_inputs);
arg_parser.settings.audio_codec = select_audio_codec_with_fallback(arg_parser.settings.audio_codec, file_extension.c_str(), uses_amix);
vec2i video_size = {0, 0};
@@ -1849,11 +1608,14 @@ int main(int argc, char **argv) {
// (Some?) livestreaming services require at least one audio track to work.
// If not audio is provided then create one silent audio track.
- if(arg_parser.settings.is_livestream && requested_audio_inputs.empty()) {
+ if(arg_parser.settings.is_livestream && requested_audio_inputs.num_items == 0) {
gsr_log(GSR_LOG_LEVEL_INFO, "live streaming but no audio track was added. Adding a silent audio track");
- MergedAudioInputs mai;
- mai.audio_inputs.push_back({""});
- requested_audio_inputs.push_back(std::move(mai));
+ gsr_merged_audio_inputs silent_audio_track;
+ memset(&silent_audio_track, 0, sizeof(silent_audio_track));
+ gsr_audio_input silent_audio_input;
+ memset(&silent_audio_input, 0, sizeof(silent_audio_input));
+ if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(&requested_audio_inputs, &silent_audio_track))
+ _exit(1);
}
AVStream *video_stream = nullptr;
@@ -1889,7 +1651,7 @@ int main(int argc, char **argv) {
video_frame->colorspace = video_codec_context->colorspace;
video_frame->chroma_location = video_codec_context->chroma_sample_location;
- const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.fps, arg_parser.settings.audio_codec, requested_audio_inputs);
+ const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.fps, arg_parser.settings.audio_codec, &requested_audio_inputs);
gsr_encoder encoder;
if(!gsr_encoder_init(&encoder, arg_parser.settings.replay_storage, estimated_replay_buffer_packets, arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.filename)) {
gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder");
@@ -1956,8 +1718,9 @@ int main(int argc, char **argv) {
int audio_max_frame_size = 1024;
int audio_stream_index = VIDEO_STREAM_INDEX + 1;
- for(const MergedAudioInputs &merged_audio_inputs : requested_audio_inputs) {
- const bool use_amix = audio_inputs_should_use_amix(merged_audio_inputs.audio_inputs);
+ for(size_t audio_track_index = 0; audio_track_index < requested_audio_inputs.num_items; ++audio_track_index) {
+ const gsr_merged_audio_inputs &merged_audio_inputs = requested_audio_inputs.items[audio_track_index];
+ const bool use_amix = gsr_audio_inputs_should_use_amix(&merged_audio_inputs);
AVCodecContext *audio_codec_context = create_audio_codec_context(arg_parser.settings.fps, arg_parser.settings.audio_codec, use_amix, arg_parser.settings.audio_bitrate);
if(!audio_codec_context)
_exit(1);
@@ -1969,8 +1732,8 @@ int main(int argc, char **argv) {
gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources");
}
- if(audio_stream && !merged_audio_inputs.track_name.empty() && !arg_parser.settings.exclude_metadata)
- av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name.c_str(), 0);
+ if(audio_stream && !merged_audio_inputs.track_name[0] == '\0' && !arg_parser.settings.exclude_metadata)
+ av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name, 0);
if(!open_audio(audio_codec_context, arg_parser.settings.ffmpeg_audio_opts))
_exit(1);
@@ -1989,7 +1752,7 @@ int main(int argc, char **argv) {
AVFilterGraph *graph = nullptr;
AVFilterContext *sink = nullptr;
if(use_amix) {
- int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.audio_inputs.size());
+ int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.num_items);
if(err < 0) {
gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter");
_exit(1);
@@ -2005,13 +1768,13 @@ int main(int argc, char **argv) {
const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec;
std::vector<AudioDeviceData> audio_track_audio_devices;
- if(audio_inputs_has_app_audio(merged_audio_inputs.audio_inputs)) {
+ if(gsr_audio_inputs_has_app_audio(&merged_audio_inputs)) {
assert(!use_amix);
#ifdef GSR_APP_AUDIO
audio_track_audio_devices.push_back(create_application_audio_audio_input(merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, &pipewire_audio));
#endif
} else {
- audio_track_audio_devices = create_device_audio_inputs(merged_audio_inputs.audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix);
+ audio_track_audio_devices = create_device_audio_inputs(&merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix);
}
AudioTrack audio_track;
diff --git a/src/recorder/audio_input.c b/src/recorder/audio_input.c
new file mode 100644
index 0000000..bbfb9bd
--- /dev/null
+++ b/src/recorder/audio_input.c
@@ -0,0 +1,336 @@
+#include "../../include/recorder/audio_input.h"
+#include "../../include/recorder/error.h"
+#include "../../include/utils.h"
+#include "../../include/log.h"
+
+#include <string.h>
+#include <strings.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+typedef struct {
+ gsr_merged_audio_inputs *merged_audio_inputs;
+ int error;
+} parse_audio_input_userdata;
+
+bool gsr_app_audio_names_add(gsr_app_audio_names *self, const char *name) {
+ if(!gsr_array_ensure_capacity((void**)&self->items, self->num_items, &self->capacity_items, sizeof(gsr_app_audio_name)))
+ return false;
+
+ snprintf(self->items[self->num_items].name, sizeof(self->items[self->num_items].name), "%s", name);
+ ++self->num_items;
+ return true;
+}
+
+void gsr_app_audio_names_deinit(gsr_app_audio_names *self) {
+ if(self->items) {
+ free(self->items);
+ self->items = NULL;
+ }
+ self->num_items = 0;
+ self->capacity_items = 0;
+}
+
+bool gsr_merged_audio_inputs_add(gsr_merged_audio_inputs *self, const gsr_audio_input *audio_input) {
+ if(!gsr_array_ensure_capacity((void**)&self->items, self->num_items, &self->capacity_items, sizeof(gsr_audio_input)))
+ return false;
+
+ self->items[self->num_items] = *audio_input;
+ ++self->num_items;
+ return true;
+}
+
+void gsr_merged_audio_inputs_deinit(gsr_merged_audio_inputs *self) {
+ if(self->items) {
+ free(self->items);
+ self->items = NULL;
+ }
+ self->num_items = 0;
+ self->capacity_items = 0;
+}
+
+static bool parse_audio_input_callback(const char *sub, size_t size, void *userdata) {
+ parse_audio_input_userdata *parse_userdata = userdata;
+ if(size == 0)
+ return true;
+
+ gsr_audio_input audio_input;
+ memset(&audio_input, 0, sizeof(audio_input));
+ snprintf(audio_input.name, sizeof(audio_input.name), "%.*s", (int)size, sub);
+
+ const size_t name_size = strlen(audio_input.name);
+ if(gsr_string_starts_with(audio_input.name, name_size, "name:")) {
+ snprintf(parse_userdata->merged_audio_inputs->track_name, sizeof(parse_userdata->merged_audio_inputs->track_name), "%s", audio_input.name + 5);
+ parse_userdata->merged_audio_inputs->has_custom_name = true;
+ return true;
+ } else if(gsr_string_starts_with(audio_input.name, name_size, "app:")) {
+ memmove(audio_input.name, audio_input.name + 4, name_size - 4 + 1);
+ audio_input.type = GSR_AUDIO_INPUT_TYPE_APPLICATION;
+ audio_input.inverted = false;
+ } else if(gsr_string_starts_with(audio_input.name, name_size, "app-inverse:")) {
+ memmove(audio_input.name, audio_input.name + 12, name_size - 12 + 1);
+ audio_input.type = GSR_AUDIO_INPUT_TYPE_APPLICATION;
+ audio_input.inverted = true;
+ } else if(gsr_string_starts_with(audio_input.name, name_size, "device:")) {
+ memmove(audio_input.name, audio_input.name + 7, name_size - 7 + 1);
+ audio_input.type = GSR_AUDIO_INPUT_TYPE_DEVICE;
+ } else {
+ audio_input.type = GSR_AUDIO_INPUT_TYPE_DEVICE;
+ }
+
+ if(!gsr_merged_audio_inputs_add(parse_userdata->merged_audio_inputs, &audio_input)) {
+ parse_userdata->error = GSR_ERROR_GENERIC;
+ return false;
+ }
+
+ return true;
+}
+
+int gsr_merged_audio_inputs_parse(gsr_merged_audio_inputs *self, const char *str) {
+ memset(self, 0, sizeof(*self));
+
+ parse_audio_input_userdata userdata;
+ userdata.merged_audio_inputs = self;
+ userdata.error = GSR_ERROR_OK;
+
+ gsr_string_split(str, '|', parse_audio_input_callback, &userdata);
+ if(userdata.error != GSR_ERROR_OK) {
+ gsr_merged_audio_inputs_deinit(self);
+ return userdata.error;
+ }
+
+ return GSR_ERROR_OK;
+}
+
+bool gsr_audio_input_tracks_add(gsr_audio_input_tracks *self, const gsr_merged_audio_inputs *merged_audio_inputs) {
+ if(!gsr_array_ensure_capacity((void**)&self->items, self->num_items, &self->capacity_items, sizeof(gsr_merged_audio_inputs)))
+ return false;
+
+ self->items[self->num_items] = *merged_audio_inputs;
+ ++self->num_items;
+ return true;
+}
+
+void gsr_audio_input_tracks_deinit(gsr_audio_input_tracks *self) {
+ for(size_t i = 0; i < self->num_items; ++i) {
+ gsr_merged_audio_inputs_deinit(&self->items[i]);
+ }
+
+ if(self->items) {
+ free(self->items);
+ self->items = NULL;
+ }
+ self->num_items = 0;
+ self->capacity_items = 0;
+}
+
+static const gsr_audio_device* get_audio_device_by_name(const gsr_audio_devices *audio_devices, const char *name) {
+ for(size_t i = 0; i < audio_devices->num_items; ++i) {
+ if(strcmp(audio_devices->items[i].name, name) == 0)
+ return &audio_devices->items[i];
+ }
+ return NULL;
+}
+
+static void audio_track_title_append(char *title, size_t title_size, size_t *offset, const char *str) {
+ const int written = snprintf(title + *offset, *offset < title_size ? title_size - *offset : 0, "%s", str);
+ if(written > 0)
+ *offset += written;
+}
+
+/* Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio.
+ Pipewire instead defaults to the default audio input if the audio input doesn't exist */
+static int validate_audio_inputs_get_track_name(const gsr_merged_audio_inputs *merged_audio_inputs, const gsr_audio_devices *audio_devices, char *track_name, size_t track_name_size) {
+ size_t offset = 0;
+ bool has_devices = false;
+ bool has_applications = false;
+ bool app_inverse = false;
+ track_name[0] = '\0';
+
+ for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) {
+ const gsr_audio_input *audio_input = &merged_audio_inputs->items[i];
+ if(audio_input->type == GSR_AUDIO_INPUT_TYPE_APPLICATION)
+ continue;
+
+ const char *device_description = NULL;
+ if(strcmp(audio_input->name, "default_output") == 0) {
+ if(audio_devices->default_output[0] == '\0') {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_output was specified but no default audio output is specified in the audio server");
+ return GSR_ERROR_UNSUPPORTED;
+ }
+ device_description = "Default output";
+ } else if(strcmp(audio_input->name, "default_input") == 0) {
+ if(audio_devices->default_input[0] == '\0') {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_input was specified but no default audio input is specified in the audio server");
+ return GSR_ERROR_UNSUPPORTED;
+ }
+ device_description = "Default input";
+ } else {
+ const gsr_audio_device *audio_device = get_audio_device_by_name(audio_devices, audio_input->name);
+ if(audio_device)
+ device_description = audio_device->description;
+ }
+
+ if(!device_description) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "Audio device '%s' is not a valid audio device, expected one of:", audio_input->name);
+ if(audio_devices->default_output[0] != '\0')
+ fprintf(stderr, " default_output (Default output)\n");
+ if(audio_devices->default_input[0] != '\0')
+ fprintf(stderr, " default_input (Default input)\n");
+ for(size_t j = 0; j < audio_devices->num_items; ++j) {
+ fprintf(stderr, " %s (%s)\n", audio_devices->items[j].name, audio_devices->items[j].description);
+ }
+ return GSR_ERROR_AUDIO_DEVICE_NOT_FOUND;
+ }
+
+ audio_track_title_append(track_name, track_name_size, &offset, has_devices ? ", " : "Devices: ");
+ audio_track_title_append(track_name, track_name_size, &offset, device_description);
+ has_devices = true;
+ }
+
+ for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) {
+ const gsr_audio_input *audio_input = &merged_audio_inputs->items[i];
+ if(audio_input->type != GSR_AUDIO_INPUT_TYPE_APPLICATION)
+ continue;
+
+ app_inverse = audio_input->inverted;
+ if(!has_applications) {
+ if(has_devices)
+ audio_track_title_append(track_name, track_name_size, &offset, ". ");
+ audio_track_title_append(track_name, track_name_size, &offset, app_inverse ? "All applications except: " : "Applications: ");
+ } else {
+ audio_track_title_append(track_name, track_name_size, &offset, ", ");
+ }
+
+ audio_track_title_append(track_name, track_name_size, &offset, audio_input->name);
+ has_applications = true;
+ }
+
+ return GSR_ERROR_OK;
+}
+
+int gsr_audio_input_tracks_parse(gsr_audio_input_tracks *self, const char **audio_input_args, int num_audio_input_args, const gsr_audio_devices *audio_devices) {
+ memset(self, 0, sizeof(*self));
+
+ for(int i = 0; i < num_audio_input_args; ++i) {
+ const char *audio_input = audio_input_args[i];
+ if(!audio_input || audio_input[0] == '\0')
+ continue;
+
+ gsr_merged_audio_inputs merged_audio_inputs;
+ const int parse_result = gsr_merged_audio_inputs_parse(&merged_audio_inputs, audio_input);
+ if(parse_result != GSR_ERROR_OK) {
+ gsr_audio_input_tracks_deinit(self);
+ return parse_result;
+ }
+
+ char track_name[GSR_AUDIO_TRACK_NAME_MAX_SIZE];
+ const int validate_result = validate_audio_inputs_get_track_name(&merged_audio_inputs, audio_devices, track_name, sizeof(track_name));
+ if(validate_result != GSR_ERROR_OK) {
+ gsr_merged_audio_inputs_deinit(&merged_audio_inputs);
+ gsr_audio_input_tracks_deinit(self);
+ return validate_result;
+ }
+
+ if(!merged_audio_inputs.has_custom_name)
+ snprintf(merged_audio_inputs.track_name, sizeof(merged_audio_inputs.track_name), "%s", track_name);
+
+ if(!gsr_audio_input_tracks_add(self, &merged_audio_inputs)) {
+ gsr_merged_audio_inputs_deinit(&merged_audio_inputs);
+ gsr_audio_input_tracks_deinit(self);
+ return GSR_ERROR_GENERIC;
+ }
+ }
+
+ return GSR_ERROR_OK;
+}
+
+bool gsr_audio_inputs_has_app_audio(const gsr_merged_audio_inputs *self) {
+ for(size_t i = 0; i < self->num_items; ++i) {
+ if(self->items[i].type == GSR_AUDIO_INPUT_TYPE_APPLICATION)
+ return true;
+ }
+ return false;
+}
+
+/* Should use amix if more than 1 audio device and 0 application audio, merged */
+bool gsr_audio_inputs_should_use_amix(const gsr_merged_audio_inputs *self) {
+ int num_audio_devices = 0;
+ int num_app_audio = 0;
+
+ for(size_t i = 0; i < self->num_items; ++i) {
+ if(self->items[i].type == GSR_AUDIO_INPUT_TYPE_DEVICE)
+ ++num_audio_devices;
+ else if(self->items[i].type == GSR_AUDIO_INPUT_TYPE_APPLICATION)
+ ++num_app_audio;
+ }
+
+ return num_audio_devices > 1 && num_app_audio == 0;
+}
+
+bool gsr_audio_input_tracks_has_app_audio(const gsr_audio_input_tracks *self) {
+ for(size_t i = 0; i < self->num_items; ++i) {
+ if(gsr_audio_inputs_has_app_audio(&self->items[i]))
+ return true;
+ }
+ return false;
+}
+
+bool gsr_audio_input_tracks_should_use_amix(const gsr_audio_input_tracks *self) {
+ for(size_t i = 0; i < self->num_items; ++i) {
+ if(gsr_audio_inputs_should_use_amix(&self->items[i]))
+ return true;
+ }
+ return false;
+}
+
+static void match_app_audio_input_to_available_apps(const gsr_merged_audio_inputs *merged_audio_inputs, const gsr_app_audio_names *app_audio_names) {
+ for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) {
+ const gsr_audio_input *audio_input = &merged_audio_inputs->items[i];
+ if(audio_input->type != GSR_AUDIO_INPUT_TYPE_APPLICATION || audio_input->inverted)
+ continue;
+
+ bool match = false;
+ for(size_t j = 0; j < app_audio_names->num_items; ++j) {
+ if(strcasecmp(app_audio_names->items[j].name, audio_input->name) == 0) {
+ match = true;
+ break;
+ }
+ }
+
+ if(!match) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "no audio application with the name \"%s\" was found, expected one of the following:", audio_input->name);
+ for(size_t j = 0; j < app_audio_names->num_items; ++j) {
+ fprintf(stderr, " * %s\n", app_audio_names->items[j].name);
+ }
+ fprintf(stderr, " assuming this is intentional (if you are trying to record audio for applications that haven't started yet).\n");
+ }
+ }
+}
+
+int gsr_audio_input_tracks_validate_app_audio(const gsr_audio_input_tracks *self, const gsr_app_audio_names *app_audio_names) {
+ for(size_t i = 0; i < self->num_items; ++i) {
+ const gsr_merged_audio_inputs *merged_audio_inputs = &self->items[i];
+ int num_app_audio = 0;
+ int num_app_inverted_audio = 0;
+
+ for(size_t j = 0; j < merged_audio_inputs->num_items; ++j) {
+ const gsr_audio_input *audio_input = &merged_audio_inputs->items[j];
+ if(audio_input->type == GSR_AUDIO_INPUT_TYPE_APPLICATION) {
+ if(audio_input->inverted)
+ ++num_app_inverted_audio;
+ else
+ ++num_app_audio;
+ }
+ }
+
+ match_app_audio_input_to_available_apps(merged_audio_inputs, app_audio_names);
+
+ if(num_app_audio > 0 && num_app_inverted_audio > 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "argument -a was provided with both app: and app-inverse:, only one of them can be used for one audio track");
+ return GSR_ERROR_UNSUPPORTED;
+ }
+ }
+
+ return GSR_ERROR_OK;
+}