From 5f8856226313ed3fc369cb017418fe83fea6a359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20S=C3=B6ntgen?= Date: Tue, 27 Feb 2024 08:49:35 +0100 Subject: [PATCH] gems: VFS OSS plugin for Record/Play session The new VFS OSS plugin utilizes the Record and Play session. For the time being it is a drop-in replacement for the old plugin and shares its limitations. In contrast to the old plugin it is possible to force a client to use a configured fragment size. Some clients work best with larger fragments, e.g. VBox, where raising the minimal fragment size is beneficial. Please look at the README file for more information. Issue genodelabs/genode#5167. --- repos/gems/lib/mk/vfs_oss.mk | 9 + repos/gems/lib/symbols/vfs_oss | 1 + repos/gems/recipes/src/vfs_oss/content.mk | 9 + repos/gems/recipes/src/vfs_oss/hash | 1 + repos/gems/recipes/src/vfs_oss/used_apis | 7 + repos/gems/src/lib/vfs/oss/README | 119 ++ repos/gems/src/lib/vfs/oss/symbol.map | 9 + repos/gems/src/lib/vfs/oss/vfs.cc | 1561 +++++++++++++++++++++ 8 files changed, 1716 insertions(+) create mode 100644 repos/gems/lib/mk/vfs_oss.mk create mode 100644 repos/gems/lib/symbols/vfs_oss create mode 100644 repos/gems/recipes/src/vfs_oss/content.mk create mode 100644 repos/gems/recipes/src/vfs_oss/hash create mode 100644 repos/gems/recipes/src/vfs_oss/used_apis create mode 100644 repos/gems/src/lib/vfs/oss/README create mode 100644 repos/gems/src/lib/vfs/oss/symbol.map create mode 100644 repos/gems/src/lib/vfs/oss/vfs.cc diff --git a/repos/gems/lib/mk/vfs_oss.mk b/repos/gems/lib/mk/vfs_oss.mk new file mode 100644 index 0000000000..0882ae148a --- /dev/null +++ b/repos/gems/lib/mk/vfs_oss.mk @@ -0,0 +1,9 @@ +VFS_DIR := $(REP_DIR)/src/lib/vfs/oss + +SRC_CC := vfs.cc + +LD_OPT += --version-script=$(VFS_DIR)/symbol.map + +SHARED_LIB := yes + +vpath %.cc $(VFS_DIR) diff --git a/repos/gems/lib/symbols/vfs_oss b/repos/gems/lib/symbols/vfs_oss new file mode 100644 index 0000000000..faa4598066 --- /dev/null +++ b/repos/gems/lib/symbols/vfs_oss @@ -0,0 +1 @@ +vfs_file_system_factory T diff --git a/repos/gems/recipes/src/vfs_oss/content.mk b/repos/gems/recipes/src/vfs_oss/content.mk new file mode 100644 index 0000000000..6270d89f68 --- /dev/null +++ b/repos/gems/recipes/src/vfs_oss/content.mk @@ -0,0 +1,9 @@ +MIRROR_FROM_REP_DIR := lib/mk/vfs_oss.mk lib/symbols/vfs_oss src/lib/vfs/oss + +content: $(MIRROR_FROM_REP_DIR) LICENSE + +$(MIRROR_FROM_REP_DIR): + $(mirror_from_rep_dir) + +LICENSE: + cp $(GENODE_DIR)/LICENSE $@ diff --git a/repos/gems/recipes/src/vfs_oss/hash b/repos/gems/recipes/src/vfs_oss/hash new file mode 100644 index 0000000000..ba09802bbe --- /dev/null +++ b/repos/gems/recipes/src/vfs_oss/hash @@ -0,0 +1 @@ +2024-03-28 df663c798d40deae61ff8807c4bf5d3a4dc8eb18 diff --git a/repos/gems/recipes/src/vfs_oss/used_apis b/repos/gems/recipes/src/vfs_oss/used_apis new file mode 100644 index 0000000000..e9f6e0a568 --- /dev/null +++ b/repos/gems/recipes/src/vfs_oss/used_apis @@ -0,0 +1,7 @@ +base +gems +os +play_session +record_session +so +vfs diff --git a/repos/gems/src/lib/vfs/oss/README b/repos/gems/src/lib/vfs/oss/README new file mode 100644 index 0000000000..ca57f549ac --- /dev/null +++ b/repos/gems/src/lib/vfs/oss/README @@ -0,0 +1,119 @@ +The VFS OSS plugin offers access to Genode's Record and Play sessions by +providing a file-system that can be mounted at arbitrary location within the +VFS of a component. It exposes a data file that can be used as 'dsp' file, +e.g., _/dev/dsp_ as is common with OSS. The support I/O control operations or +rather the properties of the pseudo-device are provided in form of a structured +'info' file located in the directory named after the data file, e.g., +_/dev/.dsp/info_. + +This file may by used to query the configured parameters and has the following +structure: + +! + +Each parameter can also be accessed via its own file. The following list +presents all files: + + * :channels (ro): number of available channels. Set to 2 (stereo). + Corresponding OSS commands: 'SNDCTL_DSP_CHANNELS' + + * :format (ro): sample format, e.g. s16le. Defaults to AFMT_S16_LE. + Corresponding OSS commands: 'SNDCTL_DSP_SAMPLESIZE' + + * :sample_rate (ro): sample rate of the underlying Audio_out session. + Corresponding OSS commands: 'SNDCTL_DSP_SPEED' + + * :ifrag_total (rw): total number of input fragments. Set to number of + packets in the underlying Audio_in session's packet-stream by default. + Corresponding OSS commands: 'SNDCTL_DSP_SETFRAGMENT', + 'SNDCTL_DSP_GETISPACE' + + * :ifrag_size (rw): size of an input fragment. Set to 2048 (number of + channels times size of Audio_in period times size of s16le sample) by + default. + Corresponding OSS commands: 'SNDCTL_DSP_SETFRAGMENT', + 'SNDCTL_DSP_GETISPACE' + + * :ifrag_avail (ro): number of available input fragments. Initially set to 0. + Corresponding OSS commands: 'SNDCTL_DSP_GETISPACE' + + * :ifrag_bytes (ro): number of available input bytes. Initially set to 0. + Corresponding OSS commands: 'SNDCTL_DSP_GETISPACE' + + * :enable_input (rw): writing 1 or 0 into this file enables or disables + input processing. + Corresponding OSS commands: SNDCTL_DSP_SETTRIGGER + + * :halt_input (wo): writing anything into this file halts input processing. + Corresponding OSS commands: SNDCTL_DSP_HALT + + * :ofrag_total (rw): total number of output fragments. Set to number of + packets in the underlying Audio_out session's packet-stream by default. + Corresponding OSS commands: 'SNDCTL_DSP_SETFRAGMENT', + 'SNDCTL_DSP_GETOSPACE' + + * :ofrag_size (rw): size of an output fragment. Set to 2048 (number of + channels times size of Audio_out period times size of s16le sample) by + default. + Corresponding OSS commands: 'SNDCTL_DSP_SETFRAGMENT', + 'SNDCTL_DSP_GETOSPACE' + + * :ofrag_avail (ro): number of available output fragments. Initially set to + total fragment count. + Corresponding OSS commands: 'SNDCTL_DSP_GETOSPACE' + + * :ofrag_bytes (ro): number of available output bytes. Initially set to + total count buffer space. + Corresponding OSS commands: 'SNDCTL_DSP_GETOSPACE' + + * :optr_samples (ro): total number of samples enqueued in the Play session + Corresponding OSS commands: 'SNDCTL_DSP_CURRENT_OPTR' + + * :optr_fifo_samples (ro): number of samples residing in the internal output + buffer + Corresponding OSS commands: 'SNDCTL_DSP_CURRENT_OPTR' + + * :play_underruns (rw): number of detected underrun errors, Writing anything + into this file resets the value to zero + Corresponding OSS commands: 'SNDCTL_DSP_GETERROR' + + * :enable_output (rw): writing 1 or 0 into this file enables or disables + output processing. + Corresponding OSS commands: SNDCTL_DSP_SETTRIGGER + + * :halt_output (wo): writing anything into this file halts output processing. + Corresponding OSS commands: SNDCTL_DSP_HALT + +In its current state the plugin is merely enough to use simple applications +requiring nothing more than a minimal set of the OSSv4 API. It does not allow +altering of all parameters and will only work when 44100Hz/s16le is used. + +The following '' attributes can be used to alter the behaviour of +the plugin: + + * :verbose: if set to 'true' diagnostic message will be printed to the log, + it defaults to 'false' + + * :max_ofrag_size: sets the maximal allowed output fragment size, defaults + to 8192 that correspondes to 2048 samples (~46,4 ms) at stereo 44.1 kHz + + * :min_ofrag_size: sets the minimal allowed output fragment size, defaults + to 2048 that correspondes to 512 samples (~11,6 ms) at stereo 44.1 kHz + + * :max_ifrag_size: sets the maximal allowed input fragment size, defaults + to 8192 that correspondes to 2048 samples (~46,4 ms) at stereo 44.1 kHz + + * :min_ifrag_size: sets the minimal allowed input fragment size, defaults + to 2048 that correspondes to 512 samples (~11,6 ms) at stereo 44.1 kHz + + +The following config snippet illustrates its configuration: + +! +! +! +! +! diff --git a/repos/gems/src/lib/vfs/oss/symbol.map b/repos/gems/src/lib/vfs/oss/symbol.map new file mode 100644 index 0000000000..58e4ff57a8 --- /dev/null +++ b/repos/gems/src/lib/vfs/oss/symbol.map @@ -0,0 +1,9 @@ +{ + global: + + vfs_file_system_factory; + + local: + + *; +}; diff --git a/repos/gems/src/lib/vfs/oss/vfs.cc b/repos/gems/src/lib/vfs/oss/vfs.cc new file mode 100644 index 0000000000..5f737ae44a --- /dev/null +++ b/repos/gems/src/lib/vfs/oss/vfs.cc @@ -0,0 +1,1561 @@ +/* + * \brief OSS to Record and Play session translator plugin + * \author Josef Soentgen + * \date 2024-02-20 + */ + +/* + * Copyright (C) 2024 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* Genode includes */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr bool VERBOSE = false; + +namespace Vfs { + using namespace Genode; + + struct Oss_file_system; +} /* namespace Vfs */ + + +struct Vfs::Oss_file_system +{ + using Name = String<32>; + + struct Audio; + + struct Data_file_system; + struct Local_factory; + struct Compound_file_system; +}; + + +struct Vfs::Oss_file_system::Audio +{ + public: + + struct Info + { + unsigned channels; + unsigned format; + unsigned sample_rate; + unsigned ifrag_total; + unsigned ifrag_size; + unsigned ifrag_avail; + unsigned ifrag_bytes; + unsigned ofrag_total; + unsigned ofrag_size; + unsigned ofrag_avail; + unsigned ofrag_bytes; + long long optr_samples; + unsigned optr_fifo_samples; + unsigned play_underruns; + + Readonly_value_file_system &_channels_fs; + Readonly_value_file_system &_format_fs; + Readonly_value_file_system &_sample_rate_fs; + Value_file_system &_ifrag_total_fs; + Value_file_system &_ifrag_size_fs; + Readonly_value_file_system &_ifrag_avail_fs; + Readonly_value_file_system &_ifrag_bytes_fs; + Value_file_system &_ofrag_total_fs; + Value_file_system &_ofrag_size_fs; + Readonly_value_file_system &_ofrag_avail_fs; + Readonly_value_file_system &_ofrag_bytes_fs; + Readonly_value_file_system &_optr_samples_fs; + Readonly_value_file_system &_optr_fifo_samples_fs; + Value_file_system &_play_underruns_fs; + + Info(Readonly_value_file_system &channels_fs, + Readonly_value_file_system &format_fs, + Readonly_value_file_system &sample_rate_fs, + Value_file_system &ifrag_total_fs, + Value_file_system &ifrag_size_fs, + Readonly_value_file_system &ifrag_avail_fs, + Readonly_value_file_system &ifrag_bytes_fs, + Value_file_system &ofrag_total_fs, + Value_file_system &ofrag_size_fs, + Readonly_value_file_system &ofrag_avail_fs, + Readonly_value_file_system &ofrag_bytes_fs, + Readonly_value_file_system &optr_samples_fs, + Readonly_value_file_system &optr_fifo_samples_fs, + Value_file_system &play_underruns_fs) + : + channels { 0 }, + format { 0 }, + sample_rate { 0 }, + ifrag_total { 0 }, + ifrag_size { 0 }, + ifrag_avail { 0 }, + ifrag_bytes { 0 }, + ofrag_total { 0 }, + ofrag_size { 0 }, + ofrag_avail { 0 }, + ofrag_bytes { 0 }, + optr_samples { 0 }, + optr_fifo_samples { 0 }, + play_underruns { 0 }, + _channels_fs { channels_fs }, + _format_fs { format_fs }, + _sample_rate_fs { sample_rate_fs }, + _ifrag_total_fs { ifrag_total_fs }, + _ifrag_size_fs { ifrag_size_fs }, + _ifrag_avail_fs { ifrag_avail_fs }, + _ifrag_bytes_fs { ifrag_bytes_fs }, + _ofrag_total_fs { ofrag_total_fs }, + _ofrag_size_fs { ofrag_size_fs }, + _ofrag_avail_fs { ofrag_avail_fs }, + _ofrag_bytes_fs { ofrag_bytes_fs }, + _optr_samples_fs { optr_samples_fs }, + _optr_fifo_samples_fs { optr_fifo_samples_fs }, + _play_underruns_fs { play_underruns_fs } + { } + + void update() + { + _channels_fs .value(channels); + _format_fs .value(format); + _sample_rate_fs .value(sample_rate); + _ifrag_total_fs .value(ifrag_total); + _ifrag_size_fs .value(ifrag_size); + _ifrag_avail_fs .value(ifrag_avail); + _ifrag_bytes_fs .value(ifrag_bytes); + _ofrag_total_fs .value(ofrag_total); + _ofrag_size_fs .value(ofrag_size); + _ofrag_avail_fs .value(ofrag_avail); + _ofrag_bytes_fs .value(ofrag_bytes); + _optr_samples_fs .value(optr_samples); + _optr_fifo_samples_fs.value(optr_fifo_samples); + _play_underruns_fs .value(play_underruns); + } + + void print(Genode::Output &out) const + { + char buf[512] { }; + + Genode::Xml_generator xml(buf, sizeof(buf), "oss", [&] () { + xml.attribute("channels", channels); + xml.attribute("format", format); + xml.attribute("sample_rate", sample_rate); + xml.attribute("ifrag_total", ifrag_total); + xml.attribute("ifrag_size", ifrag_size); + xml.attribute("ifrag_avail", ifrag_avail); + xml.attribute("ifrag_bytes", ifrag_bytes); + xml.attribute("ofrag_total", ofrag_total); + xml.attribute("ofrag_size", ofrag_size); + xml.attribute("ofrag_avail", ofrag_avail); + xml.attribute("ofrag_bytes", ofrag_bytes); + xml.attribute("optr_samples", optr_samples); + xml.attribute("optr_fifo_samples", optr_fifo_samples); + xml.attribute("play_underruns", play_underruns); + }); + + Genode::print(out, Genode::Cstring(buf)); + } + }; + + using Read_result = Vfs::File_io_service::Read_result; + using Write_result = Vfs::File_io_service::Write_result; + + /* + * Simple sample buffer used for storing play samples given + * by the client in float and record samples in int16_t given + * by the mixer. + * + * The used indicator variable is implicitly guarded by + * the calling code checking read/write avail beforehand. + */ + template + struct Sample_buffer_base + { + static constexpr unsigned SIZE = 1u << SIZE_LOG2, + MASK = SIZE - 1; + T _samples[SIZE] { }; + + unsigned _rpos = 0; + unsigned _wpos = 0; + unsigned _used = 0; + + void reset() + { + _rpos = _wpos = _used = 0; + } + + void insert(T value) + { + _used++; + _wpos = (_wpos + 1) & MASK; + _samples[_wpos] = value; + } + + T remove() + { + _used--; + _rpos = (_rpos + 1) & MASK; + return _samples[_rpos]; + } + + bool read_samples_avail(unsigned const min) const { + return _used >= min; } + + bool write_samples_avail(unsigned const min) const { + return SIZE - _used >= min; } + + unsigned used() const { return _used; } + + unsigned used_bytes() const { return _used * sizeof(T); } + + size_t sample_size() const { return sizeof(T); } + }; + + struct Periodic_timer + { + Timer::Connection _timer; + bool _started; + + Periodic_timer(Genode::Env &env) + : _timer { env }, _started { false } { } + + void sigh(Signal_context_capability cap) { + _timer.sigh(cap); } + + void start(unsigned const duration_us) + { + _timer.trigger_periodic(duration_us); + _started = true; + } + + void stop() + { + _timer.trigger_periodic(0); + _started = false; + } + + bool started() const { return _started; } + }; + + private: + + Audio(Audio const &); + Audio &operator = (Audio const &); + + Vfs::Env &_vfs_env; + + Info &_info; + Readonly_value_file_system &_info_fs; + + unsigned _frame_size { 0 }; + + static unsigned _format_size(unsigned fmt) + { + if (fmt == 0x00000010u) /* S16LE */ + return 2u; + + return 0u; + } + + void _with_duration(size_t const bytes, auto const &fn) + { + unsigned const samples = (unsigned)bytes / _frame_size; + float const tmp_duration = float(1'000'000u) + / float(_info.sample_rate) + * float(samples); + + fn(unsigned(tmp_duration), samples); + } + + /************ + ** Output ** + ************/ + + struct Stereo_output + { + using Sample_buffer = Sample_buffer_base; + + struct Channel { unsigned value; }; + + struct Num_samples { unsigned value; }; + + static constexpr unsigned CHANNELS = 2u; + static constexpr float const SCALE = 1.0f/32768; + + Genode::Env &_env; + + Constructible _session [CHANNELS] { }; + Sample_buffer _session_buffer[CHANNELS] { }; + Periodic_timer _timer { _env }; + Play::Time_window _time_window { }; + bool _started { false }; + + /* runtime parameters */ + Play::Duration _duration { 0 }; + Num_samples _samples { 0 }; + unsigned _underrun_limit { 0 }; + + void _for_each_sample(Sample_buffer &buffer, + Num_samples const samples, + auto const &fn) + { + for (unsigned i = 0; i < samples.value; i++) + fn(buffer.remove()); + } + + void _for_each_session(auto const &fn) + { + for (auto & session : _session) + if (session.constructed()) + fn(*session); + } + + void _create_sessions(Genode::Env &env) + { + /* for now force stereo only */ + if (CHANNELS != 2) { + struct Unsupported_channel_number : Genode::Exception { }; + throw Unsupported_channel_number(); + } + + char const * const channel_map[CHANNELS] = { "left", "right" }; + + for (unsigned i = 0; i < CHANNELS; i++) + _session[i].construct(env, channel_map[i]); + } + + void _destroy_sessions() + { + for (unsigned i = 0; i < CHANNELS; i++) + _session[i].destruct(); + } + + Stereo_output(Genode::Env &env) : _env { env } { } + + void update_parameters(Play::Duration const duration, + Num_samples const samples) + { + _duration = duration; + _samples = samples; + + _underrun_limit = 1'000'000u / _duration.us; + } + + void schedule_and_enqueue() + { + bool first = true; + + if (!_session[0].constructed()) + _create_sessions(_env); + + unsigned buffer_idx = 0; + _for_each_session([&] (Play::Connection & session) { + if (first) { + _time_window = session.schedule_and_enqueue(_time_window, _duration, + [&] (auto &submit) { + _for_each_sample(_session_buffer[buffer_idx], _samples, + [&] (float const v) { submit(v); }); }); + first = false; + } else { + session.enqueue(_time_window, + [&] (auto &submit) { + _for_each_sample(_session_buffer[buffer_idx], _samples, + [&] (float const v) { submit(v); }); }); + } + + ++buffer_idx; + }); + } + + void consume(Channel const channel, + Const_byte_range_ptr const &src, + Num_samples const src_samples) + { + int16_t const *data = reinterpret_cast(src.start); + + for (unsigned i = 0; i < src_samples.value; i++) { + float const v = SCALE * float(data[i * CHANNELS + channel.value]); + _session_buffer[channel.value].insert(v); + } + } + + void halt() + { + _timer.stop(); + + _for_each_session([&] (Play::Connection & session) { + session.stop(); + }); + + _destroy_sessions(); + + for (auto & buffer : _session_buffer) + buffer.reset(); + + _time_window = Play::Time_window { }; + } + + void timer_sigh(Signal_context_capability cap) { + _timer.sigh(cap); } + + void timer_start() { + _timer.start(_duration.us); } + + bool timer_started() const { + return _timer.started(); } + + void play_started(bool start) { + _started = start; } + + bool play_started() const { + return _started; } + + bool samples_avail(unsigned const samples) const { + return _session_buffer[0].read_samples_avail(samples); } + + bool space_avail(unsigned const samples) const { + return _session_buffer[0].write_samples_avail(samples); } + + unsigned samples_per_channel() const { return _samples.value; } + + unsigned underrun_limit() const { return _underrun_limit; } + }; + + Constructible _stereo_output { }; + + void _with_stereo_output(auto const &fn) + { + if (_stereo_output.constructed()) + fn(*_stereo_output); + } + + void _with_stereo_output(auto const &fn) const + { + if (_stereo_output.constructed()) + fn(*_stereo_output); + } + + bool _try_schedule_and_enqueue(Stereo_output &output) + { + if (!output.samples_avail(output.samples_per_channel())) + return false; + + output.play_started(true); + + if (!output.timer_started()) + output.timer_start(); + + output.schedule_and_enqueue(); + + /* + * For now we ignore 'optr_samples' altogether but we + * could use it later on to denote the samples currently + * played while 'optr_fifo_samples' sums up the samples + * in the ring-buffer. + * + * XXX optr_fifo_samples currently represents optr_samples + */ + _info.optr_fifo_samples += output.samples_per_channel(); + _update_output_info(); + + return true; + } + + void _try_starting_schedule_and_enqueue(Stereo_output &output) + { + if (!output.play_started()) + (void)_try_schedule_and_enqueue(output); + } + + void _halt_output(Stereo_output &output) + { + output.halt(); + output.play_started(false); + } + + void _update_output_info() + { + _info.ofrag_bytes = unsigned((_info.ofrag_total * _info.ofrag_size) + - (_info.optr_fifo_samples * _frame_size)); + _info.ofrag_avail = _info.ofrag_bytes / _info.ofrag_size; + + _info.update(); + _info_fs.value(_info); + } + + /*********** + ** Input ** + ***********/ + + struct Stereo_input + { + using Sample_buffer = Sample_buffer_base; + + static constexpr unsigned CHANNELS = 2u; + + Genode::Env &_env; + + struct Duration { unsigned us; }; + + Constructible _session [CHANNELS] { }; + Sample_buffer _session_buffer[CHANNELS] { }; + Periodic_timer _timer { _env }; + + /* runtime parameters */ + Duration _timer_duration { 0 }; + Record::Num_samples _num_samples { 0 }; + + void _for_each_record_session(auto const &fn) + { + for (auto & session : _session) + if (session.constructed()) + fn(*session); + } + + void _create_sessions(Genode::Env &env) + { + /* for now force stereo input */ + if (CHANNELS != 2) { + struct Unsupported_channel_number : Genode::Exception { }; + throw Unsupported_channel_number(); + } + + char const * const channel_map[CHANNELS] = { "left", "right" }; + + for (unsigned i = 0; i < CHANNELS; i++) + _session[i].construct(env, channel_map[i]); + } + + void _destroy_sessions() + { + for (unsigned i = 0; i < CHANNELS; i++) + _session[i].destruct(); + } + + Stereo_input(Genode::Env &env) : _env { env } { } + + void update_parameters(Duration const duration, + Record::Num_samples const num_samples) + { + _timer_duration = duration; + _num_samples = num_samples; + } + + void halt() + { + _timer.stop(); + + for (auto & buffer : _session_buffer) + buffer.reset(); + + _destroy_sessions(); + } + + enum class Record_result { RECORD_OK, RECORD_UNDERRUN, RECORD_OVERRUN }; + + Record_result record() + { + if (!_session[0].constructed()) + _create_sessions(_env); + + if (!_session_buffer[0].write_samples_avail(_num_samples.value())) + return Record_result::RECORD_OVERRUN; + + auto clamped = [&] (float v) + { + return (v > 1.0) ? 1.0 + : (v < -1.0) ? -1.0 + : v; + }; + + auto float_to_s16 = [&] (float v) { return int16_t(clamped(v)*32767); }; + + bool depleted = false; + _session[0]->record(_num_samples, + [&] (Record::Time_window const tw, + Record::Connection::Samples_ptr const &samples) { + for (unsigned i = 0; i < _num_samples.value(); i++) + _session_buffer[0].insert(float_to_s16(samples.start[i])); + + _session[1]->record_at(tw, _num_samples, + [&] (Record::Connection::Samples_ptr const &samples) { + for (unsigned i = 0; i < _num_samples.value(); i++) + _session_buffer[1].insert(float_to_s16(samples.start[i])); + }); + }, + [&] { depleted = true; }); + + return depleted ? Record_result::RECORD_UNDERRUN : Record_result::RECORD_OK; + } + + size_t produce(Byte_range_ptr const &dst, size_t const length) + { + unsigned const samples = unsigned(length + / (CHANNELS * _session_buffer[0].sample_size())); + + int16_t *data = reinterpret_cast(dst.start); + for (unsigned i = 0; i < samples; i++) { + data[i*CHANNELS+0] = _session_buffer[0].remove(); + data[i*CHANNELS+1] = _session_buffer[1].remove(); + } + + return length; + } + + void timer_sigh(Signal_context_capability cap) { + _timer.sigh(cap); } + + void timer_start() { + _timer.start(_timer_duration.us); } + + bool timer_started() const { + return _timer.started(); } + + unsigned bytes_avail() const { return _session_buffer[0].used_bytes() * CHANNELS; } + }; + + Constructible _stereo_input { }; + + void _with_input(auto const &fn) + { + if (_stereo_input.constructed()) + fn(*_stereo_input); + } + + void _with_input(auto const &fn) const + { + if (_stereo_input.constructed()) + fn(*_stereo_input); + } + + void _try_record(Stereo_input &input) + { + if (!input.timer_started()) + input.timer_start(); + + using Record_result = Stereo_input::Record_result; + + Record_result const result = input.record(); + switch (result) { + case Record_result::RECORD_OK: + break; + case Record_result::RECORD_UNDERRUN: + warning("underrun while recording"); + break; + case Record_result::RECORD_OVERRUN: + warning("overrun while recording"); + input.halt(); + break; + } + + _info.ifrag_bytes = input.bytes_avail(); + _update_input_info(); + } + + void _halt_input(Stereo_input &input) + { + _info.ifrag_bytes = 0; + input.halt(); + } + + void _update_input_info() + { + _info.ifrag_avail = _info.ifrag_bytes / _info.ifrag_size; + + _info.update(); + _info_fs.value(_info); + } + + struct Config + { + enum : unsigned { + + FRAGS_TOTAL = 4u, + FRAGS_QUEUED = FRAGS_TOTAL / 2, + + /* 512 S16LE stereo -> 11.6 ms at 44.1kHz */ + MIN_OFRAG_SIZE = 2048u, + /* 2048 S16LE stereo -> 46.4 ms at 44.1kHz */ + MAX_OFRAG_SIZE = 8192u, + + MIN_IFRAG_SIZE = MIN_OFRAG_SIZE, + MAX_IFRAG_SIZE = MAX_OFRAG_SIZE, + }; + + bool verbose; + + unsigned frags_total; + unsigned frags_queued; + + bool play_enabled; + unsigned max_ofrag_size; + unsigned min_ofrag_size; + + bool record_enabled; + unsigned max_ifrag_size; + unsigned min_ifrag_size; + + void print(Genode::Output &out) const + { + Genode::print(out, "verbose: ", verbose, " " + "play_enabled: ", play_enabled, " " + "min_ofrag_size: ", min_ofrag_size, " " + "max_ofrag_size: ", max_ofrag_size, " " + "record_enabled: ", record_enabled, " " + "min_ifrag_size: ", min_ifrag_size, " " + "max_ifrag_size: ", max_ifrag_size, " "); + } + + static Config from_xml(Xml_node const &); + }; + + Config const _config; + + public: + + Audio(Vfs::Env &env, + Info &info, + Readonly_value_file_system &info_fs, + Xml_node config) + : + _vfs_env { env }, + _info { info }, + _info_fs { info_fs }, + _config { Config::from_xml(config) } + { + log("OSS: ", _config); + + /* hard-code initial values for now */ + _info.channels = 2u; + _info.format = (unsigned)0x00000010; /* S16LE */ + _info.sample_rate = 44'100u; + + _frame_size = _info.channels * _format_size(_info.format); + + if (_config.play_enabled) { + _stereo_output.construct(_vfs_env.env()); + + _info.ofrag_size = _config.min_ofrag_size; + _info.ofrag_total = _config.frags_total; + _info.ofrag_avail = _info.ofrag_total; + _info.ofrag_bytes = _info.ofrag_avail * _info.ofrag_size; + + update_output_duration(_info.ofrag_size); + } + + if (_config.record_enabled) { + _stereo_input.construct(_vfs_env.env()); + + _info.ifrag_size = _config.min_ifrag_size; + _info.ifrag_total = _config.frags_total; + _info.ifrag_avail = 0; + _info.ifrag_bytes = 0; + + update_input_duration(_info.ifrag_size); + } + + _info.update(); + _info_fs.value(_info); + } + + bool verbose() const { return _config.verbose; } + + unsigned frags_total() const { return _config.frags_total; } + + /******************** + ** Record session ** + ********************/ + + unsigned max_ifrag_size() const { return _config.max_ifrag_size; } + + unsigned min_ifrag_size() const { return _config.min_ifrag_size; } + + void update_input_duration(unsigned const bytes) + { + _with_input([&] (Stereo_input &input) { + _with_duration(bytes, [&] (unsigned const duration, unsigned const samples) { + input.update_parameters(Stereo_input::Duration { duration }, + Record::Num_samples { samples }); + }); + }); + } + + void record_timer_sigh(Signal_context_capability cap) + { + _with_input([&] (Stereo_input &input) { + input.timer_sigh(cap); }); + } + + bool handle_record_timer() + { + bool result = false; + _with_input([&] (Stereo_input &input) { + _try_record(input); + result = true; + }); + return result; + } + + void enable_input(bool enable) + { + if (_config.verbose) + log(__func__, ": ", enable ? "on" : "off"); + + _with_input([&] (Stereo_input &input) { + if (enable == false) _halt_input(input); + else _try_record(input); + }); + } + + bool read_ready() const + { + if (!_config.record_enabled) + return false; + + bool result = false; + _with_input([&] (Stereo_input const &input) { + result = input.bytes_avail() >= _info.ifrag_size; + }); + return result; + } + + Read_result read(Byte_range_ptr const &dst, size_t &out_size) + { + if (!_config.record_enabled) + return Read_result::READ_ERR_INVALID; + + Read_result result = Read_result::READ_ERR_IO; + _with_input([&] (Stereo_input &input) { + + /* get the ball rolling on first read */ + if (!input.timer_started()) + _try_record(input); + + /* + * Wait until we have at least on input fragment + * available + */ + + unsigned const avail = input.bytes_avail(); + if (avail < _info.ifrag_size) { + result = Read_result::READ_QUEUED; + return; + } + + size_t const length = min((size_t)_info.ifrag_size, + dst.num_bytes); + out_size = input.produce(dst, length); + + _info.ifrag_bytes = input.bytes_avail(); + _update_input_info(); + result = Read_result::READ_OK; + }); + return result; + } + + /****************** + ** Play session ** + ******************/ + + unsigned max_ofrag_size() const { return _config.max_ofrag_size; } + + unsigned min_ofrag_size() const { return _config.min_ofrag_size; } + + void update_output_duration(unsigned const bytes) + { + _with_stereo_output([&] (Stereo_output &output) { + _with_duration(bytes, [&] (unsigned const duration, unsigned const samples) { + output.update_parameters(Play::Duration { duration }, + Stereo_output::Num_samples { samples }); + }); + }); + } + + void play_timer_sigh(Signal_context_capability cap) { + _stereo_output->timer_sigh(cap); } + + bool handle_play_timer() + { + bool enqueued = false; + _with_stereo_output([&] (Stereo_output &output) { + + /* + * We may encountered an underrun when the timer triggered + * the last time. At this point fifo samples has already + * been zero, only decrement it when something was played. + */ + if (_info.optr_fifo_samples) { + _info.optr_fifo_samples -= output.samples_per_channel(); + _update_output_info(); + } + + enqueued = _try_schedule_and_enqueue(output); + if (!enqueued) { + + _info.play_underruns++; + + if (_info.play_underruns >= output.underrun_limit()) { + warning("hit underrun limit (", output.underrun_limit(), + ") - stopping playback"); + _halt_output(output); + _info.play_underruns = 0; + } + } + }); + return enqueued; + } + + void enable_output(bool enable) + { + if (_config.verbose) + log(__func__, ": ", enable ? "on" : "off"); + + _with_stereo_output([&] (Stereo_output &output) { + if (enable == false) _halt_output(output); + else _try_starting_schedule_and_enqueue(output); + }); + } + + bool write_ready() const + { + bool result = false; + _with_stereo_output([&] (Stereo_output const &output) { + unsigned const samples_per_channel = output.samples_per_channel(); + result = output.space_avail ( samples_per_channel) + && !output.samples_avail(_config.frags_queued * samples_per_channel); + }); + return result; + } + + Write_result write(Const_byte_range_ptr const &src, size_t &out_size) + { + out_size = 0; + + auto sample_count = [&] (Const_byte_range_ptr const &range) { + return (unsigned)range.num_bytes / _frame_size; }; + + unsigned const samples = sample_count(src); + + Write_result result = Write_result::WRITE_ERR_IO; + + _with_stereo_output([&] (Stereo_output &output) { + + /* treat a full buffer and enough buffered in the same way */ + if (!output.space_avail(samples)) { + result = Write_result::WRITE_ERR_WOULD_BLOCK; + return; + } + + if (output.samples_avail(_config.frags_queued * output.samples_per_channel())) { + result = Write_result::WRITE_ERR_WOULD_BLOCK; + return; + } + + for (unsigned i = 0; i < _info.channels; i++) + output.consume(Stereo_output::Channel { i }, src, + Stereo_output::Num_samples { samples }); + + /* + * Kick-off playback at the first complete fragment, afterwards + * this is a NOP as the periodic timer handles further + * scheduling. + */ + _try_starting_schedule_and_enqueue(output); + + out_size = src.num_bytes; + result = Write_result::WRITE_OK; + }); + + return result; + } +}; + + +Vfs::Oss_file_system::Audio::Config +Vfs::Oss_file_system::Audio::Config::from_xml(Xml_node const &config) +{ + auto default_size = [&] (Xml_node const &config, + char const * const attr, + unsigned const value) { + return config.attribute_value(attr, value); }; + + auto cap_max = [&] (Xml_node const &config, + char const * const attr, + unsigned const default_value) { + return min(default_size(config, attr, default_value), + default_value); }; + + auto cap_min = [&] (Xml_node const &config, + char const * const attr, + unsigned const default_value) { + return max(default_size(config, attr, default_value), + default_value); }; + + auto limit = [&] (unsigned const value, unsigned const max_value) { + return value > max_value ? max_value : value; }; + + /* constrain frag sizes to [min, max] */ + return { + .verbose = config.attribute_value("verbose", VERBOSE), + + /* hard-coded for now */ + .frags_total = FRAGS_TOTAL, + .frags_queued = FRAGS_QUEUED, + + .play_enabled = config.attribute_value("play_enabled", true), + .max_ofrag_size = cap_max(config, "max_ofrag_size", MAX_OFRAG_SIZE), + .min_ofrag_size = limit(cap_min(config, "min_ofrag_size", MIN_OFRAG_SIZE), + MAX_OFRAG_SIZE), + + .record_enabled = config.attribute_value("record_enabled", true), + .max_ifrag_size = cap_max(config, "max_ifrag_size", MAX_IFRAG_SIZE), + .min_ifrag_size = limit(cap_min(config, "min_ifrag_size", MIN_IFRAG_SIZE), + MAX_IFRAG_SIZE) + }; +} + + +class Vfs::Oss_file_system::Data_file_system : public Single_file_system +{ + private: + + Data_file_system(Data_file_system const &); + Data_file_system &operator = (Data_file_system const &); + + Entrypoint &_ep; + Vfs::Env::User &_vfs_user; + Audio &_audio; + + struct Oss_vfs_handle : public Single_vfs_handle + { + Audio &_audio; + + bool _rd_or_rdwr() const + { + return status_flags() == STATUS_RDONLY + || status_flags() == STATUS_RDWR; + } + + bool _wr_or_rdwr() const + { + return status_flags() == STATUS_WRONLY + || status_flags() == STATUS_RDWR; + } + + Oss_vfs_handle(Directory_service &ds, + File_io_service &fs, + Allocator &alloc, + Audio &audio, + int flags) + : + Single_vfs_handle { ds, fs, alloc, flags }, + _audio { audio } + { } + + ~Oss_vfs_handle() + { + if (_rd_or_rdwr()) + _audio.enable_input(false); + + if (_wr_or_rdwr()) + _audio.enable_output(false); + } + + Read_result read(Byte_range_ptr const &dst, + size_t &out_count) override { + return _audio.read(dst, out_count); } + + Write_result write(Const_byte_range_ptr const &src, + size_t &out_count) override { + return _audio.write(src, out_count); } + + bool read_ready() const override { + return _audio.read_ready(); } + + bool write_ready() const override { + return _audio.write_ready(); } + }; + + using Registered_handle = Genode::Registered; + using Handle_registry = Genode::Registry; + + Handle_registry _handle_registry { }; + + Genode::Io_signal_handler _play_timer { + _ep, *this, &Vfs::Oss_file_system::Data_file_system::_handle_play_timer }; + + void _handle_play_timer() + { + if (_audio.handle_play_timer()) + _vfs_user.wakeup_vfs_user(); + } + + Genode::Io_signal_handler _record_timer { + _ep, *this, &Vfs::Oss_file_system::Data_file_system::_handle_record_timer }; + + void _handle_record_timer() + { + if (_audio.handle_record_timer()) + _vfs_user.wakeup_vfs_user(); + } + + public: + + Data_file_system(Genode::Entrypoint &ep, + Vfs::Env::User &vfs_user, + Audio &audio, + Name const &name) + : + Single_file_system { Node_type::CONTINUOUS_FILE, name.string(), + Node_rwx::ro(), Genode::Xml_node("") }, + + _ep { ep }, + _vfs_user { vfs_user }, + _audio { audio } + { + _audio.play_timer_sigh(_play_timer); + _audio.record_timer_sigh(_record_timer); + } + + static const char *name() { return "data"; } + char const *type() override { return "data"; } + + /********************************* + ** Directory service interface ** + *********************************/ + + Open_result open(char const *path, unsigned flags, + Vfs_handle **out_handle, + Allocator &alloc) override + { + if (!_single_file(path)) { + return OPEN_ERR_UNACCESSIBLE; + } + + try { + *out_handle = new (alloc) + Registered_handle(_handle_registry, + *this, *this, + alloc, _audio, flags); + return OPEN_OK; + } + catch (Genode::Out_of_ram) { return OPEN_ERR_OUT_OF_RAM; } + catch (Genode::Out_of_caps) { return OPEN_ERR_OUT_OF_CAPS; } + } + + /******************************** + ** File I/O service interface ** + ********************************/ + + Ftruncate_result ftruncate(Vfs_handle *, file_size) override { + return FTRUNCATE_OK; } +}; + + +struct Vfs::Oss_file_system::Local_factory : File_system_factory +{ + using Label = Genode::String<64>; + Label const _label; + Name const _name; + + Vfs::Env &_env; + + /* RO/RW files */ + Readonly_value_file_system _channels_fs { "channels", 0U }; + Readonly_value_file_system _format_fs { "format", 0U }; + Readonly_value_file_system _sample_rate_fs { "sample_rate", 0U }; + Value_file_system _ifrag_total_fs { "ifrag_total", 0U }; + Value_file_system _ifrag_size_fs { "ifrag_size", 0U} ; + Readonly_value_file_system _ifrag_avail_fs { "ifrag_avail", 0U }; + Readonly_value_file_system _ifrag_bytes_fs { "ifrag_bytes", 0U }; + Value_file_system _ofrag_total_fs { "ofrag_total", 0U }; + Value_file_system _ofrag_size_fs { "ofrag_size", 0U} ; + Readonly_value_file_system _ofrag_avail_fs { "ofrag_avail", 0U }; + Readonly_value_file_system _ofrag_bytes_fs { "ofrag_bytes", 0U }; + Readonly_value_file_system _optr_samples_fs { "optr_samples", 0LL }; + Readonly_value_file_system _optr_fifo_samples_fs { "optr_fifo_samples", 0U }; + Value_file_system _play_underruns_fs { "play_underruns", 0U }; + Value_file_system _enable_input_fs { "enable_input", 1U }; + Value_file_system _enable_output_fs { "enable_output", 1U }; + + /* WO files */ + Value_file_system _halt_input_fs { "halt_input", 0U }; + Value_file_system _halt_output_fs { "halt_output", 0U }; + + Audio::Info _info { _channels_fs, _format_fs, _sample_rate_fs, + _ifrag_total_fs, _ifrag_size_fs, + _ifrag_avail_fs, _ifrag_bytes_fs, + _ofrag_total_fs, _ofrag_size_fs, + _ofrag_avail_fs, _ofrag_bytes_fs, + _optr_samples_fs, _optr_fifo_samples_fs, + _play_underruns_fs }; + + Readonly_value_file_system _info_fs { "info", _info }; + + Audio _audio; + + Genode::Io::Watch_handler _enable_input_handler { + _enable_input_fs, "/enable_input", + _env.alloc(), + *this, + &Vfs::Oss_file_system::Local_factory::_enable_input_changed }; + + Genode::Io::Watch_handler _halt_input_handler { + _halt_input_fs, "/halt_input", + _env.alloc(), + *this, + &Vfs::Oss_file_system::Local_factory::_halt_input_changed }; + + Genode::Io::Watch_handler _ifrag_total_handler { + _ifrag_total_fs, "/ifrag_total", + _env.alloc(), + *this, + &Vfs::Oss_file_system::Local_factory::_ifrag_total_changed }; + + Genode::Io::Watch_handler _ifrag_size_handler { + _ifrag_size_fs, "/ifrag_size", + _env.alloc(), + *this, + &Vfs::Oss_file_system::Local_factory::_ifrag_size_changed }; + + Genode::Io::Watch_handler _enable_output_handler { + _enable_output_fs, "/enable_output", + _env.alloc(), + *this, + &Vfs::Oss_file_system::Local_factory::_enable_output_changed }; + + Genode::Io::Watch_handler _halt_output_handler { + _halt_output_fs, "/halt_output", + _env.alloc(), + *this, + &Vfs::Oss_file_system::Local_factory::_halt_output_changed }; + + Genode::Io::Watch_handler _ofrag_total_handler { + _ofrag_total_fs, "/ofrag_total", + _env.alloc(), + *this, + &Vfs::Oss_file_system::Local_factory::_ofrag_total_changed }; + + Genode::Io::Watch_handler _ofrag_size_handler { + _ofrag_size_fs, "/ofrag_size", + _env.alloc(), + *this, + &Vfs::Oss_file_system::Local_factory::_ofrag_size_changed }; + + Genode::Io::Watch_handler _play_underruns_handler { + _play_underruns_fs, "/play_underruns", + _env.alloc(), + *this, + &Vfs::Oss_file_system::Local_factory::_play_underruns_changed }; + + /******************** + ** Watch handlers ** + ********************/ + + void _enable_input_changed() + { + bool const enable = (bool)_enable_input_fs.value(); + _audio.enable_input(enable); + } + + void _halt_input_changed() + { + bool const halt = (bool)_halt_input_fs.value(); + if (halt) + _audio.enable_input(false); + } + + void _ifrag_total_changed() + { + /* + * NOP for now as it is set in tandem with ifrag_size + * that in return limits number of fragments. + */ + } + + void _ifrag_size_changed() + { + /* + * Should only be changed while input is currently disabled. + */ + + unsigned const ifrag_size_max = _audio.max_ifrag_size(); + unsigned const ifrag_size_min = _audio.min_ifrag_size(); + + unsigned ifrag_size_new = _ifrag_size_fs.value(); + + ifrag_size_new = max(ifrag_size_new, ifrag_size_min); + ifrag_size_new = min(ifrag_size_new, ifrag_size_max); + + _info.ifrag_size = ifrag_size_new; + + _info.ifrag_total = _audio.frags_total(); + _info.ifrag_avail = 0; + _info.ifrag_bytes = _info.ifrag_avail * _info.ifrag_size; + + _audio.update_input_duration(_info.ifrag_size); + + _info.update(); + _info_fs.value(_info); + + if (_audio.verbose()) + log("Input fragment size changed to ", _info.ifrag_size); + } + + void _enable_output_changed() + { + bool const enable = (bool)_enable_output_fs.value(); + _audio.enable_output(enable); + } + + void _halt_output_changed() + { + bool const halt = (bool)_halt_output_fs.value(); + if (halt) + _audio.enable_output(false); + } + + void _ofrag_total_changed() + { + /* + * NOP for now as it is set in tandem with ofrag_size + * that in return limits number of fragments. + */ + } + + void _ofrag_size_changed() + { + /* + * Should only be changed while output is currently disabled. + */ + + unsigned const ofrag_size_max = _audio.max_ofrag_size(); + unsigned const ofrag_size_min = _audio.min_ofrag_size(); + + unsigned ofrag_size_new = _ofrag_size_fs.value(); + + ofrag_size_new = max(ofrag_size_new, ofrag_size_min); + ofrag_size_new = min(ofrag_size_new, ofrag_size_max); + + _info.ofrag_size = ofrag_size_new; + + _info.ofrag_total = _audio.frags_total(); + + _info.ofrag_avail = _info.ofrag_total; + _info.ofrag_bytes = _info.ofrag_total * _info.ofrag_size; + + _audio.update_output_duration(_info.ofrag_size); + + _info.update(); + _info_fs.value(_info); + + if (_audio.verbose()) + log("Output fragment size changed to ", _info.ofrag_size); + } + + void _play_underruns_changed() + { + /* reset counter */ + _info.play_underruns = 0; + + _info.update(); + _info_fs.value(_info); + } + + static Name name(Xml_node config) + { + return config.attribute_value("name", Name("oss")); + } + + Data_file_system _data_fs; + + Local_factory(Vfs::Env &env, Xml_node config) + : + _label { config.attribute_value("label", Label("")) }, + _name { name(config) }, + _env { env }, + _audio { _env, _info, _info_fs, config }, + _data_fs { _env.env().ep(), env.user(), _audio, name(config) } + { } + + Vfs::File_system *create(Vfs::Env&, Xml_node node) override + { + if (node.has_type("data")) return &_data_fs; + if (node.has_type("info")) return &_info_fs; + + if (node.has_type(Readonly_value_file_system::type_name())) { + + if (_channels_fs.matches(node)) return &_channels_fs; + if (_sample_rate_fs.matches(node)) return &_sample_rate_fs; + if (_ifrag_avail_fs.matches(node)) return &_ifrag_avail_fs; + if (_ifrag_bytes_fs.matches(node)) return &_ifrag_bytes_fs; + if (_ofrag_avail_fs.matches(node)) return &_ofrag_avail_fs; + if (_ofrag_bytes_fs.matches(node)) return &_ofrag_bytes_fs; + if (_format_fs.matches(node)) return &_format_fs; + if (_optr_samples_fs.matches(node)) return &_optr_samples_fs; + if (_optr_fifo_samples_fs.matches(node)) return &_optr_fifo_samples_fs; + } + + if (node.has_type(Value_file_system::type_name())) { + + if (_enable_input_fs.matches(node)) return &_enable_input_fs; + if (_enable_output_fs.matches(node)) return &_enable_output_fs; + if (_halt_input_fs.matches(node)) return &_halt_input_fs; + if (_halt_output_fs.matches(node)) return &_halt_output_fs; + if (_ifrag_total_fs.matches(node)) return &_ifrag_total_fs; + if (_ifrag_size_fs.matches(node)) return &_ifrag_size_fs; + if (_ofrag_total_fs.matches(node)) return &_ofrag_total_fs; + if (_ofrag_size_fs.matches(node)) return &_ofrag_size_fs; + if (_play_underruns_fs.matches(node)) return &_play_underruns_fs; + } + + return nullptr; + } +}; + + +class Vfs::Oss_file_system::Compound_file_system : private Local_factory, + public Vfs::Dir_file_system +{ + private: + + using Name = Oss_file_system::Name; + + using Config = String<1024>; + static Config _config(Name const &name) + { + char buf[Config::capacity()] { }; + + /* + * By not using the node type "dir", we operate the + * 'Dir_file_system' in root mode, allowing multiple sibling nodes + * to be present at the mount point. + */ + Genode::Xml_generator xml(buf, sizeof(buf), "compound", [&] () { + + xml.node("data", [&] () { + xml.attribute("name", name); }); + + xml.node("dir", [&] () { + xml.attribute("name", Name(".", name)); + xml.node("info", [&] () { }); + + xml.node("readonly_value", [&] { + xml.attribute("name", "channels"); + }); + + xml.node("readonly_value", [&] { + xml.attribute("name", "sample_rate"); + }); + + xml.node("readonly_value", [&] { + xml.attribute("name", "format"); + }); + + xml.node("value", [&] { + xml.attribute("name", "enable_input"); + }); + + xml.node("value", [&] { + xml.attribute("name", "enable_output"); + }); + + xml.node("value", [&] { + xml.attribute("name", "halt_input"); + }); + + xml.node("value", [&] { + xml.attribute("name", "halt_output"); + }); + + xml.node("value", [&] { + xml.attribute("name", "ifrag_total"); + }); + + xml.node("value", [&] { + xml.attribute("name", "ifrag_size"); + }); + + xml.node("readonly_value", [&] { + xml.attribute("name", "ifrag_avail"); + }); + + xml.node("readonly_value", [&] { + xml.attribute("name", "ifrag_bytes"); + }); + + xml.node("value", [&] { + xml.attribute("name", "ofrag_total"); + }); + + xml.node("value", [&] { + xml.attribute("name", "ofrag_size"); + }); + + xml.node("readonly_value", [&] { + xml.attribute("name", "ofrag_avail"); + }); + + xml.node("readonly_value", [&] { + xml.attribute("name", "ofrag_bytes"); + }); + + xml.node("readonly_value", [&] { + xml.attribute("name", "optr_samples"); + }); + + xml.node("readonly_value", [&] { + xml.attribute("name", "optr_fifo_samples"); + }); + + xml.node("value", [&] { + xml.attribute("name", "play_underruns"); + }); + }); + }); + + return Config(Genode::Cstring(buf)); + } + + public: + + Compound_file_system(Vfs::Env &vfs_env, Genode::Xml_node node) + : + Local_factory { vfs_env, node }, + Vfs::Dir_file_system { vfs_env, + Xml_node(_config(Local_factory::name(node)).string()), + *this } + { } + + static const char *name() { return "oss_next"; } + + char const *type() override { return name(); } +}; + + +extern "C" Vfs::File_system_factory *vfs_file_system_factory(void) +{ + struct Factory : Vfs::File_system_factory + { + Vfs::File_system *create(Vfs::Env &env, Genode::Xml_node config) override + { + return new (env.alloc()) + Vfs::Oss_file_system::Compound_file_system(env, config); + } + }; + + static Factory f; + return &f; +}