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; +}