diff --git a/src/internal_modules/roc_pipeline/config.h b/src/internal_modules/roc_pipeline/config.h index 31dc27df0..c0b015695 100644 --- a/src/internal_modules/roc_pipeline/config.h +++ b/src/internal_modules/roc_pipeline/config.h @@ -36,11 +36,8 @@ namespace roc { namespace pipeline { -//! Default sample rate, number of samples per second. -const size_t DefaultSampleRate = 44100; - //! Default sample specification. -static const audio::SampleSpec DefaultSampleSpec(DefaultSampleRate, +static const audio::SampleSpec DefaultSampleSpec(44100, audio::Sample_RawFormat, audio::ChanLayout_Surround, audio::ChanOrder_Smpte, diff --git a/src/internal_modules/roc_sndio/backend_map.cpp b/src/internal_modules/roc_sndio/backend_map.cpp index fc10daa90..a55d3b00f 100644 --- a/src/internal_modules/roc_sndio/backend_map.cpp +++ b/src/internal_modules/roc_sndio/backend_map.cpp @@ -39,16 +39,6 @@ const DriverInfo& BackendMap::nth_driver(size_t driver_index) const { return drivers_[driver_index]; } -void BackendMap::set_frame_size(core::nanoseconds_t frame_length, - const audio::SampleSpec& sample_spec) { -#ifdef ROC_TARGET_SOX - sox_backend_->set_frame_size(frame_length, sample_spec); -#endif // ROC_TARGET_SOX - - (void)frame_length; - (void)sample_spec; -} - void BackendMap::register_backends_() { #ifdef ROC_TARGET_PULSEAUDIO pulseaudio_backend_.reset(new (pulseaudio_backend_) PulseaudioBackend); diff --git a/src/internal_modules/roc_sndio/backend_map.h b/src/internal_modules/roc_sndio/backend_map.h index 8ec6f658d..766a28f78 100644 --- a/src/internal_modules/roc_sndio/backend_map.h +++ b/src/internal_modules/roc_sndio/backend_map.h @@ -55,10 +55,6 @@ class BackendMap : public core::NonCopyable<> { //! Get driver by index. const DriverInfo& nth_driver(size_t driver_index) const; - //! Set internal buffer size for all backends that need it. - void set_frame_size(core::nanoseconds_t frame_length, - const audio::SampleSpec& sample_spec); - private: friend class core::Singleton; diff --git a/src/internal_modules/roc_sndio/io_config.h b/src/internal_modules/roc_sndio/io_config.h index 22c865228..ad8e75009 100644 --- a/src/internal_modules/roc_sndio/io_config.h +++ b/src/internal_modules/roc_sndio/io_config.h @@ -21,6 +21,7 @@ namespace roc { namespace sndio { //! Default frame length. +//! @remarks //! 10ms is rather high, but works well even on cheap sound cards and CPUs. //! Usually you can use much lower values. const core::nanoseconds_t DefaultFrameLength = 10 * core::Millisecond; @@ -30,16 +31,17 @@ struct IoConfig { //! Sample spec audio::SampleSpec sample_spec; - //! Duration of the internal frames, in nanoseconds. - core::nanoseconds_t frame_length; - //! Requested input or output latency. core::nanoseconds_t latency; + //! Duration of the internal frames, in nanoseconds. + core::nanoseconds_t frame_length; + //! Initialize. IoConfig() - : frame_length(DefaultFrameLength) - , latency(0) { + : sample_spec() + , latency(0) + , frame_length(DefaultFrameLength) { } }; diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp index 8512bb35d..804f6a4ec 100644 --- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp +++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.cpp @@ -184,19 +184,6 @@ SoxBackend::SoxBackend() sox_get_globals()->output_message_handler = log_handler; } -void SoxBackend::set_frame_size(core::nanoseconds_t frame_length, - const audio::SampleSpec& sample_spec) { - size_t size = sample_spec.ns_2_samples_overall(frame_length); - - if (first_created_) { - roc_panic( - "sox backend:" - " set_frame_size() can be called only before creating first source or sink"); - } - - sox_get_globals()->bufsiz = size * sizeof(sox_sample_t); -} - const char* SoxBackend::name() const { return "sox"; } diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h index 2ece15326..0b2442bfa 100644 --- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h +++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_backend.h @@ -25,12 +25,6 @@ class SoxBackend : public IBackend, core::NonCopyable<> { public: SoxBackend(); - //! Set internal SoX frame size. - //! @remarks - //! Number of samples for all channels. - void set_frame_size(core::nanoseconds_t frame_length, - const audio::SampleSpec& sample_spec); - //! Returns name of backend. virtual const char* name() const; diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.cpp b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.cpp index 880556ba0..ad1562617 100644 --- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.cpp +++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_sink.cpp @@ -64,6 +64,15 @@ SoxSink::SoxSink(audio::FrameFactory& frame_factory, return; } + { + audio::SampleSpec spec = sample_spec_; + spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround, + audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo, 44100); + + sox_get_globals()->bufsiz = + spec.ns_2_samples_overall(frame_length_) * sizeof(sox_sample_t); + } + memset(&out_signal_, 0, sizeof(out_signal_)); out_signal_.rate = (sox_rate_t)sample_spec_.sample_rate(); out_signal_.channels = (unsigned)sample_spec_.num_channels(); diff --git a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp index 7b025e2ff..8164626c7 100644 --- a/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp +++ b/src/internal_modules/roc_sndio/target_sox/roc_sndio/sox_source.cpp @@ -68,6 +68,15 @@ SoxSource::SoxSource(audio::FrameFactory& frame_factory, return; } + { + audio::SampleSpec spec = sample_spec_; + spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround, + audio::ChanOrder_Smpte, audio::ChanMask_Surround_Stereo, 44100); + + sox_get_globals()->bufsiz = + spec.ns_2_samples_overall(frame_length_) * sizeof(sox_sample_t); + } + memset(&in_signal_, 0, sizeof(in_signal_)); in_signal_.rate = (sox_rate_t)sample_spec_.sample_rate(); in_signal_.channels = (unsigned)sample_spec_.num_channels(); diff --git a/src/tools/roc_copy/cmdline.ggo b/src/tools/roc_copy/cmdline.ggo index 1139060d6..8cb92197f 100644 --- a/src/tools/roc_copy/cmdline.ggo +++ b/src/tools/roc_copy/cmdline.ggo @@ -1,47 +1,51 @@ package "roc-copy" usage "roc-copy OPTIONS" -section "Options" +section "General options" option "verbose" v "Increase verbosity level (may be used multiple times)" multiple optional + option "color" - "Set colored logging mode for stderr output" + values="auto","always","never" default="auto" enum optional - option "list-supported" L "list supported schemes and formats" optional + option "list-supported" L "List supported protocols, formats, etc." optional - option "input" i "Input file URI" typestr="FILE_URI" string required - option "output" o "Output file URI" typestr="FILE_URI" string optional +section "I/O options" + option "input" i "Input file URI" typestr="FILE_URI" string optional option "input-format" - "Force input file format" typestr="FILE_FORMAT" string optional + + option "output" o "Output file URI" typestr="FILE_URI" string optional option "output-format" - "Force output file format" typestr="FILE_FORMAT" string optional + option "output-encoding" - "Output file encoding" typestr="IO_ENCODING" string optional - option "frame-len" - "Duration of the internal frames, TIME units" - typestr="TIME" string optional + option "io-frame-len" - "I/O frame length, TIME units" typestr="TIME" string optional - option "output-encoding" - "Output file encoding" - typestr="IO_ENCODING" string optional +section "Transcoding options" option "resampler-backend" - "Resampler backend" values="default","builtin","speex","speexdec" default="default" enum optional - option "resampler-profile" - "Resampler profile" values="low","medium","high" default="medium" enum optional - option "profile" - "Enable self profiling" flag off +section "Debugging options" - option "color" - "Set colored logging mode for stderr output" - values="auto","always","never" default="auto" enum optional + option "prof" - "Enable self-profiling" flag off text " FILE_URI defines an absolute or relative file path, e.g.: file:///home/user/test.wav; file:./test.wav; file:- + (use 'file://-' for stdout) -FILE_FORMAT is the output file format name, e.g.: +FILE_FORMAT is file format name: wav; ogg; mp3 + (useful when format can't be auto-detected) -IO_ENCODING is input or output format/rate/channels, e.g.: - s16/44100/mono; f32/48000/stereo +IO_ENCODING is input or output encoding in form //: + s16/44100/mono; f32/48000/stereo; -/-/mono + (any component may be '-' to use default value) -TIME is an integer or floating-point number with a suffix, e.g.: +TIME defines duration using a number with mandatory suffix: 123ns; 1.23us; 1.23ms; 1.23s; 1.23m; 1.23h; Use --list-supported option to print the list of the supported diff --git a/src/tools/roc_copy/main.cpp b/src/tools/roc_copy/main.cpp index df5ab51e7..5067681fb 100644 --- a/src/tools/roc_copy/main.cpp +++ b/src/tools/roc_copy/main.cpp @@ -16,8 +16,6 @@ #include "roc_dbgio/print_supported.h" #include "roc_pipeline/transcoder_sink.h" #include "roc_sndio/backend_dispatcher.h" -#include "roc_sndio/backend_map.h" -#include "roc_sndio/io_config.h" #include "roc_sndio/io_pump.h" #include "roc_status/code_to_str.h" @@ -25,23 +23,9 @@ using namespace roc; -int main(int argc, char** argv) { - core::HeapArena::set_guards(core::HeapArena_DefaultGuards - | core::HeapArena_LeakGuard); - - core::HeapArena arena; - - core::CrashHandler crash_handler; - - gengetopt_args_info args; - - const int code = cmdline_parser(argc, argv, &args); - if (code != 0) { - return code; - } - - core::ScopedRelease args_holder(&args, &cmdline_parser_free); +namespace { +void init_logger(const gengetopt_args_info& args) { core::Logger::instance().set_verbosity(args.verbose_given); switch (args.color_arg) { @@ -57,22 +41,43 @@ int main(int argc, char** argv) { default: break; } +} - pipeline::TranscoderConfig transcoder_config; - - sndio::IoConfig source_config; - - if (args.frame_len_given) { - if (!core::parse_duration(args.frame_len_arg, source_config.frame_length)) { +bool build_input_config(const gengetopt_args_info& args, sndio::IoConfig& input_config) { + if (args.io_frame_len_given) { + if (!core::parse_duration(args.io_frame_len_arg, input_config.frame_length)) { roc_log(LogError, "invalid --frame-len: bad format"); - return 1; + return false; } - if (source_config.frame_length <= 0) { + if (input_config.frame_length <= 0) { roc_log(LogError, "invalid --frame-len: should be > 0"); + return false; + } + } + + return true; +} + +bool build_output_config(const gengetopt_args_info& args, + const sndio::IoConfig& input_config, + sndio::IoConfig& output_config) { + output_config = input_config; + + if (args.output_encoding_given) { + if (!audio::parse_sample_spec(args.output_encoding_arg, + output_config.sample_spec)) { + roc_log(LogError, "invalid --output-encoding"); return 1; } } + return true; +} + +bool build_transcoder_config(const gengetopt_args_info& args, + pipeline::TranscoderConfig& transcoder_config, + sndio::ISource& input_source, + sndio::ISink* output_sink) { switch (args.resampler_backend_arg) { case resampler_backend_arg_default: transcoder_config.resampler.backend = audio::ResamplerBackend_Auto; @@ -104,129 +109,211 @@ int main(int argc, char** argv) { break; } - transcoder_config.enable_profiling = args.profile_flag; + transcoder_config.enable_profiling = args.prof_flag; - core::SlabPool frame_pool("frame_pool", arena); - core::SlabPool frame_buffer_pool( - "frame_buffer_pool", arena, - sizeof(core::Buffer) - + transcoder_config.input_sample_spec.ns_2_bytes(source_config.frame_length)); + transcoder_config.input_sample_spec = input_source.sample_spec(); + transcoder_config.output_sample_spec = + output_sink ? output_sink->sample_spec() : input_source.sample_spec(); - sndio::BackendDispatcher backend_dispatcher(frame_pool, frame_buffer_pool, arena); + return true; +} - sndio::BackendMap::instance().set_frame_size(source_config.frame_length, - transcoder_config.input_sample_spec); +size_t compute_max_frame_size(const sndio::IoConfig& io_config) { + audio::SampleSpec spec = io_config.sample_spec; + spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround, + audio::ChanOrder_Smpte, audio::ChanMask_Surround_7_1_4, 48000); - if (args.list_supported_given) { - if (!dbgio::print_supported(dbgio::Print_Sndio | dbgio::Print_Audio, - backend_dispatcher, arena)) { - return 1; - } - return 0; + return spec.ns_2_samples_overall(io_config.frame_length) * sizeof(audio::sample_t); +} + +bool parse_input_uri(const gengetopt_args_info& args, address::IoUri& input_uri) { + if (!args.input_given) { + roc_log(LogError, "missing mandatory --input URI"); + return false; } - address::IoUri input_uri(arena); - if (!address::parse_io_uri(args.input_arg, input_uri) || !input_uri.is_file()) { - roc_log(LogError, "invalid --input file URI"); - return 1; + if (!address::parse_io_uri(args.input_arg, input_uri)) { + roc_log(LogError, "invalid --input URI: bad format"); + return false; } + + if (!input_uri.is_file()) { + roc_log(LogError, "invalid --input URI: should be file"); + return false; + } + if (!args.input_format_given && input_uri.is_special_file()) { roc_log(LogError, "--input-format should be specified if --input is \"-\""); - return 1; + return false; } - core::ScopedPtr input_source; - { - const status::StatusCode code = backend_dispatcher.open_source( - input_uri, args.input_format_arg, source_config, input_source); + return true; +} - if (code != status::StatusOK) { - roc_log(LogError, "can't open --input file or device: status=%s", - status::code_to_str(code)); - return 1; - } +bool parse_output_uri(const gengetopt_args_info& args, address::IoUri& output_uri) { + if (!address::parse_io_uri(args.output_arg, output_uri)) { + roc_log(LogError, "invalid --output URI: bad format"); + return false; + } - if (input_source->has_clock()) { - roc_log(LogError, "unsupported --input type"); - return 1; - } + if (!output_uri.is_file()) { + roc_log(LogError, "invalid --output URI: should be file"); + return false; } - transcoder_config.input_sample_spec = input_source->sample_spec(); + if (!args.output_format_given && output_uri.is_special_file()) { + roc_log(LogError, "--output-format should be specified if --output is \"-\""); + return false; + } - if (args.output_encoding_given) { - if (!audio::parse_sample_spec(args.output_encoding_arg, - transcoder_config.output_sample_spec)) { - roc_log(LogError, "invalid --output-encoding"); - return 1; - } - } else { - transcoder_config.output_sample_spec = transcoder_config.input_sample_spec; + return true; +} + +bool open_input_source(sndio::BackendDispatcher& backend_dispatcher, + const sndio::IoConfig& io_config, + const address::IoUri& input_uri, + const char* input_format, + core::ScopedPtr& input_source) { + const status::StatusCode code = + backend_dispatcher.open_source(input_uri, input_format, io_config, input_source); + + if (code != status::StatusOK) { + roc_log(LogError, "can't open --input file or device: status=%s", + status::code_to_str(code)); + return false; } - audio::IFrameWriter* output_writer = NULL; + if (input_source->has_clock()) { + roc_log(LogError, "unsupported --input type"); + return false; + } - sndio::IoConfig sink_config; - sink_config.sample_spec = transcoder_config.output_sample_spec; - sink_config.frame_length = source_config.frame_length; + return true; +} - address::IoUri output_uri(arena); - if (args.output_given) { - if (!address::parse_io_uri(args.output_arg, output_uri) - || !output_uri.is_file()) { - roc_log(LogError, "invalid --output file URI"); +bool open_output_sink(sndio::BackendDispatcher& backend_dispatcher, + const sndio::IoConfig& io_config, + const address::IoUri& output_uri, + const char* output_format, + core::ScopedPtr& output_sink) { + const status::StatusCode code = + backend_dispatcher.open_sink(output_uri, output_format, io_config, output_sink); + + if (code != status::StatusOK) { + roc_log(LogError, "can't open --output file or device: status=%s", + status::code_to_str(code)); + return false; + } + + if (output_sink->has_clock()) { + roc_log(LogError, "unsupported --output type"); + return false; + } + + return true; +} + +} // namespace + +int main(int argc, char** argv) { + core::CrashHandler crash_handler; + + core::HeapArena::set_guards(core::HeapArena_DefaultGuards + | core::HeapArena_LeakGuard); + core::HeapArena heap_arena; + + gengetopt_args_info args; + const int code = cmdline_parser(argc, argv, &args); + if (code != 0) { + return code; + } + core::ScopedRelease args_releaser(&args, &cmdline_parser_free); + + init_logger(args); + + sndio::IoConfig input_config; + if (!build_input_config(args, input_config)) { + return 1; + } + + core::SlabPool frame_pool("frame_pool", heap_arena); + core::SlabPool frame_buffer_pool( + "frame_buffer_pool", heap_arena, + sizeof(core::Buffer) + compute_max_frame_size(input_config)); + + sndio::BackendDispatcher backend_dispatcher(frame_pool, frame_buffer_pool, + heap_arena); + + if (args.list_supported_given) { + if (!dbgio::print_supported(dbgio::Print_Sndio | dbgio::Print_Audio, + backend_dispatcher, heap_arena)) { return 1; } + return 0; } - if (!args.output_format_given && output_uri.is_special_file()) { - roc_log(LogError, "--output-format should be specified if --output is \"-\""); + + address::IoUri input_uri(heap_arena); + if (!parse_input_uri(args, input_uri)) { return 1; } - core::ScopedPtr output_sink; - if (args.output_given) { - const status::StatusCode code = backend_dispatcher.open_sink( - output_uri, args.output_format_arg, sink_config, output_sink); + core::ScopedPtr input_source; + if (!open_input_source(backend_dispatcher, input_config, input_uri, + args.input_format_arg, input_source)) { + return 1; + } - if (code != status::StatusOK) { - roc_log(LogError, "can't open --output file or device: status=%s", - status::code_to_str(code)); + input_config.sample_spec = input_source->sample_spec(); + + sndio::IoConfig output_config; + if (!build_output_config(args, input_config, output_config)) { + return 1; + } + + address::IoUri output_uri(heap_arena); + if (args.output_given) { + if (!parse_output_uri(args, output_uri)) { return 1; } + } - if (output_sink->has_clock()) { - roc_log(LogError, "unsupported --output type"); + core::ScopedPtr output_sink; + if (args.output_given) { + if (!open_output_sink(backend_dispatcher, output_config, output_uri, + args.output_format_arg, output_sink)) { return 1; } + output_config.sample_spec = output_sink->sample_spec(); + } - output_writer = output_sink.get(); + pipeline::TranscoderConfig transcoder_config; + if (!build_transcoder_config(args, transcoder_config, *input_source, + output_sink.get())) { + return 1; } - audio::ProcessorMap processor_map(arena); + audio::ProcessorMap processor_map(heap_arena); - pipeline::TranscoderSink transcoder(transcoder_config, output_writer, processor_map, - frame_pool, frame_buffer_pool, arena); + pipeline::TranscoderSink transcoder(transcoder_config, output_sink.get(), + processor_map, frame_pool, frame_buffer_pool, + heap_arena); if (transcoder.init_status() != status::StatusOK) { roc_log(LogError, "can't create transcoder pipeline: status=%s", status::code_to_str(transcoder.init_status())); return 1; } - sndio::IoConfig pump_config; - pump_config.sample_spec = input_source->sample_spec(); - pump_config.frame_length = source_config.frame_length; - sndio::IoPump pump(frame_pool, frame_buffer_pool, *input_source, NULL, transcoder, - pump_config, sndio::IoPump::ModePermanent); + input_config, sndio::IoPump::ModePermanent); if (pump.init_status() != status::StatusOK) { - roc_log(LogError, "can't create audio pump: status=%s", + roc_log(LogError, "can't create io pump: status=%s", status::code_to_str(pump.init_status())); return 1; } const status::StatusCode status = pump.run(); if (status != status::StatusOK) { - roc_log(LogError, "can't run audio pump: status=%s", status::code_to_str(status)); + roc_log(LogError, "io pump failed: status=%s", status::code_to_str(status)); return 1; } diff --git a/src/tools/roc_recv/cmdline.ggo b/src/tools/roc_recv/cmdline.ggo index c9e23e187..7d71e9566 100644 --- a/src/tools/roc_recv/cmdline.ggo +++ b/src/tools/roc_recv/cmdline.ggo @@ -1,107 +1,132 @@ package "roc-recv" usage "roc-recv OPTIONS" -section "Options" +section "General options" option "verbose" v "Increase verbosity level (may be used multiple times)" multiple optional option "color" - "Set colored logging mode for stderr output" values="auto","always","never" default="auto" enum optional - option "list-supported" L "List supported schemes and formats" optional + option "list-supported" L "List supported protocols, formats, etc." optional + +section "Operation options" + + option "oneshot" 1 "Exit when last connection is closed" flag off + +section "Output options" option "output" o "Output file or device URI" typestr="IO_URI" string optional option "output-format" - "Force output file format" typestr="FILE_FORMAT" string optional - option "backup" - "Backup file or device URI (if set, used when there are no sessions)" + option "io-encoding" - "Output device encoding" typestr="IO_ENCODING" string optional + option "io-latency" - "Output device latency, TIME units" typestr="TIME"string optional + option "io-frame-len" - "Output frame length, TIME units" typestr="TIME" string optional + +section "Backup input options" + + option "backup" - + "Backup file or device URI (used as input when there are no connections)" typestr="IO_URI" string optional option "backup-format" - "Force backup file format" typestr="FILE_FORMAT" string optional - option "oneshot" 1 "Exit when last connected client disconnects" - flag off - option "source" s "Local source endpoint" typestr="ENDPOINT_URI" +section "Network options" + + option "source" s "Local source endpoint to listen on" typestr="NET_URI" string multiple optional - option "repair" r "Local repair endpoint" typestr="ENDPOINT_URI" + option "repair" r "Local repair endpoint to listen on" typestr="NET_URI" string multiple optional - option "control" c "Local control endpoint" typestr="ENDPOINT_URI" + option "control" c "Local control endpoint to listen on" typestr="NET_URI" string multiple optional option "miface" - - "IPv4 or IPv6 address of the network interface on which to join the multicast group" - typestr="MIFACE" string multiple optional + "IPv4 or IPv6 address of the network interface on which to join the multicast group" + typestr="IPADDR" string multiple optional option "reuseaddr" - - "Enable SO_REUSEADDR when binding sockets" optional + "Enable SO_REUSEADDR when binding sockets" optional + +section "Decoding options" + + option "packet-encoding" - "Custom network packet encoding(s) (may be used multiple times)" + typestr="PKT_ENCODING" string multiple optional + + option "plc" - "Algorithm to mask unrecoverable packet losses" + values="none","beep" default="none" enum optional - option "io-latency" - "Playback device latency, TIME units" - string optional + option "resampler-backend" - "Resampler backend" + values="auto","builtin","speex","speexdec" default="auto" enum optional + option "resampler-profile" - "Resampler profile" + values="low","medium","high" default="medium" enum optional + +section "Latency options" option "target-latency" - "Target latency, TIME units or 'auto' for adaptive mode" - string default="auto" optional + typestr="TIME" string default="auto" optional option "latency-tolerance" - "Maximum deviation from target latency, TIME units" - string optional + typestr="TIME" string optional option "start-latency" - "Starting target latency in adaptive mode, TIME units" - string optional + typestr="TIME" string optional option "min-latency" - "Minimum target latency in adaptive mode, TIME units" - string optional + typestr="TIME" string optional option "max-latency" - "Maximum target latency in adaptive mode, TIME units" - string optional - - option "no-play-timeout" - "No-playback timeout, TIME units" - string optional - option "choppy-play-timeout" - "Choppy playback timeout, TIME units" - string optional + typestr="TIME" string optional option "latency-backend" - "Which latency to use in latency tuner" values="niq" default="niq" enum optional option "latency-profile" - "Latency tuning profile" values="auto","responsive","gradual","intact" default="auto" enum optional - option "resampler-backend" - "Resampler backend" - values="auto","builtin","speex","speexdec" default="auto" enum optional - option "resampler-profile" - "Resampler profile" - values="low","medium","high" default="medium" enum optional - - option "plc" - "Which PLC algorithm to use" - values="none","beep" default="none" enum optional +section "Timeout options" - option "io-encoding" - "Force output file or device encoding" - typestr="IO_ENCODING" string optional - - option "frame-len" - "Duration of the I/O frames, TIME units" + option "no-play-timeout" - "No-playback timeout, TIME units" typestr="TIME" string optional + option "choppy-play-timeout" - "Choppy playback timeout, TIME units" + typestr="TIME" string optional + +section "Memory options" - option "max-packet-size" - "Maximum packet size, in SIZE units" + option "max-packet-size" - "Maximum network packet size, SIZE units" typestr="SIZE" string optional - option "max-frame-size" - "Maximum internal frame size, in SIZE units" + option "max-frame-size" - "Maximum I/O and processing frame size, SIZE units" typestr="SIZE" string optional - option "profile" - "Enable self-profiling" flag off - option "dump" - "Path for a CSV file where to dump run-time metrics" +section "Debugging options" + + option "prof" - "Enable self-profiling" flag off + option "dump" - "Dump run-time metrics to specified CSV file" typestr="PATH" string optional text " -ENDPOINT_URI is a network endpoint URI, e.g.: - rtp://0.0.0.0:10001; rtp+rs8m://127.0.0.1:10001; rs8m://[::1]:10001 - -IO_URI is a device or file URI, e.g.: +IO_URI is a device or file URI: pulse://default; pulse://alsa_input.pci-0000_00_1f.3.analog-stereo file:///home/user/test.wav; file:./test.wav; file:- + (use '://default' for default device, 'file://-' for stdout) -IO_ENCODING is output file or device format/rate/channels, e.g.: - s16/44100/mono; f32/48000/stereo +IO_ENCODING is device or file encoding in form //: + s16/44100/mono; f32/48000/stereo; -/-/mono + (any component may be '-' to use default value) -FILE_FORMAT is the output file format name, e.g.: +FILE_FORMAT is file format name: wav; ogg; mp3 + (useful when format can't be auto-detected) + +NET_URI is a network endpoint URI: + rtp://0.0.0.0:10001; rtp+rs8m://127.0.0.1:10001; rs8m://[::1]:10001 + +PKT_ENCODING is media packets encoding in form ://: + 101:s16/44100/mono; 102:f32/48000/stereo + (numeric id is arbitrary number in range 100..127 that should be used + on both sender and receiver to identify encoding) -TIME is an integer or floating-point number with a suffix, e.g.: +TIME defines duration using a number with mandatory suffix: 123ns; 1.23us; 1.23ms; 1.23s; 1.23m; 1.23h; -SIZE is an integer or floating-point number with an optional suffix, e.g.: +SIZE defines byte size using a number with optional suffix: 123; 1.23K; 1.23M; 1.23G; Use --list-supported (-L) option to print the list of the supported -URI schemes and file formats. +protocols, formats, channel masks, etc. See further details in roc-recv(1) manual page locally or online: https://roc-streaming.org/toolkit/docs/manuals/roc_recv.html" diff --git a/src/tools/roc_recv/main.cpp b/src/tools/roc_recv/main.cpp index 7456bfa83..a39f9394c 100644 --- a/src/tools/roc_recv/main.cpp +++ b/src/tools/roc_recv/main.cpp @@ -15,15 +15,11 @@ #include "roc_core/parse_units.h" #include "roc_core/scoped_ptr.h" #include "roc_core/scoped_release.h" -#include "roc_core/time.h" #include "roc_dbgio/print_supported.h" -#include "roc_netio/network_loop.h" #include "roc_node/context.h" #include "roc_node/receiver.h" -#include "roc_pipeline/receiver_source.h" #include "roc_pipeline/transcoder_source.h" #include "roc_sndio/backend_dispatcher.h" -#include "roc_sndio/backend_map.h" #include "roc_sndio/io_pump.h" #include "roc_status/code_to_str.h" @@ -31,23 +27,9 @@ using namespace roc; -int main(int argc, char** argv) { - core::HeapArena::set_guards(core::HeapArena_DefaultGuards - | core::HeapArena_LeakGuard); - - core::HeapArena heap_arena; - - core::CrashHandler crash_handler; - - gengetopt_args_info args; - - const int code = cmdline_parser(argc, argv, &args); - if (code != 0) { - return code; - } - - core::ScopedRelease args_holder(&args, &cmdline_parser_free); +namespace { +void init_logger(const gengetopt_args_info& args) { core::Logger::instance().set_verbosity(args.verbose_given); switch (args.color_arg) { @@ -63,69 +45,101 @@ int main(int argc, char** argv) { default: break; } +} - pipeline::ReceiverSourceConfig receiver_config; +bool build_io_config(const gengetopt_args_info& args, sndio::IoConfig& io_config) { + if (args.io_encoding_given) { + if (!audio::parse_sample_spec(args.io_encoding_arg, io_config.sample_spec)) { + roc_log(LogError, "invalid --io-encoding"); + return false; + } + } - sndio::IoConfig io_config; + if (args.io_latency_given) { + if (!core::parse_duration(args.io_latency_arg, io_config.latency)) { + roc_log(LogError, "invalid --io-latency: bad format"); + return false; + } + if (io_config.latency <= 0) { + roc_log(LogError, "invalid --io-latency: should be > 0"); + return false; + } + } - if (args.frame_len_given) { - if (!core::parse_duration(args.frame_len_arg, io_config.frame_length)) { + if (args.io_frame_len_given) { + if (!core::parse_duration(args.io_frame_len_arg, io_config.frame_length)) { roc_log(LogError, "invalid --frame-len: bad format"); - return 1; + return false; } if (io_config.frame_length <= 0) { roc_log(LogError, "invalid --frame-len: should be > 0"); - return 1; + return false; } } - if (args.io_latency_given) { - if (!core::parse_duration(args.io_latency_arg, io_config.latency)) { - roc_log(LogError, "invalid --io-latency: bad format"); - return 1; + return true; +} + +bool build_context_config(const gengetopt_args_info& args, + const sndio::IoConfig& io_config, + node::ContextConfig& context_config) { + if (args.max_packet_size_given) { + if (!core::parse_size(args.max_packet_size_arg, context_config.max_packet_size)) { + roc_log(LogError, "invalid --max-packet-size: bad format"); + return false; } - if (io_config.latency <= 0) { - roc_log(LogError, "invalid --io-latency: should be > 0"); - return 1; + if (context_config.max_packet_size == 0) { + roc_log(LogError, "invalid --max-packet-size: should be > 0"); + return false; } } - if (args.io_encoding_given) { - if (!audio::parse_sample_spec(args.io_encoding_arg, io_config.sample_spec)) { - roc_log(LogError, "invalid --io-encoding"); - return 1; + if (args.max_frame_size_given) { + if (!core::parse_size(args.max_frame_size_arg, context_config.max_frame_size)) { + roc_log(LogError, "invalid --max-frame-size: bad format"); + return false; } + if (context_config.max_frame_size == 0) { + roc_log(LogError, "invalid --max-frame-size: should be > 0"); + return false; + } + } else { + audio::SampleSpec spec = io_config.sample_spec; + spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround, + audio::ChanOrder_Smpte, audio::ChanMask_Surround_7_1_4, 48000); + context_config.max_frame_size = + spec.ns_2_samples_overall(io_config.frame_length) * sizeof(audio::sample_t); } - // TODO(gh-568): remove set_frame_size() after removing sox - sndio::BackendMap::instance().set_frame_size( - io_config.frame_length, receiver_config.common.output_sample_spec); + return true; +} - switch (args.latency_backend_arg) { - case latency_backend_arg_niq: - receiver_config.session_defaults.latency.tuner_backend = - audio::LatencyTunerBackend_Niq; - break; - default: - break; +bool build_receiver_config(const gengetopt_args_info& args, + pipeline::ReceiverSourceConfig& receiver_config, + node::Context& context, + sndio::ISink& output_sink) { + for (size_t n = 0; n < args.packet_encoding_given; n++) { + rtp::Encoding encoding; + if (!rtp::parse_encoding(args.packet_encoding_arg[n], encoding)) { + roc_log(LogError, "invalid --packet-encoding"); + return false; + } + + const status::StatusCode code = + context.encoding_map().register_encoding(encoding); + if (code != status::StatusOK) { + roc_log(LogError, "can't register packet encoding: status=%s", + status::code_to_str(code)); + return false; + } } - switch (args.latency_profile_arg) { - case latency_profile_arg_auto: - receiver_config.session_defaults.latency.tuner_profile = - audio::LatencyTunerProfile_Auto; - break; - case latency_profile_arg_responsive: - receiver_config.session_defaults.latency.tuner_profile = - audio::LatencyTunerProfile_Responsive; - break; - case latency_profile_arg_gradual: - receiver_config.session_defaults.latency.tuner_profile = - audio::LatencyTunerProfile_Gradual; + switch (args.plc_arg) { + case plc_arg_none: + receiver_config.session_defaults.plc.backend = audio::PlcBackend_None; break; - case latency_profile_arg_intact: - receiver_config.session_defaults.latency.tuner_profile = - audio::LatencyTunerProfile_Intact; + case plc_arg_beep: + receiver_config.session_defaults.plc.backend = audio::PlcBackend_Beep; break; default: break; @@ -166,12 +180,31 @@ int main(int argc, char** argv) { break; } - switch (args.plc_arg) { - case plc_arg_none: - receiver_config.session_defaults.plc.backend = audio::PlcBackend_None; + switch (args.latency_backend_arg) { + case latency_backend_arg_niq: + receiver_config.session_defaults.latency.tuner_backend = + audio::LatencyTunerBackend_Niq; break; - case plc_arg_beep: - receiver_config.session_defaults.plc.backend = audio::PlcBackend_Beep; + default: + break; + } + + switch (args.latency_profile_arg) { + case latency_profile_arg_auto: + receiver_config.session_defaults.latency.tuner_profile = + audio::LatencyTunerProfile_Auto; + break; + case latency_profile_arg_responsive: + receiver_config.session_defaults.latency.tuner_profile = + audio::LatencyTunerProfile_Responsive; + break; + case latency_profile_arg_gradual: + receiver_config.session_defaults.latency.tuner_profile = + audio::LatencyTunerProfile_Gradual; + break; + case latency_profile_arg_intact: + receiver_config.session_defaults.latency.tuner_profile = + audio::LatencyTunerProfile_Intact; break; default: break; @@ -185,11 +218,11 @@ int main(int argc, char** argv) { args.target_latency_arg, receiver_config.session_defaults.latency.target_latency)) { roc_log(LogError, "invalid --target-latency: bad format"); - return 1; + return false; } if (receiver_config.session_defaults.latency.target_latency <= 0) { roc_log(LogError, "invalid --target-latency: should be 'auto' or > 0"); - return 1; + return false; } } } @@ -199,11 +232,11 @@ int main(int argc, char** argv) { args.latency_tolerance_arg, receiver_config.session_defaults.latency.latency_tolerance)) { roc_log(LogError, "invalid --latency-tolerance: bad format"); - return 1; + return false; } if (receiver_config.session_defaults.latency.latency_tolerance <= 0) { roc_log(LogError, "invalid --latency-tolerance: should be > 0"); - return 1; + return false; } } @@ -213,17 +246,17 @@ int main(int argc, char** argv) { LogError, "--start-latency can be specified only in" " adaptive latency mode (i.e. --target-latency is 'auto' or omitted)"); - return 1; + return false; } if (!core::parse_duration( args.start_latency_arg, receiver_config.session_defaults.latency.start_target_latency)) { roc_log(LogError, "invalid --start-latency: bad format"); - return 1; + return false; } if (receiver_config.session_defaults.latency.start_target_latency <= 0) { roc_log(LogError, "invalid --start-latency: should be > 0"); - return 1; + return false; } } @@ -233,39 +266,39 @@ int main(int argc, char** argv) { LogError, "--min-latency and --max-latency can be specified only in" " adaptive latency mode (i.e. --target-latency is 'auto' or omitted)"); - return 1; + return false; } if (!args.min_latency_given || !args.max_latency_given) { roc_log(LogError, "--min-latency and --max-latency should be specified together"); - return 1; + return false; } if (!core::parse_duration( args.min_latency_arg, receiver_config.session_defaults.latency.min_target_latency)) { roc_log(LogError, "invalid --min-latency: bad format"); - return 1; + return false; } if (receiver_config.session_defaults.latency.min_target_latency <= 0) { roc_log(LogError, "invalid --min-latency: should be > 0"); - return 1; + return false; } if (!core::parse_duration( args.max_latency_arg, receiver_config.session_defaults.latency.max_target_latency)) { roc_log(LogError, "invalid --max-latency: bad format"); - return 1; + return false; } if (receiver_config.session_defaults.latency.max_target_latency <= 0) { roc_log(LogError, "invalid --max-latency: should be > 0"); - return 1; + return false; } if (receiver_config.session_defaults.latency.min_target_latency > receiver_config.session_defaults.latency.max_target_latency) { roc_log( LogError, "incorrect --max-latency: should be greater or equal to --min-latency"); - return 1; + return false; } } @@ -274,11 +307,11 @@ int main(int argc, char** argv) { args.no_play_timeout_arg, receiver_config.session_defaults.watchdog.no_playback_timeout)) { roc_log(LogError, "invalid --no-play-timeout: bad format"); - return 1; + return false; } if (receiver_config.session_defaults.watchdog.no_playback_timeout <= 0) { roc_log(LogError, "invalid --no-play-timeout: should be > 0"); - return 1; + return false; } } @@ -287,74 +320,38 @@ int main(int argc, char** argv) { args.choppy_play_timeout_arg, receiver_config.session_defaults.watchdog.choppy_playback_timeout)) { roc_log(LogError, "invalid --choppy-play-timeout: bad format"); - return 1; + return false; } if (receiver_config.session_defaults.watchdog.choppy_playback_timeout <= 0) { roc_log(LogError, "invalid --choppy-play-timeout: should be > 0"); - return 1; + return false; } } - receiver_config.common.enable_profiling = args.profile_flag; + receiver_config.common.enable_profiling = args.prof_flag; if (args.dump_given) { receiver_config.common.dumper.dump_file = args.dump_arg; } - node::ContextConfig context_config; + receiver_config.common.enable_cpu_clock = !output_sink.has_clock(); + receiver_config.common.output_sample_spec = output_sink.sample_spec(); - if (args.max_packet_size_given) { - if (!core::parse_size(args.max_packet_size_arg, context_config.max_packet_size)) { - roc_log(LogError, "invalid --max-packet-size: bad format"); - return 1; - } - if (context_config.max_packet_size == 0) { - roc_log(LogError, "invalid --max-packet-size: should be > 0"); - return 1; - } - } - - if (args.max_frame_size_given) { - if (!core::parse_size(args.max_frame_size_arg, context_config.max_frame_size)) { - roc_log(LogError, "invalid --max-frame-size: bad format"); - return 1; - } - if (context_config.max_frame_size == 0) { - roc_log(LogError, "invalid --max-frame-size: should be > 0"); - return 1; - } - } else { - audio::SampleSpec spec = io_config.sample_spec; - spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround, - audio::ChanOrder_Smpte, audio::ChanMask_Surround_7_1_4, 48000); - context_config.max_frame_size = - spec.ns_2_samples_overall(io_config.frame_length) * sizeof(audio::sample_t); - } - - node::Context context(context_config, heap_arena); - if (context.init_status() != status::StatusOK) { - roc_log(LogError, "can't initialize node context: status=%s", - status::code_to_str(context.init_status())); - return 1; + if (!receiver_config.common.output_sample_spec.is_valid()) { + roc_log(LogError, + "can't detect output encoding, try to set it" + " explicitly with --io-encoding option"); + return false; } - sndio::BackendDispatcher backend_dispatcher( - context.frame_pool(), context.frame_buffer_pool(), context.arena()); - - if (args.list_supported_given) { - if (!dbgio::print_supported(dbgio::Print_Netio | dbgio::Print_Sndio - | dbgio::Print_Audio, - backend_dispatcher, context.arena())) { - return 1; - } - return 0; - } + return true; +} - address::IoUri output_uri(context.arena()); +bool parse_output_uri(const gengetopt_args_info& args, address::IoUri& output_uri) { if (args.output_given) { if (!address::parse_io_uri(args.output_arg, output_uri)) { roc_log(LogError, "invalid --output file or device URI"); - return 1; + return false; } } @@ -362,24 +359,31 @@ int main(int argc, char** argv) { if (output_uri.is_valid() && !output_uri.is_file()) { roc_log(LogError, "--output-format can't be used if --output is not a file URI"); - return 1; + return false; } } else { if (output_uri.is_special_file()) { roc_log(LogError, "--output-format should be specified if --output is \"-\""); - return 1; + return false; } } - core::ScopedPtr output_sink; + return true; +} + +bool open_output_sink(sndio::BackendDispatcher& backend_dispatcher, + const sndio::IoConfig& io_config, + const address::IoUri& output_uri, + const char* output_format, + core::ScopedPtr& output_sink) { if (output_uri.is_valid()) { const status::StatusCode code = backend_dispatcher.open_sink( - output_uri, args.output_format_arg, io_config, output_sink); + output_uri, output_format, io_config, output_sink); if (code != status::StatusOK) { roc_log(LogError, "can't open --output file or device: status=%s", status::code_to_str(code)); - return 1; + return false; } } else { const status::StatusCode code = @@ -388,94 +392,96 @@ int main(int argc, char** argv) { if (code != status::StatusOK) { roc_log(LogError, "can't open default --output device: status=%s", status::code_to_str(code)); - return 1; + return false; } } - receiver_config.common.enable_cpu_clock = !output_sink->has_clock(); - receiver_config.common.output_sample_spec = output_sink->sample_spec(); + return true; +} - if (!receiver_config.common.output_sample_spec.is_valid()) { - roc_log(LogError, - "can't detect output encoding, try to set it" - " explicitly with --rate option"); - return 1; +bool parse_backup_uri(const gengetopt_args_info& args, address::IoUri& backup_uri) { + if (!address::parse_io_uri(args.backup_arg, backup_uri)) { + roc_log(LogError, "invalid --backup file or device URI"); + return false; } - core::ScopedPtr backup_source; - core::ScopedPtr backup_pipeline; - - if (args.backup_given) { - address::IoUri backup_uri(context.arena()); - - if (!address::parse_io_uri(args.backup_arg, backup_uri)) { - roc_log(LogError, "invalid --backup file or device URI"); - return 1; - } - - if (args.backup_format_given) { - if (backup_uri.is_valid() && !backup_uri.is_file()) { - roc_log(LogError, - "--backup-format can't be used if --backup is not a file URI"); - return 1; - } - } else { - if (backup_uri.is_special_file()) { - roc_log(LogError, - "--backup-format should be specified if --backup is \"-\""); - return 1; - } + if (args.backup_format_given) { + if (backup_uri.is_valid() && !backup_uri.is_file()) { + roc_log(LogError, + "--backup-format can't be used if --backup is not a file URI"); + return false; } - - const status::StatusCode code = backend_dispatcher.open_source( - backup_uri, args.backup_format_arg, io_config, backup_source); - - if (code != status::StatusOK) { - roc_log(LogError, "can't open --backup file or device: status=%s", - status::code_to_str(code)); - return 1; + } else { + if (backup_uri.is_special_file()) { + roc_log(LogError, "--backup-format should be specified if --backup is \"-\""); + return false; } + } - pipeline::TranscoderConfig transcoder_config; - - transcoder_config.resampler.backend = - receiver_config.session_defaults.resampler.backend; - transcoder_config.resampler.profile = - receiver_config.session_defaults.resampler.profile; + return true; +} - transcoder_config.input_sample_spec = - audio::SampleSpec(backup_source->sample_spec().sample_rate(), - receiver_config.common.output_sample_spec.pcm_format(), - receiver_config.common.output_sample_spec.channel_set()); - transcoder_config.output_sample_spec = - audio::SampleSpec(receiver_config.common.output_sample_spec.sample_rate(), - receiver_config.common.output_sample_spec.pcm_format(), - receiver_config.common.output_sample_spec.channel_set()); +bool open_backup_source(sndio::BackendDispatcher& backend_dispatcher, + const sndio::IoConfig& io_config, + const address::IoUri& backup_uri, + const char* backup_format, + core::ScopedPtr& backup_source) { + const status::StatusCode code = backend_dispatcher.open_source( + backup_uri, backup_format, io_config, backup_source); - backup_pipeline.reset(new (context.arena()) pipeline::TranscoderSource( - transcoder_config, *backup_source, context.processor_map(), - context.frame_pool(), context.frame_buffer_pool(), context.arena())); - if (!backup_pipeline) { - roc_log(LogError, "can't allocate backup pipeline"); - return 1; - } - if (backup_pipeline->init_status() != status::StatusOK) { - roc_log(LogError, "can't create backup pipeline: status=%s", - status::code_to_str(backup_pipeline->init_status())); - return 1; - } + if (code != status::StatusOK) { + roc_log(LogError, "can't open --backup file or device: status=%s", + status::code_to_str(code)); + return false; } - node::Receiver receiver(context, receiver_config); - if (receiver.init_status() != status::StatusOK) { - roc_log(LogError, "can't create receiver node: status=%s", - status::code_to_str(receiver.init_status())); - return 1; - } + return true; +} + +bool open_backup_transcoder( + core::ScopedPtr& backup_transcoder, + sndio::ISource& backup_source, + node::Context& context, + const pipeline::ReceiverSourceConfig& receiver_config) { + pipeline::TranscoderConfig transcoder_config; + + transcoder_config.resampler.backend = + receiver_config.session_defaults.resampler.backend; + transcoder_config.resampler.profile = + receiver_config.session_defaults.resampler.profile; + + transcoder_config.input_sample_spec = + audio::SampleSpec(backup_source.sample_spec().sample_rate(), + receiver_config.common.output_sample_spec.pcm_format(), + receiver_config.common.output_sample_spec.channel_set()); + transcoder_config.output_sample_spec = + audio::SampleSpec(receiver_config.common.output_sample_spec.sample_rate(), + receiver_config.common.output_sample_spec.pcm_format(), + receiver_config.common.output_sample_spec.channel_set()); + + backup_transcoder.reset(new (context.arena()) pipeline::TranscoderSource( + transcoder_config, backup_source, context.processor_map(), context.frame_pool(), + context.frame_buffer_pool(), context.arena())); + if (!backup_transcoder) { + roc_log(LogError, "can't allocate backup pipeline"); + return false; + } + + if (backup_transcoder->init_status() != status::StatusOK) { + roc_log(LogError, "can't create backup pipeline: status=%s", + status::code_to_str(backup_transcoder->init_status())); + return false; + } + + return true; +} +bool prepare_receiver(const gengetopt_args_info& args, + node::Context& context, + node::Receiver& receiver) { if (args.source_given == 0) { roc_log(LogError, "at least one --source endpoint should be specified"); - return 1; + return false; } if (args.repair_given != 0 && args.repair_given != args.source_given) { @@ -483,7 +489,7 @@ int main(int argc, char** argv) { "invalid number of --repair endpoints: expected either 0 or %d endpoints" " (one per --source), got %d endpoints", (int)args.source_given, (int)args.repair_given); - return 1; + return false; } if (args.control_given != 0 && args.control_given != args.source_given) { @@ -491,7 +497,7 @@ int main(int argc, char** argv) { "invalid number of --control endpoints: expected either 0 or %d endpoints" " (one per --source), got %d endpoints", (int)args.source_given, (int)args.control_given); - return 1; + return false; } if (args.miface_given != 0 && args.miface_given != args.source_given) { @@ -499,7 +505,7 @@ int main(int argc, char** argv) { "invalid number of --miface values: expected either 0 or %d values" " (one per --source), got %d values", (int)args.source_given, (int)args.miface_given); - return 1; + return false; } for (size_t slot = 0; slot < (size_t)args.source_given; slot++) { @@ -508,7 +514,7 @@ int main(int argc, char** argv) { if (!address::parse_network_uri(args.source_arg[slot], address::NetworkUri::Subset_Full, endpoint)) { roc_log(LogError, "can't parse --source endpoint: %s", args.source_arg[slot]); - return 1; + return false; } netio::UdpConfig iface_config; @@ -519,19 +525,19 @@ int main(int argc, char** argv) { >= sizeof(iface_config.multicast_interface)) { roc_log(LogError, "invalid --miface \"%s\": string too long", args.miface_arg[slot]); - return 1; + return false; } strcpy(iface_config.multicast_interface, args.miface_arg[slot]); } if (!receiver.configure(slot, address::Iface_AudioSource, iface_config)) { roc_log(LogError, "can't configure --source endpoint"); - return 1; + return false; } if (!receiver.bind(slot, address::Iface_AudioSource, endpoint)) { roc_log(LogError, "can't bind --source endpoint: %s", args.source_arg[slot]); - return 1; + return false; } } @@ -541,7 +547,7 @@ int main(int argc, char** argv) { if (!address::parse_network_uri(args.repair_arg[slot], address::NetworkUri::Subset_Full, endpoint)) { roc_log(LogError, "can't parse --repair endpoint: %s", args.source_arg[slot]); - return 1; + return false; } netio::UdpConfig iface_config; @@ -552,19 +558,19 @@ int main(int argc, char** argv) { >= sizeof(iface_config.multicast_interface)) { roc_log(LogError, "invalid --miface \"%s\": string too long", args.miface_arg[slot]); - return 1; + return false; } strcpy(iface_config.multicast_interface, args.miface_arg[slot]); } if (!receiver.configure(slot, address::Iface_AudioRepair, iface_config)) { roc_log(LogError, "can't configure --repair endpoint"); - return 1; + return false; } if (!receiver.bind(slot, address::Iface_AudioRepair, endpoint)) { roc_log(LogError, "can't bind --repair port: %s", args.repair_arg[slot]); - return 1; + return false; } } @@ -575,7 +581,7 @@ int main(int argc, char** argv) { address::NetworkUri::Subset_Full, endpoint)) { roc_log(LogError, "can't parse --control endpoint: %s", args.control_arg[slot]); - return 1; + return false; } netio::UdpConfig iface_config; @@ -586,40 +592,137 @@ int main(int argc, char** argv) { >= sizeof(iface_config.multicast_interface)) { roc_log(LogError, "invalid --miface \"%s\": string too long", args.miface_arg[slot]); - return 1; + return false; } strcpy(iface_config.multicast_interface, args.miface_arg[slot]); } if (!receiver.configure(slot, address::Iface_AudioControl, iface_config)) { roc_log(LogError, "can't configure --control endpoint"); - return 1; + return false; } if (!receiver.bind(slot, address::Iface_AudioControl, endpoint)) { roc_log(LogError, "can't bind --control endpoint: %s", args.control_arg[slot]); + return false; + } + } + + return true; +} + +} // namespace + +int main(int argc, char** argv) { + core::CrashHandler crash_handler; + + core::HeapArena::set_guards(core::HeapArena_DefaultGuards + | core::HeapArena_LeakGuard); + core::HeapArena heap_arena; + + gengetopt_args_info args; + const int code = cmdline_parser(argc, argv, &args); + if (code != 0) { + return code; + } + core::ScopedRelease args_releaser(&args, &cmdline_parser_free); + + init_logger(args); + + sndio::IoConfig io_config; + if (!build_io_config(args, io_config)) { + return 1; + } + + node::ContextConfig context_config; + if (!build_context_config(args, io_config, context_config)) { + return 1; + } + + node::Context context(context_config, heap_arena); + if (context.init_status() != status::StatusOK) { + roc_log(LogError, "can't initialize node context: status=%s", + status::code_to_str(context.init_status())); + return 1; + } + + sndio::BackendDispatcher backend_dispatcher( + context.frame_pool(), context.frame_buffer_pool(), context.arena()); + + if (args.list_supported_given) { + if (!dbgio::print_supported(dbgio::Print_Netio | dbgio::Print_Sndio + | dbgio::Print_Audio, + backend_dispatcher, context.arena())) { return 1; } + return 0; + } + + address::IoUri output_uri(context.arena()); + if (!parse_output_uri(args, output_uri)) { + return 1; + } + + core::ScopedPtr output_sink; + if (!open_output_sink(backend_dispatcher, io_config, output_uri, + args.output_format_arg, output_sink)) { + return 1; + } + + io_config.sample_spec = output_sink->sample_spec(); + + pipeline::ReceiverSourceConfig receiver_config; + if (!build_receiver_config(args, receiver_config, context, *output_sink)) { + return 1; + } + + core::ScopedPtr backup_source; + core::ScopedPtr backup_transcoder; + + if (args.backup_given) { + address::IoUri backup_uri(context.arena()); + if (!parse_backup_uri(args, backup_uri)) { + return 1; + } + + if (!open_backup_source(backend_dispatcher, io_config, backup_uri, + args.backup_format_arg, backup_source)) { + return 1; + } + + if (!open_backup_transcoder(backup_transcoder, *backup_source, context, + receiver_config)) { + return 1; + } + } + + node::Receiver receiver(context, receiver_config); + if (receiver.init_status() != status::StatusOK) { + roc_log(LogError, "can't create receiver node: status=%s", + status::code_to_str(receiver.init_status())); + return 1; + } + + if (!prepare_receiver(args, context, receiver)) { + return 1; } - sndio::IoConfig pump_config; - pump_config.sample_spec = output_sink->sample_spec(); - pump_config.frame_length = io_config.frame_length; + const sndio::IoPump::Mode pump_mode = + args.oneshot_flag ? sndio::IoPump::ModeOneshot : sndio::IoPump::ModePermanent; - sndio::IoPump pump( - context.frame_pool(), context.frame_buffer_pool(), receiver.source(), - backup_pipeline.get(), *output_sink, pump_config, - args.oneshot_flag ? sndio::IoPump::ModeOneshot : sndio::IoPump::ModePermanent); + sndio::IoPump pump(context.frame_pool(), context.frame_buffer_pool(), + receiver.source(), backup_transcoder.get(), *output_sink, + io_config, pump_mode); if (pump.init_status() != status::StatusOK) { - roc_log(LogError, "can't create audio pump: status=%s", + roc_log(LogError, "can't create io pump: status=%s", status::code_to_str(pump.init_status())); return 1; } const status::StatusCode status = pump.run(); if (status != status::StatusOK) { - roc_log(LogError, "can't run audio pump: status=%s", status::code_to_str(status)); + roc_log(LogError, "io pump failed: status=%s", status::code_to_str(status)); return 1; } diff --git a/src/tools/roc_send/cmdline.ggo b/src/tools/roc_send/cmdline.ggo index f497e0388..fcff2ed21 100644 --- a/src/tools/roc_send/cmdline.ggo +++ b/src/tools/roc_send/cmdline.ggo @@ -1,100 +1,123 @@ package "roc-send" usage "roc-send OPTIONS" -section "Options" +section "General options" option "verbose" v "Increase verbosity level (may be used multiple times)" multiple optional option "color" - "Set colored logging mode for stderr output" values="auto","always","never" default="auto" enum optional - option "list-supported" L "List supported schemes and formats" optional + option "list-supported" L "List supported protocols, formats, etc." optional + +section "Input options" option "input" i "Input file or device URI" typestr="IO_URI" string optional option "input-format" - "Force input file format" typestr="FILE_FORMAT" string optional - option "source" s "Remote source endpoint" typestr="ENDPOINT_URI" + option "io-encoding" - "Input device encoding" typestr="IO_ENCODING" string optional + option "io-latency" - "Input device latency, TIME units" typestr="TIME"string optional + option "io-frame-len" - "Input frame length, TIME units" typestr="TIME" string optional + +section "Network options" + + option "source" s "Remote source endpoint to connect to" typestr="NET_URI" string multiple optional - option "repair" r "Remote repair endpoint" typestr="ENDPOINT_URI" + option "repair" r "Remote repair endpoint to connect to" typestr="NET_URI" string multiple optional - option "control" c "Remote control endpoint" typestr="ENDPOINT_URI" + option "control" c "Remote control endpoint to connect to" typestr="NET_URI" string multiple optional - option "reuseaddr" - "Enable SO_REUSEADDR when binding sockets" optional - - option "io-latency" - "Recording device latency, TIME units" - string optional - - option "target-latency" - - "Target latency (only for sender-side tuning), TIME units or 'auto' for adaptive mode" - string optional - option "latency-tolerance" - - "Maximum deviation from target latency (only for sender-side tuning), TIME units" - string optional - option "start-latency" - - "Starting target latency in adaptive mode (only for sender-side tuning), TIME units" - string optional - option "min-latency" - - "Minimum target latency in adaptive mode (only for sender-side tuning), TIME units" - string optional - option "max-latency" - - "Maximum target latency in adaptive mode (only for sender-side tuning), TIME units" - string optional + option "miface" - + "IPv4 or IPv6 address of the network interface on which to join the multicast group" + typestr="IPADDR" string multiple optional + option "reuseaddr" - + "Enable SO_REUSEADDR when binding sockets" optional - option "latency-backend" - "Which latency to use in latency tuner" - values="niq" default="niq" enum optional - option "latency-profile" - "Latency tuning profile" - values="responsive","gradual","intact" default="intact" enum optional +section "Encoding options" + + option "packet-encoding" - "Custom network packet encoding" + typestr="PKT_ENCODING" string optional + option "packet-len" - "Network packet length, TIME units" + typestr="TIME" string optional + + option "fec-encoding" - "FEC encoding, 'auto' to auto-detect from network endpoints" + typestr="FEC_ENCODING" string optional + option "fec-block-src" - "Number of source packets in FEC block" + int optional + option "fec-block-rpr" - "Number of repair packets in FEC block" + int optional option "resampler-backend" - "Resampler backend" values="auto","builtin","speex","speexdec" default="auto" enum optional option "resampler-profile" - "Resampler profile" values="low","medium","high" default="medium" enum optional - option "io-encoding" - "Force input device encoding" - typestr="IO_ENCODING" string optional - - option "nbsrc" - "Number of source packets in FEC block" - int optional - option "nbrpr" - "Number of repair packets in FEC block" - int optional +section "Latency options (only for sender-side latency tuning)" - option "packet-len" - "Outgoing packet length, TIME units" - string optional - option "frame-len" - "Duration of the I/O frames, TIME units" + option "target-latency" - "Target latency, TIME units or 'auto' for adaptive mode" + typestr="TIME" string optional + option "latency-tolerance" - "Maximum deviation from target latency, TIME units" + typestr="TIME" string optional + option "start-latency" - "Starting target latency in adaptive mode, TIME units" + typestr="TIME" string optional + option "min-latency" - "Minimum target latency in adaptive mode, TIME units" + typestr="TIME" string optional + option "max-latency" - "Maximum target latency in adaptive mode, TIME units" typestr="TIME" string optional - option "max-packet-size" - "Maximum packet size, in SIZE units" + option "latency-backend" - "Which latency to use in latency tuner" + values="niq" default="niq" enum optional + option "latency-profile" - "Latency tuning profile" + values="responsive","gradual","intact" default="intact" enum optional + +section "Memory options" + + option "max-packet-size" - "Maximum network packet size, SIZE units" typestr="SIZE" string optional - option "max-frame-size" - "Maximum internal frame size, in SIZE units" + option "max-frame-size" - "Maximum I/O and processing frame size, SIZE units" typestr="SIZE" string optional - option "profile" - "Enable self-profiling" flag off - option "dump" - "Path for a CSV file where to dump run-time metrics" +section "Debugging options" + + option "prof" - "Enable self-profiling" flag off + option "dump" - "Dump run-time metrics to specified CSV file" typestr="PATH" string optional text " -ENDPOINT_URI is a network endpoint URI, e.g.: - rtp://127.0.0.1:10001; rtp+rs8m://127.0.0.1:10001; rs8m://[::1]:10001 - -IO_URI is a device or file URI, e.g.: +IO_URI is a device or file URI: pulse://default; pulse://alsa_input.pci-0000_00_1f.3.analog-stereo file:///home/user/test.wav; file:./test.wav; file:- + (use '://default' for default device, 'file://-' for stdout) -IO_ENCODING is input device format/rate/channels, e.g.: - s16/44100/mono; f32/48000/stereo +IO_ENCODING is device or file encoding in form //: + s16/44100/mono; f32/48000/stereo; -/-/mono + (any component may be '-' to use default value) -FILE_FORMAT is the output file format name, e.g.: +FILE_FORMAT is file format name: wav; ogg; mp3 + (useful when format can't be auto-detected) + +NET_URI is a network endpoint URI: + rtp://0.0.0.0:10001; rtp+rs8m://127.0.0.1:10001; rs8m://[::1]:10001 + +PKT_ENCODING is media packets encoding in form ://: + 101:s16/44100/mono; 102:f32/48000/stereo + (numeric id is arbitrary number in range 100..127 that should be used + on both sender and receiver to identify encoding) + +FEC_ENCODING is repair packets encoding name: + auto; rs8m; ldpc + (use 'auto' to auto-detect fec encoding from network URIs) -TIME is an integer or floating-point number with a suffix, e.g.: +TIME defines duration using a number with mandatory suffix: 123ns; 1.23us; 1.23ms; 1.23s; 1.23m; 1.23h; -SIZE is an integer or floating-point number with an optional suffix, e.g.: +SIZE defines byte size using a number with optional suffix: 123; 1.23K; 1.23M; 1.23G; Use --list-supported (-L) option to print the list of the supported -URI schemes and file formats. +protocols, formats, channel masks, etc. See further details in roc-send(1) manual page locally or online: https://roc-streaming.org/toolkit/docs/manuals/roc_send.html" diff --git a/src/tools/roc_send/main.cpp b/src/tools/roc_send/main.cpp index 2550d2b8a..7b8879318 100644 --- a/src/tools/roc_send/main.cpp +++ b/src/tools/roc_send/main.cpp @@ -15,14 +15,10 @@ #include "roc_core/parse_units.h" #include "roc_core/scoped_ptr.h" #include "roc_core/scoped_release.h" -#include "roc_core/time.h" #include "roc_dbgio/print_supported.h" -#include "roc_netio/network_loop.h" #include "roc_node/context.h" #include "roc_node/sender.h" -#include "roc_pipeline/sender_sink.h" #include "roc_sndio/backend_dispatcher.h" -#include "roc_sndio/backend_map.h" #include "roc_sndio/io_pump.h" #include "roc_status/code_to_str.h" @@ -30,23 +26,9 @@ using namespace roc; -int main(int argc, char** argv) { - core::HeapArena::set_guards(core::HeapArena_DefaultGuards - | core::HeapArena_LeakGuard); - - core::HeapArena heap_arena; - - core::CrashHandler crash_handler; - - gengetopt_args_info args; - - const int code = cmdline_parser(argc, argv, &args); - if (code != 0) { - return code; - } - - core::ScopedRelease args_holder(&args, &cmdline_parser_free); +namespace { +void init_logger(const gengetopt_args_info& args) { core::Logger::instance().set_verbosity(args.verbose_given); switch (args.color_arg) { @@ -62,63 +44,125 @@ int main(int argc, char** argv) { default: break; } +} - pipeline::SenderSinkConfig sender_config; +bool build_io_config(const gengetopt_args_info& args, sndio::IoConfig& io_config) { + if (args.io_encoding_given) { + if (!audio::parse_sample_spec(args.io_encoding_arg, io_config.sample_spec)) { + roc_log(LogError, "invalid --io-encoding"); + return false; + } + } - sndio::IoConfig io_config; + if (args.io_latency_given) { + if (!core::parse_duration(args.io_latency_arg, io_config.latency)) { + roc_log(LogError, "invalid --io-latency: bad format"); + return false; + } + if (io_config.latency <= 0) { + roc_log(LogError, "invalid --io-latency: should be > 0"); + return false; + } + } - if (args.frame_len_given) { - if (!core::parse_duration(args.frame_len_arg, io_config.frame_length)) { + if (args.io_frame_len_given) { + if (!core::parse_duration(args.io_frame_len_arg, io_config.frame_length)) { roc_log(LogError, "invalid --frame-len: bad format"); - return 1; + return false; } if (io_config.frame_length <= 0) { roc_log(LogError, "invalid --frame-len: should be > 0"); - return 1; + return false; } } - if (args.io_latency_given) { - if (!core::parse_duration(args.io_latency_arg, io_config.latency)) { - roc_log(LogError, "invalid --io-latency: bad format"); - return 1; + return true; +} + +bool build_context_config(const gengetopt_args_info& args, + const sndio::IoConfig& io_config, + node::ContextConfig& context_config) { + if (args.max_packet_size_given) { + if (!core::parse_size(args.max_packet_size_arg, context_config.max_packet_size)) { + roc_log(LogError, "invalid --max-packet-size: bad format"); + return false; } - if (io_config.latency <= 0) { - roc_log(LogError, "invalid --io-latency: should be > 0"); - return 1; + if (context_config.max_packet_size == 0) { + roc_log(LogError, "invalid --max-packet-size: should be > 0"); + return false; } + } else { + audio::SampleSpec spec = io_config.sample_spec; + spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround, + audio::ChanOrder_Smpte, audio::ChanMask_Surround_7_1_4, 48000); + context_config.max_packet_size = packet::Packet::approx_size( + spec.ns_2_samples_overall(io_config.frame_length)); } - if (args.io_encoding_given) { - if (!audio::parse_sample_spec(args.io_encoding_arg, io_config.sample_spec)) { - roc_log(LogError, "invalid --io-encoding"); - return 1; + if (args.max_frame_size_given) { + if (!core::parse_size(args.max_frame_size_arg, context_config.max_frame_size)) { + roc_log(LogError, "invalid --max-frame-size: bad format"); + return false; + } + if (context_config.max_frame_size == 0) { + roc_log(LogError, "invalid --max-frame-size: should be > 0"); + return false; } } - // TODO(gh-568): remove set_frame_size() after removing sox - sndio::BackendMap::instance().set_frame_size(io_config.frame_length, - sender_config.input_sample_spec); + return true; +} + +bool build_sender_config(const gengetopt_args_info& args, + pipeline::SenderSinkConfig& sender_config, + node::Context& context, + sndio::ISource& input_source) { + if (args.packet_encoding_given) { + rtp::Encoding encoding; + if (!rtp::parse_encoding(args.packet_encoding_arg, encoding)) { + roc_log(LogError, "invalid --packet-encoding"); + return false; + } + + const status::StatusCode code = + context.encoding_map().register_encoding(encoding); + if (code != status::StatusOK) { + roc_log(LogError, "can't register packet encoding: status=%s", + status::code_to_str(code)); + return false; + } + + sender_config.payload_type = encoding.payload_type; + } if (args.packet_len_given) { if (!core::parse_duration(args.packet_len_arg, sender_config.packet_length)) { roc_log(LogError, "invalid --packet-len: bad format"); - return 1; + return false; } if (sender_config.packet_length <= 0) { roc_log(LogError, "invalid --packet-len: should be > 0"); - return 1; + return false; } } - if (args.source_given) { - address::NetworkUri source_endpoint(heap_arena); + if (args.fec_encoding_given && strcmp(args.fec_encoding_arg, "none") == 0) { + sender_config.fec_encoder.scheme = packet::FEC_None; + } else if (args.fec_encoding_given && strcmp(args.fec_encoding_arg, "auto") != 0) { + sender_config.fec_encoder.scheme = + packet::fec_scheme_from_str(args.fec_encoding_arg); + + if (sender_config.fec_encoder.scheme == packet::FEC_None) { + roc_log(LogError, "invalid --fec-encoding"); + return false; + } + } else if (args.source_given) { + address::NetworkUri source_endpoint(context.arena()); if (!address::parse_network_uri( args.source_arg[0], address::NetworkUri::Subset_Full, source_endpoint)) { roc_log(LogError, "can't parse --source endpoint: %s", args.source_arg[0]); - return 1; + return false; } - const address::ProtocolAttrs* source_attrs = address::ProtocolMap::instance().find_by_id(source_endpoint.proto()); if (source_attrs) { @@ -126,52 +170,28 @@ int main(int argc, char** argv) { } } - if (args.nbsrc_given) { + if (args.fec_block_src_given || args.fec_block_rpr_given) { if (sender_config.fec_encoder.scheme == packet::FEC_None) { roc_log(LogError, - "--nbsrc can't be used when --source protocol doesn't support fec)"); - return 1; + "--fec-block-src and --fec-block-rpr can't be used when" + " --source protocol doesn't support fec"); + return false; } - if (args.nbsrc_arg <= 0) { - roc_log(LogError, "invalid --nbsrc: should be > 0"); - return 1; - } - sender_config.fec_writer.n_source_packets = (size_t)args.nbsrc_arg; - } - - if (args.nbrpr_given) { - if (sender_config.fec_encoder.scheme == packet::FEC_None) { + if (!args.fec_block_src_given || !args.fec_block_rpr_given) { roc_log(LogError, - "--nbrpr can't be used when --source protocol doesn't support fec"); - return 1; + "--fec-block-src and --fec-block-rpr should be specified together"); + return false; } - if (args.nbrpr_arg <= 0) { - roc_log(LogError, "invalid --nbrpr: should be > 0"); - return 1; + if (args.fec_block_src_arg <= 0) { + roc_log(LogError, "invalid --fec-block-src: should be > 0"); + return false; } - sender_config.fec_writer.n_repair_packets = (size_t)args.nbrpr_arg; - } - - switch (args.latency_backend_arg) { - case latency_backend_arg_niq: - sender_config.latency.tuner_backend = audio::LatencyTunerBackend_Niq; - break; - default: - break; - } - - switch (args.latency_profile_arg) { - case latency_profile_arg_responsive: - sender_config.latency.tuner_profile = audio::LatencyTunerProfile_Responsive; - break; - case latency_profile_arg_gradual: - sender_config.latency.tuner_profile = audio::LatencyTunerProfile_Gradual; - break; - case latency_profile_arg_intact: - sender_config.latency.tuner_profile = audio::LatencyTunerProfile_Intact; - break; - default: - break; + if (args.fec_block_rpr_arg <= 0) { + roc_log(LogError, "invalid --fec-block-rpr: should be > 0"); + return false; + } + sender_config.fec_writer.n_source_packets = (size_t)args.fec_block_src_arg; + sender_config.fec_writer.n_repair_packets = (size_t)args.fec_block_rpr_arg; } switch (args.resampler_backend_arg) { @@ -205,12 +225,34 @@ int main(int argc, char** argv) { break; } + switch (args.latency_backend_arg) { + case latency_backend_arg_niq: + sender_config.latency.tuner_backend = audio::LatencyTunerBackend_Niq; + break; + default: + break; + } + + switch (args.latency_profile_arg) { + case latency_profile_arg_responsive: + sender_config.latency.tuner_profile = audio::LatencyTunerProfile_Responsive; + break; + case latency_profile_arg_gradual: + sender_config.latency.tuner_profile = audio::LatencyTunerProfile_Gradual; + break; + case latency_profile_arg_intact: + sender_config.latency.tuner_profile = audio::LatencyTunerProfile_Intact; + break; + default: + break; + } + if (args.target_latency_given) { if (sender_config.latency.tuner_profile == audio::LatencyTunerProfile_Intact) { roc_log(LogError, "--target-latency can be specified only" " when --latency-profile is not 'intact'"); - return 1; + return false; } if (strcmp(args.target_latency_arg, "auto") == 0) { sender_config.latency.target_latency = 0; @@ -218,11 +260,11 @@ int main(int argc, char** argv) { if (!core::parse_duration(args.target_latency_arg, sender_config.latency.target_latency)) { roc_log(LogError, "invalid --target-latency: bad format"); - return 1; + return false; } if (sender_config.latency.target_latency <= 0) { roc_log(LogError, "invalid --target-latency: should be > 0"); - return 1; + return false; } } } @@ -232,16 +274,16 @@ int main(int argc, char** argv) { roc_log(LogError, "--latency-tolerance can be specified only" " when --latency-profile is not 'intact'"); - return 1; + return false; } if (!core::parse_duration(args.latency_tolerance_arg, sender_config.latency.latency_tolerance)) { roc_log(LogError, "invalid --latency-tolerance: bad format"); - return 1; + return false; } if (sender_config.latency.latency_tolerance <= 0) { roc_log(LogError, "invalid --latency-tolerance: should be > 0"); - return 1; + return false; } } @@ -250,23 +292,23 @@ int main(int argc, char** argv) { roc_log(LogError, "--start-latency can be specified only" " when --latency-profile is not 'intact'"); - return 1; + return false; } if (sender_config.latency.target_latency != 0) { roc_log( LogError, "--start-latency can be specified only in" " adaptive latency mode (i.e. --target-latency is 'auto' or omitted)"); - return 1; + return false; } if (!core::parse_duration(args.start_latency_arg, sender_config.latency.start_target_latency)) { roc_log(LogError, "invalid --start-latency: bad format"); - return 1; + return false; } if (sender_config.latency.start_target_latency <= 0) { roc_log(LogError, "invalid --start-latency: should be > 0"); - return 1; + return false; } } @@ -275,107 +317,71 @@ int main(int argc, char** argv) { roc_log(LogError, "--min-latency and --max-latency can be specified only" " when --latency-profile is not 'intact'"); - return 1; + return false; } if (sender_config.latency.target_latency != 0) { roc_log( LogError, "--min-latency and --max-latency can be specified only in" " adaptive latency mode (i.e. --target-latency is 'auto' or omitted)"); - return 1; + return false; } if (!args.min_latency_given || !args.max_latency_given) { roc_log(LogError, "--min-latency and --max-latency should be specified together"); - return 1; + return false; } if (!core::parse_duration(args.min_latency_arg, sender_config.latency.min_target_latency)) { roc_log(LogError, "invalid --min-latency: bad format"); - return 1; + return false; } if (sender_config.latency.min_target_latency <= 0) { roc_log(LogError, "invalid --min-latency: should be > 0"); - return 1; + return false; } if (!core::parse_duration(args.max_latency_arg, sender_config.latency.max_target_latency)) { roc_log(LogError, "invalid --max-latency: bad format"); - return 1; + return false; } if (sender_config.latency.max_target_latency <= 0) { roc_log(LogError, "invalid --max-latency: should be > 0"); - return 1; + return false; } if (sender_config.latency.min_target_latency > sender_config.latency.max_target_latency) { roc_log( LogError, "incorrect --max-latency: should be greater or equal to --min-latency"); - return 1; + return false; } } - sender_config.enable_profiling = args.profile_flag; + sender_config.enable_profiling = args.prof_flag; if (args.dump_given) { sender_config.dumper.dump_file = args.dump_arg; } - node::ContextConfig context_config; + sender_config.enable_cpu_clock = !input_source.has_clock(); + sender_config.input_sample_spec = input_source.sample_spec(); - if (args.max_packet_size_given) { - if (!core::parse_size(args.max_packet_size_arg, context_config.max_packet_size)) { - roc_log(LogError, "invalid --max-packet-size: bad format"); - return 1; - } - if (context_config.max_packet_size == 0) { - roc_log(LogError, "invalid --max-packet-size: should be > 0"); - return 1; - } - } else { - audio::SampleSpec spec = io_config.sample_spec; - spec.use_defaults(audio::Sample_RawFormat, audio::ChanLayout_Surround, - audio::ChanOrder_Smpte, audio::ChanMask_Surround_7_1_4, 48000); - context_config.max_packet_size = packet::Packet::approx_size( - spec.ns_2_samples_overall(io_config.frame_length)); - } - - if (args.max_frame_size_given) { - if (!core::parse_size(args.max_frame_size_arg, context_config.max_frame_size)) { - roc_log(LogError, "invalid --max-frame-size: bad format"); - return 1; - } - if (context_config.max_frame_size == 0) { - roc_log(LogError, "invalid --max-frame-size: should be > 0"); - return 1; - } - } - - node::Context context(context_config, heap_arena); - if (context.init_status() != status::StatusOK) { - roc_log(LogError, "can't initialize node context: status=%s", - status::code_to_str(context.init_status())); - return 1; + if (!sender_config.input_sample_spec.is_valid()) { + roc_log(LogError, + "can't detect input encoding, try to set it " + "explicitly with --io-encoding option"); + return false; } - sndio::BackendDispatcher backend_dispatcher( - context.frame_pool(), context.frame_buffer_pool(), context.arena()); - - if (args.list_supported_given) { - if (!dbgio::print_supported(dbgio::Print_Netio | dbgio::Print_Sndio - | dbgio::Print_Audio | dbgio::Print_FEC, - backend_dispatcher, context.arena())) { - return 1; - } - return 0; - } + return true; +} - address::IoUri input_uri(context.arena()); +bool parse_input_uri(const gengetopt_args_info& args, address::IoUri& input_uri) { if (args.input_given) { if (!address::parse_io_uri(args.input_arg, input_uri)) { roc_log(LogError, "invalid --input file or device URI"); - return 1; + return false; } } @@ -383,24 +389,31 @@ int main(int argc, char** argv) { if (input_uri.is_valid() && !input_uri.is_file()) { roc_log(LogError, "--input-format can't be used if --input is not a file URI"); - return 1; + return false; } } else { if (input_uri.is_special_file()) { roc_log(LogError, "--input-format should be specified if --input is \"-\""); - return 1; + return false; } } - core::ScopedPtr input_source; + return true; +} + +bool open_input_source(sndio::BackendDispatcher& backend_dispatcher, + const sndio::IoConfig& io_config, + const address::IoUri& input_uri, + const char* input_format, + core::ScopedPtr& input_source) { if (input_uri.is_valid()) { const status::StatusCode code = backend_dispatcher.open_source( - input_uri, args.input_format_arg, io_config, input_source); + input_uri, input_format, io_config, input_source); if (code != status::StatusOK) { roc_log(LogError, "can't open --input file or device: status=%s", status::code_to_str(code)); - return 1; + return false; } } else { const status::StatusCode code = @@ -409,30 +422,19 @@ int main(int argc, char** argv) { if (code != status::StatusOK) { roc_log(LogError, "can't open default --input device: status=%s", status::code_to_str(code)); - return 1; + return false; } } - sender_config.enable_cpu_clock = !input_source->has_clock(); - sender_config.input_sample_spec = input_source->sample_spec(); - - if (!sender_config.input_sample_spec.is_valid()) { - roc_log(LogError, - "can't detect input encoding, try to set it " - "explicitly with --rate option"); - return 1; - } - - node::Sender sender(context, sender_config); - if (sender.init_status() != status::StatusOK) { - roc_log(LogError, "can't create sender node: status=%s", - status::code_to_str(sender.init_status())); - return 1; - } + return true; +} +bool prepare_sender(const gengetopt_args_info& args, + node::Context& context, + node::Sender& sender) { if (args.source_given == 0) { roc_log(LogError, "at least one --source endpoint should be specified"); - return 1; + return false; } if (args.repair_given != 0 && args.repair_given != args.source_given) { @@ -440,7 +442,7 @@ int main(int argc, char** argv) { "invalid number of --repair endpoints: expected either 0 or %d endpoints" " (one per --source), got %d endpoints", (int)args.source_given, (int)args.repair_given); - return 1; + return false; } if (args.control_given != 0 && args.control_given != args.source_given) { @@ -448,7 +450,15 @@ int main(int argc, char** argv) { "invalid number of --control endpoints: expected either 0 or %d endpoints" " (one per --source), got %d endpoints", (int)args.source_given, (int)args.control_given); - return 1; + return false; + } + + if (args.miface_given != 0 && args.miface_given != args.source_given) { + roc_log(LogError, + "invalid number of --miface values: expected either 0 or %d values" + " (one per --source), got %d values", + (int)args.source_given, (int)args.miface_given); + return false; } for (size_t slot = 0; slot < (size_t)args.source_given; slot++) { @@ -457,20 +467,30 @@ int main(int argc, char** argv) { address::NetworkUri::Subset_Full, source_endpoint)) { roc_log(LogError, "can't parse --source endpoint: %s", args.source_arg[slot]); - return 1; + return false; } netio::UdpConfig iface_config; iface_config.enable_reuseaddr = args.reuseaddr_given; + if (args.miface_given) { + if (strlen(args.miface_arg[slot]) + >= sizeof(iface_config.multicast_interface)) { + roc_log(LogError, "invalid --miface \"%s\": string too long", + args.miface_arg[slot]); + return false; + } + strcpy(iface_config.multicast_interface, args.miface_arg[slot]); + } + if (!sender.configure(slot, address::Iface_AudioSource, iface_config)) { roc_log(LogError, "can't configure --source endpoint"); - return 1; + return false; } if (!sender.connect(slot, address::Iface_AudioSource, source_endpoint)) { roc_log(LogError, "can't connect sender to source endpoint"); - return 1; + return false; } } @@ -480,7 +500,7 @@ int main(int argc, char** argv) { address::NetworkUri::Subset_Full, repair_endpoint)) { roc_log(LogError, "can't parse --repair endpoint: %s", args.repair_arg[slot]); - return 1; + return false; } netio::UdpConfig iface_config; @@ -488,12 +508,12 @@ int main(int argc, char** argv) { if (!sender.configure(slot, address::Iface_AudioRepair, iface_config)) { roc_log(LogError, "can't configure --repair endpoint"); - return 1; + return false; } if (!sender.connect(slot, address::Iface_AudioRepair, repair_endpoint)) { roc_log(LogError, "can't connect sender to repair endpoint"); - return 1; + return false; } } @@ -504,7 +524,7 @@ int main(int argc, char** argv) { control_endpoint)) { roc_log(LogError, "can't parse --control endpoint: %s", args.control_arg[slot]); - return 1; + return false; } netio::UdpConfig iface_config; @@ -512,12 +532,12 @@ int main(int argc, char** argv) { if (!sender.configure(slot, address::Iface_AudioControl, iface_config)) { roc_log(LogError, "can't configure --control endpoint"); - return 1; + return false; } if (!sender.connect(slot, address::Iface_AudioControl, control_endpoint)) { roc_log(LogError, "can't connect sender to control endpoint"); - return 1; + return false; } } @@ -526,24 +546,99 @@ int main(int argc, char** argv) { LogError, "incomplete sender configuration:" " FEC is implied by protocol, but matching --source or --repair is missing"); + return false; + } + + return true; +} + +} // namespace + +int main(int argc, char** argv) { + core::CrashHandler crash_handler; + + core::HeapArena::set_guards(core::HeapArena_DefaultGuards + | core::HeapArena_LeakGuard); + core::HeapArena heap_arena; + + gengetopt_args_info args; + const int code = cmdline_parser(argc, argv, &args); + if (code != 0) { + return code; + } + core::ScopedRelease args_releaser(&args, &cmdline_parser_free); + + init_logger(args); + + sndio::IoConfig io_config; + if (!build_io_config(args, io_config)) { + return 1; + } + + node::ContextConfig context_config; + if (!build_context_config(args, io_config, context_config)) { + return 1; + } + + node::Context context(context_config, heap_arena); + if (context.init_status() != status::StatusOK) { + roc_log(LogError, "can't initialize node context: status=%s", + status::code_to_str(context.init_status())); + return 1; + } + + sndio::BackendDispatcher backend_dispatcher( + context.frame_pool(), context.frame_buffer_pool(), context.arena()); + + if (args.list_supported_given) { + if (!dbgio::print_supported(dbgio::Print_Netio | dbgio::Print_Sndio + | dbgio::Print_Audio | dbgio::Print_FEC, + backend_dispatcher, context.arena())) { + return 1; + } + return 0; + } + + address::IoUri input_uri(context.arena()); + if (!parse_input_uri(args, input_uri)) { + return 1; + } + + core::ScopedPtr input_source; + if (!open_input_source(backend_dispatcher, io_config, input_uri, + args.input_format_arg, input_source)) { + return 1; + } + + io_config.sample_spec = input_source->sample_spec(); + + pipeline::SenderSinkConfig sender_config; + if (!build_sender_config(args, sender_config, context, *input_source)) { return 1; } - sndio::IoConfig pump_config; - pump_config.sample_spec = input_source->sample_spec(); - pump_config.frame_length = io_config.frame_length; + node::Sender sender(context, sender_config); + if (sender.init_status() != status::StatusOK) { + roc_log(LogError, "can't create sender node: status=%s", + status::code_to_str(sender.init_status())); + return 1; + } + + if (!prepare_sender(args, context, sender)) { + return 1; + } sndio::IoPump pump(context.frame_pool(), context.frame_buffer_pool(), *input_source, - NULL, sender.sink(), pump_config, sndio::IoPump::ModePermanent); + NULL, sender.sink(), io_config, sndio::IoPump::ModePermanent); if (pump.init_status() != status::StatusOK) { - roc_log(LogError, "can't create audio pump: status=%s", + roc_log(LogError, "can't create io pump: status=%s", status::code_to_str(pump.init_status())); return 1; } const status::StatusCode status = pump.run(); if (status != status::StatusOK) { - roc_log(LogError, "can't run audio pump: status=%s", status::code_to_str(status)); + roc_log(LogError, "io pump failed: status=%s", status::code_to_str(status)); return 1; }