diff --git a/repos/gems/recipes/src/rom_osci/content.mk b/repos/gems/recipes/src/rom_osci/content.mk new file mode 100644 index 0000000000..303e72ee5e --- /dev/null +++ b/repos/gems/recipes/src/rom_osci/content.mk @@ -0,0 +1,2 @@ +SRC_DIR = src/app/rom_osci +include $(GENODE_DIR)/repos/base/recipes/src/content.inc diff --git a/repos/gems/recipes/src/rom_osci/hash b/repos/gems/recipes/src/rom_osci/hash new file mode 100644 index 0000000000..47222982c8 --- /dev/null +++ b/repos/gems/recipes/src/rom_osci/hash @@ -0,0 +1 @@ +2024-01-19 8490b1bc29f4a69c27c9dce66b6630e984310e2b diff --git a/repos/gems/recipes/src/rom_osci/used_apis b/repos/gems/recipes/src/rom_osci/used_apis new file mode 100644 index 0000000000..e6dc84c584 --- /dev/null +++ b/repos/gems/recipes/src/rom_osci/used_apis @@ -0,0 +1,10 @@ +base +os +blit +gems +framebuffer_session +input_session +gui_session +timer_session +nitpicker_gfx +polygon_gfx diff --git a/repos/gems/run/waveform_player.run b/repos/gems/run/waveform_player.run new file mode 100644 index 0000000000..54b8f1db67 --- /dev/null +++ b/repos/gems/run/waveform_player.run @@ -0,0 +1,198 @@ +create_boot_directory + +import_from_depot [depot_user]/src/[base_src] \ + [depot_user]/pkg/[drivers_interactive_pkg] \ + [depot_user]/pkg/fonts_fs \ + [depot_user]/src/init \ + [depot_user]/src/report_rom \ + [depot_user]/src/dynamic_rom \ + [depot_user]/src/nitpicker \ + [depot_user]/src/libc \ + [depot_user]/src/libpng \ + [depot_user]/src/zlib + +build { server/record_play_mixer server/record_rom app/waveform_player app/rom_osci } + +proc play_period_ms { } { + if {[have_spec linux]} { return 80 } + return 5 +} + +proc record_period_ms { } { + if {[have_spec linux]} { return 40 } + return 5 +} + +proc jitter_ms { } { + if {[have_spec linux]} { return 20 } + return 2 +} + +install_config { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +build_boot_image [build_artifacts] + +append qemu_args " -nographic " + +run_genode_until forever + diff --git a/repos/gems/src/app/rom_osci/main.cc b/repos/gems/src/app/rom_osci/main.cc new file mode 100644 index 0000000000..7b896b7d3e --- /dev/null +++ b/repos/gems/src/app/rom_osci/main.cc @@ -0,0 +1,340 @@ +/* + * \brief Oscilloscope showing data obtained from a dynamic ROM + * \author Norman Feske + * \date 2023-12-20 + */ + +/* + * Copyright (C) 2023 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Osci { + + using namespace Genode; + + struct Main; + + static void with_skipped_bytes(Const_byte_range_ptr const &bytes, + size_t const n, auto const &fn) + { + if (bytes.num_bytes < n) + return; + + Const_byte_range_ptr const remainder { bytes.start + n, + bytes.num_bytes - n }; + fn(remainder); + } + + static void with_skipped_whitespace(Const_byte_range_ptr const &bytes, auto const &fn) + { + auto whitespace = [] (char const c) { return c == ' ' || c == '\n'; }; + + size_t skip = 0; + while (whitespace(bytes.start[skip]) && (skip < bytes.num_bytes)) + skip++; + + Const_byte_range_ptr const remainder { bytes.start + skip, + bytes.num_bytes - skip }; + fn(remainder); + } + + template + static void with_parsed_value(Const_byte_range_ptr const &bytes, auto const &fn) + { + T value { }; + size_t const n = ascii_to(bytes.start, value); + with_skipped_bytes(bytes, n, [&] (Const_byte_range_ptr const &remainder) { + with_skipped_whitespace(remainder, [&] (Const_byte_range_ptr const &remainder) { + fn(value, remainder); }); }); + } +} + + +struct Osci::Main +{ + Env &_env; + + using Point = Gui_buffer::Point; + using Area = Gui_buffer::Area; + using Rect = Gui_buffer::Rect; + + Area _size { }; + Color _background { }; + unsigned _fps { }; + bool _phase_lock { }; + + Heap _heap { _env.ram(), _env.rm() }; + + Timer::Connection _timer { _env }; + Gui::Connection _gui { _env }; + + Constructible _gui_buffer { }; + + struct View + { + Gui::Connection &_gui; + + Gui::Session::View_handle _handle { _gui.create_view() }; + + View(Gui::Connection &gui, Point position, Area size) : _gui(gui) + { + using Command = Gui::Session::Command; + _gui.enqueue(_handle, Rect(position, size)); + _gui.enqueue(_handle, Gui::Session::View_handle()); + _gui.execute(); + } + + ~View() { _gui.destroy_view(_handle); } + }; + + Constructible _view { }; + + Attached_rom_dataspace _config { _env, "config" }; + Attached_rom_dataspace _recording { _env, "recording" }; + + Signal_handler
_timer_handler { _env.ep(), *this, &Main::_handle_timer }; + Signal_handler
_config_handler { _env.ep(), *this, &Main::_handle_config }; + + struct Phase_lock { unsigned offset; }; + + struct Captured_channel + { + static constexpr unsigned SIZE_LOG2 = 10, SIZE = 1 << SIZE_LOG2, MASK = SIZE - 1; + + float _samples[SIZE] { }; + + unsigned _pos = 0; + + Captured_channel() { }; + + Captured_channel(Xml_node const &channel) + { + auto insert = [&] (float value) + { + _pos = (_pos + 1) & MASK; + _samples[_pos] = value; + }; + + channel.with_raw_content([&] (char const *start, size_t len) { + while (len > 0) { + Const_byte_range_ptr bytes { start, len }; + with_parsed_value(bytes, [&] (double v, Const_byte_range_ptr const &remaining) { + insert(float(v)); + start = remaining.start; + len = remaining.num_bytes; + }); + } + }); + } + + float past_value(unsigned past) const + { + return _samples[(_pos - past) & MASK]; + } + }; + + struct Channel : private List_model>::Element + { + friend class List_model>; + friend class List>; + + using Label = String<20>; + Label const label; + + static Label _label_from_xml(Xml_node const &node) + { + return node.attribute_value("label", Label()); + } + + struct Attr + { + double v_pos, v_scale; + Color color; + + static Attr from_xml(Xml_node const &node, Attr const defaults) + { + return Attr { + .v_pos = node.attribute_value("v_pos", defaults.v_pos), + .v_scale = node.attribute_value("v_scale", defaults.v_scale), + .color = node.attribute_value("color", defaults.color), + }; + } + } _attr { }; + + Captured_channel _capture { }; + + Line_painter const _line_painter { }; + + Channel(Xml_node const &node) : label(_label_from_xml(node)) { } + + virtual ~Channel() { }; + + void update(Xml_node const &node, Attr const defaults) + { + _attr = Attr::from_xml(node, defaults); + } + + void capture(Xml_node const &node) { _capture = Captured_channel(node); } + + Phase_lock phase_lock(unsigned start, float const threshold, unsigned const max) const + { + float curr_value = 0.0f; + for (unsigned i = 0 ; i < max; i++) { + float const prev_value = curr_value; + curr_value = _capture.past_value(i + start); + if (prev_value <= threshold && curr_value > threshold) + return { i }; + } + return { 0 }; + } + + void render(Gui_buffer::Pixel_surface &pixel, + Gui_buffer::Alpha_surface &, Phase_lock const phase_lock) const + { + /* + * Draw captured samples from right to left. + */ + + Area const area = pixel.size(); + unsigned const w = area.w(); + int const y_pos = int(_attr.v_pos*area.h()); + double const screen_v_scale = _attr.v_scale*area.h()/2; + + auto _horizontal_line = [&] (Color c, int y, int alpha) + { + _line_painter.paint(pixel, Point { 0, y }, + Point { int(w) - 2, y }, + Color { c.r, c.g, c.b, alpha }); + }; + + _horizontal_line(_attr.color, y_pos, 80); + _horizontal_line(_attr.color, y_pos - int(screen_v_scale), 40); + _horizontal_line(_attr.color, y_pos + int(screen_v_scale), 40); + + Point const centered { 0, y_pos }; + Point previous_p { }; + bool first_iteration = true; + for (unsigned i = 0; i < w; i++) { + + Point p { int(w - i), + int(screen_v_scale*_capture.past_value(i + phase_lock.offset)) }; + + p = p + centered; + + if (!first_iteration) + _line_painter.paint(pixel, p, previous_p, _attr.color); + + previous_p = p; + first_iteration = false; + } + } + + /* + * List_model::Element + */ + static bool type_matches(Xml_node const &node) + { + return node.has_type("channel"); + } + + bool matches(Xml_node const &node) const + { + return _label_from_xml(node) == label; + } + }; + + List_model> _channels { }; + + Registry> _channel_registry { }; /* for non-const for_each */ + + void _handle_config() + { + _config.update(); + + Xml_node const config = _config.xml(); + + _size = Area::from_xml(config); + _background = config.attribute_value("background", Color { 0, 0, 0 }); + _fps = config.attribute_value("fps", 50u); + _phase_lock = config.attribute_value("phase_lock", false); + + _fps = max(_fps, 1u); + + /* channel defaults obtained from top-level config node */ + Channel::Attr const channel_defaults = Channel::Attr::from_xml(config, { + .v_pos = 0.5, + .v_scale = 0.6, + .color = { 255, 255, 255 }, + }); + + _gui_buffer.construct(_gui, _size, _env.ram(), _env.rm(), + Gui_buffer::Alpha::OPAQUE, _background); + + _view.construct(_gui, Point::from_xml(config), _size); + + _channels.update_from_xml(config, + [&] (Xml_node const &node) -> Registered & { + return *new (_heap) Registered(_channel_registry, node); }, + [&] (Registered &channel) { + destroy(_heap, &channel); }, + [&] (Channel &channel, Xml_node const &node) { + channel.update(node, channel_defaults); } + ); + + _timer.trigger_periodic(1000*1000/_fps); + } + + void _handle_timer() + { + /* import recorded samples */ + _recording.update(); + _recording.xml().for_each_sub_node("channel", [&] (Xml_node const &node) { + Channel::Label const label = node.attribute_value("label", Channel::Label()); + _channel_registry.for_each([&] (Registered &channel) { + if (channel.label == label) + channel.capture(node); }); }); + + /* determine phase-locking offset from one channel */ + Phase_lock phase_lock { }; + if (_phase_lock) + _channels.with_first([&] (Channel const &channel) { + phase_lock = channel.phase_lock(_size.w()/2, -0.1f, _size.w()/2); }); + + _gui_buffer->reset_surface(); + _gui_buffer->apply_to_surface([&] (auto &pixel, auto &alpha) { + _channels.for_each([&] (Channel const &channel) { + channel.render(pixel, alpha, phase_lock); }); }); + + _gui_buffer->flush_surface(); + _gui.framebuffer()->refresh(0, 0, _size.w(), _size.h()); + } + + Main(Env &env) : _env(env) + { + _timer.sigh(_timer_handler); + _config.sigh(_config_handler); + _handle_config(); + } +}; + + +void Component::construct(Genode::Env &env) +{ + static Osci::Main main(env); +} diff --git a/repos/gems/src/app/rom_osci/target.mk b/repos/gems/src/app/rom_osci/target.mk new file mode 100644 index 0000000000..6d3074a0e7 --- /dev/null +++ b/repos/gems/src/app/rom_osci/target.mk @@ -0,0 +1,3 @@ +TARGET = rom_osci +SRC_CC = main.cc +LIBS = base blit diff --git a/repos/os/include/play_session/connection.h b/repos/os/include/play_session/connection.h new file mode 100644 index 0000000000..b9a7f30d61 --- /dev/null +++ b/repos/os/include/play_session/connection.h @@ -0,0 +1,171 @@ +/* + * \brief Connection to audio-play service + * \author Norman Feske + * \date 2023-12-11 + */ + +/* + * Copyright (C) 2023 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. + */ + +#ifndef _INCLUDE__PLAY_SESSION__CONNECTION_H_ +#define _INCLUDE__PLAY_SESSION__CONNECTION_H_ + +#include +#include +#include +#include + +namespace Play { struct Connection; } + + +struct Play::Connection : Genode::Connection, Rpc_client +{ + private: + + static constexpr Ram_quota RAM_QUOTA = { DATASPACE_SIZE + 4096 }; + + Attached_dataspace _ds; + + Shared_buffer &_buffer = *_ds.local_addr(); + + Seq _seq { }; + + unsigned _slot_id { }; + + Sample_start _sample_start { }; /* position within 'samples' ring buffer */ + + Num_samples _submit_samples(auto const &fn) + { + _seq = { _seq.value() + 1 }; + _slot_id = (_slot_id + 1) % Shared_buffer::NUM_SLOTS; + + Shared_buffer::Slot &slot = _buffer.slots[_slot_id]; + + slot.acquired_seq = _seq; + slot.time_window = { }; + slot.sample_start = { }; + slot.num_samples = { }; + + class Submission + { + private: + + float * const _dst; + unsigned _pos; + unsigned _count = 0; + + /* + * Noncopyable + */ + Submission &operator = (Submission const &); + Submission(Submission const &); + + public: + + Submission(float *dst, unsigned pos) : _dst(dst), _pos(pos) { } + + void operator () (float value) + { + _dst[_pos] = value; + if (++_pos >= Shared_buffer::MAX_SAMPLES) + _pos = 0; + _count++; + } + + Num_samples num_samples() const { return { _count }; } + }; + + Submission submission(_buffer.samples, _sample_start.index); + + fn(submission); + + return submission.num_samples(); + } + + void _commit_to_current_slot(Num_samples n, Time_window tw) + { + Shared_buffer::Slot &slot = _buffer.slots[_slot_id]; + + slot.sample_start = _sample_start; + slot.num_samples = n; + slot.time_window = tw; + slot.committed_seq = _seq; + + /* advance destination position for next submission */ + _sample_start.index = (_sample_start.index + n.value()) + % Shared_buffer::MAX_SAMPLES; + } + + public: + + Connection(Genode::Env &env, Label const &label = Label()) + : + Genode::Connection(env, label, RAM_QUOTA, Args()), + Rpc_client(cap()), + _ds(env.rm(), call()) + { + if (_ds.size() < DATASPACE_SIZE) { + error("play buffer has insufficient size"); + sleep_forever(); + } + } + + /** + * Schedule playback of data after the given 'previous' time window + * + * \param previous time window returned by previous call, or + * a default-constructed 'Time_window' when starting + * \param duration duration of the sample data in microseconds + * \param fn functor to be repeatedly called with float sample values + * + * The sample rate depends on the given 'duration' and the number of + * 'fn' calls in the scope of the 'schedule' call. Note that the + * duration is evaluated only as a hint when starting a new playback. + * During continuous playback, the duration is inferred by the rate + * of the periodic 'schedule_and_enqueue' calls. + */ + Time_window schedule_and_enqueue(Time_window previous, Duration duration, + auto const &fn) + { + Num_samples const n = _submit_samples(fn); + Time_window const tw = call(previous, duration, n); + + _commit_to_current_slot(n, tw); + + return tw; + } + + /** + * Passively enqueue data for the playback at the given time window + * + * In contrast to 'schedule_and_enqueue', this method does not allocate + * a new time window but schedules sample data for an already known + * time window. It is designated for the synchronized playback of + * multiple audio channels whereas each channel is a separate play + * session. + * + * One channel (e.g., the left) drives the allocation of time windows + * using 'schedule_and_enqueue' whereas the other channels (e.g., the + * right) merely submit audio data for the already allocated time + * windows using 'enqueue'. This way, only one RPC per period is needed + * to drive the synchronized output of an arbitrary number of channels. + */ + void enqueue(Time_window time_window, auto const &fn) + { + _commit_to_current_slot(_submit_samples(fn), time_window); + } + + /** + * Inform the server that no further data is expected + * + * By calling 'stop', the client allows the server to distinguish the + * (temporary) end of playback from jitter. + */ + void stop() { call(); } +}; + +#endif /* _INCLUDE__RECORD_SESSION__CONNECTION_H_ */ diff --git a/repos/os/include/play_session/play_session.h b/repos/os/include/play_session/play_session.h new file mode 100644 index 0000000000..94a7593746 --- /dev/null +++ b/repos/os/include/play_session/play_session.h @@ -0,0 +1,102 @@ +/* + * \brief Audio-play session interface + * \author Norman Feske + * \date 2023-12-11 + */ + +/* + * Copyright (C) 2023 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. + */ + +#ifndef _INCLUDE__PLAY_SESSION__PLAY_SESSION_H_ +#define _INCLUDE__PLAY_SESSION__PLAY_SESSION_H_ + +#include +#include +#include + +namespace Play { + + using namespace Genode; + + struct Seq + { + static constexpr unsigned LIMIT = 1 << 7, MASK = LIMIT - 1; + unsigned _value; /* 0...127 */ + unsigned value() const { return _value & MASK; } + }; + + struct Time_window { unsigned start, end; }; + + struct Sample_start { unsigned index; }; + + struct Num_samples + { + unsigned _value; /* 0...4095 */ + unsigned value() const { return _value & 0xfff; } + }; + + struct Duration + { + static constexpr unsigned LIMIT = 100*1000; + unsigned us; + bool valid() const { return us > 0 && us <= LIMIT; } + }; + + struct Session; +} + + +struct Play::Session : Genode::Session +{ + /** + * Layout of the audio buffer shared between client and server + */ + struct Shared_buffer + { + static const unsigned NUM_SLOTS = 20; + static const unsigned MAX_SAMPLES = 8*1024; /* 160 ms at 50 KHz */ + + struct Slot + { + Seq acquired_seq; + Time_window time_window; + Sample_start sample_start; /* offset within 'samples' */ + Num_samples num_samples; + Seq committed_seq; /* detect slot modification during read */ + }; + + Slot slots[NUM_SLOTS]; + + float samples[MAX_SAMPLES]; /* ring buffer of sample values */ + }; + + static constexpr size_t DATASPACE_SIZE = align_addr(sizeof(Shared_buffer), 12); + + /** + * \noapi + */ + static const char *service_name() { return "Play"; } + + /* + * A play session consumes a dataspace capability for the server's + * session-object allocation, a dataspace capability for the audio + * buffer, and its session capability. + */ + static constexpr unsigned CAP_QUOTA = 3; + + + /********************* + ** RPC declaration ** + *********************/ + + GENODE_RPC(Rpc_dataspace, Dataspace_capability, dataspace); + GENODE_RPC(Rpc_schedule, Time_window, schedule, Time_window, Duration, Num_samples); + GENODE_RPC(Rpc_stop, void, stop); + GENODE_RPC_INTERFACE(Rpc_dataspace, Rpc_schedule, Rpc_stop); +}; + +#endif /* _INCLUDE__EVENT_SESSION__EVENT_SESSION_H_ */ diff --git a/repos/os/include/record_session/connection.h b/repos/os/include/record_session/connection.h new file mode 100644 index 0000000000..05a6da4ac9 --- /dev/null +++ b/repos/os/include/record_session/connection.h @@ -0,0 +1,96 @@ +/* + * \brief Connection to an audio-record service + * \author Norman Feske + * \date 2023-12-11 + */ + +/* + * Copyright (C) 2023 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. + */ + +#ifndef _INCLUDE__RECORD_SESSION__CONNECTION_H_ +#define _INCLUDE__RECORD_SESSION__CONNECTION_H_ + +#include +#include +#include +#include + +namespace Record { struct Connection; } + + +struct Record::Connection : Genode::Connection, Rpc_client +{ + private: + + Attached_dataspace _ds; + + public: + + static constexpr Ram_quota RAM_QUOTA = { DATASPACE_SIZE + 2*4096 }; + + Connection(Env &env, Session_label const &label = Session_label()) + : + Genode::Connection(env, label, RAM_QUOTA, Args()), + Rpc_client(cap()), + _ds(env.rm(), call()) + { + if (_ds.size() < DATASPACE_SIZE) { + error("record buffer has insufficient size"); + sleep_forever(); + } + } + + /** + * Register signal handler on new data becomes available after depletion + */ + void wakeup_sigh(Signal_context_capability sigh) { call(sigh); } + + /** + * Read-only sample data as an array of float values + */ + struct Samples_ptr : Genode::Noncopyable + { + struct { float const * const start; unsigned num_samples; }; + + Samples_ptr(float const *start, unsigned num_samples) + : start(start), num_samples(num_samples) { } + }; + + /** + * Record the specified number of audio samples + * + * \param fn called with the 'Time_window' and 'Samples_ptr const &' + * of the recording + * + * \param depleted_fn called when no sample data is available + * + * Subsequent 'record' calls result in consecutive time windows. + */ + void record(Num_samples n, auto const &fn, auto const &depleted_fn) + { + call(n).with_result( + [&] (Time_window const &tw) { + fn(tw, Samples_ptr(_ds.local_addr(), n.value())); + }, + [&] (Depleted) { depleted_fn(); }); + } + + /** + * Record specified number of audio samples at the given time window + * + * By using the time window returned by 'record' as argument for + * 'record_at', a user of multiple sessions (e.g., for left and right) + * can obtain sample data synchronized between the sessions. + */ + void record_at(Time_window tw, Num_samples n, auto const &fn) + { + call(tw, n); + fn(Samples_ptr(_ds.local_addr(), n.value())); + } +}; + +#endif /* _INCLUDE__RECORD_SESSION__CONNECTION_H_ */ diff --git a/repos/os/include/record_session/record_session.h b/repos/os/include/record_session/record_session.h new file mode 100644 index 0000000000..b81c2cdf13 --- /dev/null +++ b/repos/os/include/record_session/record_session.h @@ -0,0 +1,79 @@ +/* + * \brief Audio-record session interface + * \author Norman Feske + * \date 2023-12-11 + */ + +/* + * Copyright (C) 2023 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. + */ + +#ifndef _INCLUDE__RECORD_SESSION__RECORD_SESSION_H_ +#define _INCLUDE__RECORD_SESSION__RECORD_SESSION_H_ + +#include +#include +#include + +namespace Record { + + using namespace Genode; + + /* + * The 'Time_window' values are merely used as tokens between 'record' + * and 'record_at' calls. They are not meant to be interpreted by the + * client. + */ + struct Time_window { unsigned start, end; }; + + struct Num_samples + { + unsigned _value; /* 0...8191 (13 bits) */ + unsigned value() const { return _value & 0x1fff; } + }; + + struct Session; +} + + +struct Record::Session : Genode::Session +{ + /* + * The dataspace shared between client and server can hold 160 ms of 50 KHz + * audio, using one float (4 bytes) per sample. + */ + static constexpr size_t DATASPACE_SIZE = 32*1024; + + /** + * \noapi + */ + static const char *service_name() { return "Record"; } + + /* + * A record session consumes a dataspace capability for the server's + * session-object allocation, a dataspace capability for the audio + * buffer, and its session capability. + */ + static constexpr unsigned CAP_QUOTA = 3; + + struct Depleted { }; + + using Record_result = Attempt; + + + /********************* + ** RPC declaration ** + *********************/ + + GENODE_RPC(Rpc_dataspace, Dataspace_capability, dataspace); + GENODE_RPC(Rpc_wakeup_sigh, void, wakeup_sigh, Signal_context_capability); + GENODE_RPC(Rpc_record, Record_result, record, Num_samples); + GENODE_RPC(Rpc_record_at, void, record_at, Time_window, Num_samples); + + GENODE_RPC_INTERFACE(Rpc_dataspace, Rpc_wakeup_sigh, Rpc_record, Rpc_record_at); +}; + +#endif /* _INCLUDE__EVENT_SESSION__EVENT_SESSION_H_ */ diff --git a/repos/os/recipes/api/play_session/content.mk b/repos/os/recipes/api/play_session/content.mk new file mode 100644 index 0000000000..6b603ee7bf --- /dev/null +++ b/repos/os/recipes/api/play_session/content.mk @@ -0,0 +1,2 @@ +MIRRORED_FROM_REP_DIR := include/play_session +include $(REP_DIR)/recipes/api/session.inc diff --git a/repos/os/recipes/api/play_session/hash b/repos/os/recipes/api/play_session/hash new file mode 100644 index 0000000000..721809e694 --- /dev/null +++ b/repos/os/recipes/api/play_session/hash @@ -0,0 +1 @@ +2024-01-19 3e0d1397ae70a3d2a9c297d9e8b32eb750889c24 diff --git a/repos/os/recipes/api/record_session/content.mk b/repos/os/recipes/api/record_session/content.mk new file mode 100644 index 0000000000..3257c632da --- /dev/null +++ b/repos/os/recipes/api/record_session/content.mk @@ -0,0 +1,2 @@ +MIRRORED_FROM_REP_DIR := include/record_session +include $(REP_DIR)/recipes/api/session.inc diff --git a/repos/os/recipes/api/record_session/hash b/repos/os/recipes/api/record_session/hash new file mode 100644 index 0000000000..eaed885bc3 --- /dev/null +++ b/repos/os/recipes/api/record_session/hash @@ -0,0 +1 @@ +2024-01-19 3a9fdc374119a6b1b43fcb6a80ccbea571793f55 diff --git a/repos/os/recipes/src/record_play_mixer/content.mk b/repos/os/recipes/src/record_play_mixer/content.mk new file mode 100644 index 0000000000..b15ce7e524 --- /dev/null +++ b/repos/os/recipes/src/record_play_mixer/content.mk @@ -0,0 +1,2 @@ +SRC_DIR = src/server/record_play_mixer +include $(GENODE_DIR)/repos/base/recipes/src/content.inc diff --git a/repos/os/recipes/src/record_play_mixer/hash b/repos/os/recipes/src/record_play_mixer/hash new file mode 100644 index 0000000000..b29d6e2b1f --- /dev/null +++ b/repos/os/recipes/src/record_play_mixer/hash @@ -0,0 +1 @@ +2024-01-19 2c71de8cc006a5ed9455d8c052584db4a7a238f2 diff --git a/repos/os/recipes/src/record_play_mixer/used_apis b/repos/os/recipes/src/record_play_mixer/used_apis new file mode 100644 index 0000000000..fe9a94ba7d --- /dev/null +++ b/repos/os/recipes/src/record_play_mixer/used_apis @@ -0,0 +1,6 @@ +base +os +record_session +play_session +timer_session +report_session diff --git a/repos/os/recipes/src/record_rom/content.mk b/repos/os/recipes/src/record_rom/content.mk new file mode 100644 index 0000000000..d71129bf8f --- /dev/null +++ b/repos/os/recipes/src/record_rom/content.mk @@ -0,0 +1,2 @@ +SRC_DIR = src/server/record_rom +include $(GENODE_DIR)/repos/base/recipes/src/content.inc diff --git a/repos/os/recipes/src/record_rom/hash b/repos/os/recipes/src/record_rom/hash new file mode 100644 index 0000000000..f9babffcb5 --- /dev/null +++ b/repos/os/recipes/src/record_rom/hash @@ -0,0 +1 @@ +2024-01-19 0b59428f86704633fdf5e0507759e512e18f6b20 diff --git a/repos/os/recipes/src/record_rom/used_apis b/repos/os/recipes/src/record_rom/used_apis new file mode 100644 index 0000000000..83422159f2 --- /dev/null +++ b/repos/os/recipes/src/record_rom/used_apis @@ -0,0 +1,4 @@ +base +os +timer_session +record_session diff --git a/repos/os/recipes/src/waveform_player/content.mk b/repos/os/recipes/src/waveform_player/content.mk new file mode 100644 index 0000000000..cffe52f778 --- /dev/null +++ b/repos/os/recipes/src/waveform_player/content.mk @@ -0,0 +1,2 @@ +SRC_DIR = src/app/waveform_player +include $(GENODE_DIR)/repos/base/recipes/src/content.inc diff --git a/repos/os/recipes/src/waveform_player/hash b/repos/os/recipes/src/waveform_player/hash new file mode 100644 index 0000000000..41b9cc8180 --- /dev/null +++ b/repos/os/recipes/src/waveform_player/hash @@ -0,0 +1 @@ +2024-01-19 851b68f4d966f6671ff5dbbd008365df547a7728 diff --git a/repos/os/recipes/src/waveform_player/used_apis b/repos/os/recipes/src/waveform_player/used_apis new file mode 100644 index 0000000000..c792cfa546 --- /dev/null +++ b/repos/os/recipes/src/waveform_player/used_apis @@ -0,0 +1,4 @@ +base +os +timer_session +play_session diff --git a/repos/os/src/app/waveform_player/main.cc b/repos/os/src/app/waveform_player/main.cc new file mode 100644 index 0000000000..110ef3ea99 --- /dev/null +++ b/repos/os/src/app/waveform_player/main.cc @@ -0,0 +1,330 @@ +/* + * \brief Waveform generator targeting play sessions + * \author Norman Feske + * \date 2023-12-20 + */ + +/* + * Copyright (C) 2023 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. + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace Waveform_player { + + using namespace Genode; + + struct Main; +} + + +struct Waveform_player::Main +{ + Env &_env; + + Heap _heap { _env.ram(), _env.rm() }; + + Timer::Connection _timer { _env }; + + Attached_rom_dataspace _config_ds { _env, "config" }; + + Signal_handler
_config_handler { + _env.ep(), *this, &Main::_handle_config }; + + Signal_handler
_timer_handler { + _env.ep(), *this, &Main::_handle_timer }; + + struct Phase + { + /* value range corresponds to 0...2*pi */ + static constexpr unsigned PI2 = (1u << 16); + uint16_t angle; + }; + + struct Waveform + { + static constexpr unsigned STEPS_LOG2 = 10u, STEPS = 1u << STEPS_LOG2; + + float _values[STEPS] { }; + + float value(Phase phase) const + { + return _values[phase.angle >> (16u - STEPS_LOG2)]; + } + }; + + struct Sine_waveform : Waveform + { + Sine_waveform() + { + /* sin and cos values of 2*pi/STEPS */ + static constexpr double SINA = 0.00613588, COSA = 0.99998117; + + struct Point + { + double x, y; + + Point rotated() const { return { .x = x*COSA - y*SINA, + .y = y*COSA + x*SINA }; } + } p { .x = 1.0, .y = 0.0 }; + + for (unsigned i = 0; i < STEPS; i++, p = p.rotated()) + _values[i] = float(p.y); + } + }; + + struct Square_waveform : Waveform + { + Square_waveform() + { + for (unsigned i = 0; i < STEPS; i++) + _values[i] = (i < STEPS/2) ? -1.0f : 1.0f; + } + }; + + struct Saw_waveform : Waveform + { + Saw_waveform() + { + for (unsigned i = 0; i < STEPS; i++) + _values[i] = -1.0f + (float(i)*2)/STEPS; + } + }; + + Sine_waveform const _sine_waveform { }; + Square_waveform const _square_waveform { }; + Saw_waveform const _saw_waveform { }; + + enum class Wave { NONE, SINE, SQUARE, SAW }; + + void with_waveform(Wave const wave, auto const &fn) const + { + auto waveform_ptr = [&] (Wave const wave) -> Waveform const * + { + if (wave == Wave::SINE) return &_sine_waveform; + if (wave == Wave::SQUARE) return &_square_waveform; + if (wave == Wave::SAW) return &_saw_waveform; + return nullptr; + }; + + Waveform const *ptr = waveform_ptr(wave); + if (ptr) + fn(*ptr); + } + + struct Channel : private List_model>::Element + { + friend class List_model>; + friend class List>; + + using Label = String<20>; + Label const label; + + static Label _label_from_xml(Xml_node const &node) + { + return node.attribute_value("label", Label()); + } + + struct Attr + { + unsigned sample_rate_hz; + double wave_hz; + Wave wave; + + static Attr from_xml(Xml_node const &node, Attr const defaults) + { + auto wave_from_xml = [] (Xml_node const &node) + { + auto const attr = node.attribute_value("wave", String<16>()); + + if (attr == "sine") return Wave::SINE; + if (attr == "square") return Wave::SQUARE; + if (attr == "saw") return Wave::SAW; + if (attr == "") return Wave::SINE; + + warning("unsupported waveform '", attr, "'"); + return Wave::NONE; + }; + return Attr { + .sample_rate_hz = node.attribute_value("sample_rate_hz", defaults.sample_rate_hz), + .wave_hz = node.attribute_value("hz", defaults.wave_hz), + .wave = wave_from_xml(node) + }; + } + + bool ready_to_play() const { return wave_hz && sample_rate_hz; } + }; + + Play::Connection _play; + + Attr _attr { }; + Phase _phase { }; + + Phase _phase_increment() const + { + return { uint16_t(uint32_t(_attr.wave_hz*Phase::PI2)/_attr.sample_rate_hz) }; + } + + unsigned _samples_per_period(unsigned period_ms) const + { + return (period_ms*_attr.sample_rate_hz)/1000; + } + + void _produce_samples(Waveform const &waveform, Phase increment, + unsigned num_samples, auto const fn) + { + for (unsigned i = 0; i < num_samples; i++) { + fn(waveform.value(_phase)); + _phase.angle += increment.angle; + } + } + + Channel(Env &env, Xml_node const &node) + : + label(_label_from_xml(node)), _play(env, label) + { } + + virtual ~Channel() { }; + + bool ready_to_play() const { return _attr.ready_to_play(); } + + Play::Time_window play(Main const &main, Play::Time_window previous, unsigned period_ms) + { + Play::Duration const duration { .us = period_ms*1000 }; + unsigned const num_samples = _samples_per_period(period_ms); + Phase const increment = _phase_increment(); + + return _play.schedule_and_enqueue(previous, duration, [&] (auto &submit) { + main.with_waveform(_attr.wave, [&] (Waveform const &waveform) { + _produce_samples(waveform, increment, num_samples, + [&] (float v) { submit(v); }); }); }); + } + + void play_at(Main const &main, Play::Time_window tw, unsigned period_ms) + { + unsigned const num_samples = _samples_per_period(period_ms); + Phase const increment = _phase_increment(); + + _play.enqueue(tw, [&] (auto &submit) { + main.with_waveform(_attr.wave, [&] (Waveform const &waveform) { + _produce_samples(waveform, increment, num_samples, + [&] (float v) { submit(v); }); }); }); + } + + void stop() { _play.stop(); } + + void update(Xml_node const &node, Attr const defaults) + { + _attr = Attr::from_xml(node, defaults); + }; + + /* + * List_model::Element + */ + static bool type_matches(Xml_node const &node) + { + return node.has_type("play"); + } + + bool matches(Xml_node const &node) const + { + return _label_from_xml(node) == label; + } + }; + + List_model> _channels { }; + + Registry> _channel_registry { }; /* for non-const for_each */ + + struct Config + { + unsigned period_ms; + + Channel::Attr channel_defaults; + + static Config from_xml(Xml_node const &config) + { + return { + .period_ms = config.attribute_value("period_ms", 10u), + .channel_defaults = Channel::Attr::from_xml(config, { + .sample_rate_hz = 44100u, + .wave_hz = 1000.0, + .wave = Wave::SINE + }) }; + } + }; + + Config _config { }; + + Play::Time_window _time_window { }; + + void _play_channels() + { + bool const was_playing = ( _time_window.start != _time_window.end ); + + /* + * The first channel drives the time window that is reused for all + * other channels to attain time-synchronized data. + */ + bool first = true; + bool playing = false; + _channel_registry.for_each([&] (Registered &channel) { + if (channel.ready_to_play()) { + playing = true; + if (first) + _time_window = channel.play(*this, _time_window, _config.period_ms); + else + channel.play_at(*this, _time_window, _config.period_ms); + first = false; + } + }); + + if (was_playing && !playing) { + _channel_registry.for_each([&] (Registered &channel) { + channel.stop(); }); + _time_window = { }; + } + } + + void _handle_timer() { _play_channels(); } + + void _handle_config() + { + _config_ds.update(); + Xml_node const &config = _config_ds.xml(); + + _config = Config::from_xml(config); + + _channels.update_from_xml(config, + [&] (Xml_node const &node) -> Registered & { + return *new (_heap) + Registered(_channel_registry, _env, node); }, + [&] (Registered &channel) { + destroy(_heap, &channel); }, + [&] (Channel &channel, Xml_node const &node) { + channel.update(node, _config.channel_defaults); } + ); + + if (_config.period_ms) + _timer.trigger_periodic(_config.period_ms*1000); + } + + Main(Env &env) : _env(env) + { + _timer.sigh(_timer_handler); + _config_ds.sigh(_config_handler); + _handle_config(); + } +}; + + +void Component::construct(Genode::Env &env) { static Waveform_player::Main main(env); } diff --git a/repos/os/src/app/waveform_player/target.mk b/repos/os/src/app/waveform_player/target.mk new file mode 100644 index 0000000000..5fc41df6e5 --- /dev/null +++ b/repos/os/src/app/waveform_player/target.mk @@ -0,0 +1,3 @@ +TARGET = waveform_player +SRC_CC = main.cc +LIBS = base diff --git a/repos/os/src/server/record_play_mixer/README b/repos/os/src/server/record_play_mixer/README new file mode 100644 index 0000000000..183b2f2178 --- /dev/null +++ b/repos/os/src/server/record_play_mixer/README @@ -0,0 +1,78 @@ +The mixer routes and mixes audio signals produced by play clients to record +clients according to its configuration. Typical play clients are an audio +player or a microphone driver whereas typical record clients are an audio +recorder or an audio-output driver. + +Both play and record clients are expected to operate periodically. The number +of samples produced per period is up to each client and does not need to be +constant over time. The mixer infers the used sample rates and periods by +observing the behavior of the clients. Sample rates between play and record +clients are converted automatically. + +The latency depends on the period lengths of both record and play sides as +well as on the observed jitter. By default, the mixer automatically determines +the buffering parameters needed for continuous playback in the presence of +jitter. If jitter for a given scenario is known, the expected jitter can be +configured such that the mixer won't try to optimize latency beyond known-good +bounds. + +Configuration +~~~~~~~~~~~~~ + +A simple mixer configuration looks as follows: + +! +! +! +! +! +! +! +! +! + +This configuration defines two signals "left" and "right" that are mixed from +the audio input of the matching clients. In the example, each play +session labeled as "left" is mixed into the "left" signal. Each node +can host an arbitrary number of nodes. The same policy can appear +at multiple nodes. + +A node assigns a signal to a record client. In the example, a record +client labeled "left" is connected to the signal "left". + +The mixer allows for the cascading of nodes. For example, the following +signal "lefty" is a mix if the two signals "left" and "right", weighted by +respective volume attributes. + +! +! +! +! + +The 'volume' can be specified for , , and nodes and +defines a factor that is multiplied with each sample value. + +The 'jitter_ms' value at the node denotes the expected jitter. The +mixer won't try to optimize latency beyond that value. A jitter of 5 ms +means that the periodicity of play and record clients is expected to vary +from period to period up to 5 ms. To attain continuous playback given +this situation, the mixer buffers 10 ms of data. In the worst case - should +the play client come 5 ms too late while the record client comes 5 ms too +early - the audio still does not stutter. + +In cases where the jittering behavior of client differs, it is possible to +define the expected jitter as 'jitter_ms' attribute at individual and + nodes. This way, a high-priority audio driver strictly driven by +interrupts in 5 ms intervals can be configured to a low jitter value like 1 +ms. This way, a high-priority sporadic play client operating at a period of 5 +ms can attain low latency while a low-priority media player operating at a +period of 80 ms could be configured to a jitter of 10 ms. In the presence of +multiple jitter attributes (e.g, one present in the node and one +present in a node, the highest value takes effect. + +Example +~~~~~~~ + +The _gems/run/waveform_player.run_ script illustrates the integration and +configuration of the mixer by using a waveform generator as play client and an +oscilloscope as record client. diff --git a/repos/os/src/server/record_play_mixer/audio_signal.h b/repos/os/src/server/record_play_mixer/audio_signal.h new file mode 100644 index 0000000000..7269c7e22c --- /dev/null +++ b/repos/os/src/server/record_play_mixer/audio_signal.h @@ -0,0 +1,66 @@ +/* + * \brief Audio-signal types + * \author Norman Feske + * \date 2023-12-13 + */ + +/* + * Copyright (C) 2023 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. + */ + +#ifndef _AUDIO_SIGNAL_H_ +#define _AUDIO_SIGNAL_H_ + +/* Genode includes */ +#include + +/* local includes */ +#include + +namespace Mixer { class Audio_signal; } + + +class Mixer::Audio_signal : List_model::Element, public Sample_producer +{ + public: + + using Name = String<32>; + + static constexpr auto mix_type_name = "mix"; + + private: + + friend class List_model; + friend class List; + + public: + + Name const name; + + Audio_signal(Name const name) : name(name) { }; + + virtual void update(Xml_node const &) { } + + virtual void bind_inputs(List_model &, Play_sessions &) = 0; + + /** + * List_model::Element + */ + bool matches(Xml_node const &node) const + { + return node.attribute_value("name", Name()) == name; + } + + /** + * List_model::Element + */ + static bool type_matches(Xml_node const &node) + { + return node.has_type(mix_type_name); + } +}; + +#endif /* _AUDIO_SIGNAL_H_ */ diff --git a/repos/os/src/server/record_play_mixer/main.cc b/repos/os/src/server/record_play_mixer/main.cc new file mode 100644 index 0000000000..81abf7d093 --- /dev/null +++ b/repos/os/src/server/record_play_mixer/main.cc @@ -0,0 +1,215 @@ +/* + * \brief Audio mixer + * \author Norman Feske + * \date 2023-12-13 + */ + +/* + * Copyright (C) 2023 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 + +/* local includes */ +#include +#include +#include + +namespace Mixer { struct Main; } + + +struct Mixer::Main : Record_session::Operations, Play_session::Operations +{ + Env &_env; + + Attached_rom_dataspace _config { _env, "config" }; + + Heap _heap { _env.ram(), _env.rm() }; + + Timer::Connection _timer { _env }; + + Expanding_reporter _state_reporter { _env, "state", "state" }; + + Play_sessions _play_sessions { }; + Record_sessions _record_sessions { }; + + Play_root _play_root { _env, _heap, _play_sessions, *this }; + Record_root _record_root { _env, _heap, _record_sessions, *this }; + + using Config_version = String<32>; + Config_version _version { }; + + Constructible _clock_from_config { }; + + Time_window_scheduler::Config _global_record_config { }; + unsigned _global_play_jitter_us { }; + + /** + * Record_session::Operations + */ + Clock current_clock_value() override + { + if (_clock_from_config.constructed()) + return *_clock_from_config; + + return Clock { unsigned(_timer.elapsed_us()) }; + } + + List_model _audio_signals { }; + + /** + * Record_session::Operations + */ + void bind_sample_producers_to_record_sessions() override + { + _record_sessions.for_each([&] (Record_session &record_session) { + + record_session.release_sample_producer(); + + using Name = Audio_signal::Name; + with_matching_policy(record_session.label(), _config.xml(), + [&] (Xml_node const &policy) { + record_session.apply_config(policy, _global_record_config); + Name const name = policy.attribute_value("record", Name()); + _audio_signals.for_each([&] (Audio_signal &audio_signal) { + if (audio_signal.name == name) + record_session.assign_sample_producer(audio_signal); }); + }, + [&] { + log("no policy for session ", record_session.label()); + }); + }); + } + + /** + * Play_session::Operations + */ + void bind_play_sessions_to_audio_signals() override + { + _play_sessions.for_each([&] (Play_session &play_session) { + play_session.global_jitter_us(_global_play_jitter_us); }); + + _audio_signals.for_each([&] (Audio_signal &audio_signal) { + audio_signal.bind_inputs(_audio_signals, _play_sessions); }); + } + + /** + * Play_session::Operations + */ + void wakeup_record_clients() override + { + _record_sessions.for_each([&] (Record_session &record_session) { + record_session.wakeup(); }); + } + + void _generate_state_report(Xml_generator &xml) const + { + if (_clock_from_config.constructed()) + xml.attribute("clock_value", _clock_from_config->us()); + (void)xml; + } + + void _update_state_report() + { + _state_reporter.generate([&] (Xml_generator &xml) { + xml.attribute("version", _version); + _generate_state_report(xml); + }); + } + + void _handle_config() + { + _config.update(); + Xml_node const config = _config.xml(); + + double const default_jitter_ms = config.attribute_value("jitter_ms", 1.0); + _global_record_config = { + .period_us = us_from_ms_attr(config, "record_period_ms", 5.0), + .jitter_us = us_from_ms_attr(config, "record_jitter_ms", default_jitter_ms), + }; + _global_play_jitter_us = us_from_ms_attr(config, "play_jitter_ms", default_jitter_ms); + + _version = config.attribute_value("version", _version); + + _clock_from_config.destruct(); + if (config.has_attribute("clock_value")) + _clock_from_config.construct(config.attribute_value("clock_value", 0u)); + + _audio_signals.update_from_xml(config, + + /* create */ + [&] (Xml_node const &node) -> Audio_signal & { + + if (node.has_type(Audio_signal::mix_type_name)) + return *new (_heap) Mix_signal(node, _heap); + + error("unable to create signal: ", node); + sleep_forever(); /* should never be reachable */ + }, + + /* destroy */ + [&] (Audio_signal &audio_signal) { + destroy(_heap, &audio_signal); }, + + /* update */ + [&] (Audio_signal &audio_signal, Xml_node const &node) { + audio_signal.update(node); } + ); + + bind_play_sessions_to_audio_signals(); + bind_sample_producers_to_record_sessions(); + + _update_state_report(); + } + + struct Timer_count { unsigned value; }; + + Timer_count _count { }; + + void _handle_timer() + { + _count.value++; + } + + Timer_count _once_in_a_while_triggered { }; + + bool once_in_a_while() override + { + if (_count.value == _once_in_a_while_triggered.value) + return false; + + _once_in_a_while_triggered = _count; + return true; + } + + Signal_handler
_config_handler + { _env.ep(), *this, &Main::_handle_config }; + + Signal_handler
_timer_handler + { _env.ep(), *this, &Main::_handle_timer }; + + Main(Genode::Env &env) : _env(env) + { + _config.sigh(_config_handler); + _handle_config(); + + _timer.sigh(_timer_handler); + _timer.trigger_periodic(1000*1000); + + _env.parent().announce(_env.ep().manage(_play_root)); + _env.parent().announce(_env.ep().manage(_record_root)); + } +}; + + +void Component::construct(Genode::Env &env) { static Mixer::Main inst(env); } diff --git a/repos/os/src/server/record_play_mixer/median.h b/repos/os/src/server/record_play_mixer/median.h new file mode 100644 index 0000000000..a8806fb13e --- /dev/null +++ b/repos/os/src/server/record_play_mixer/median.h @@ -0,0 +1,65 @@ +/* + * \brief Utility to determine the median of N values + * \author Norman Feske + * \date 2023-12-13 + */ + +/* + * 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. + */ + +#ifndef _MEDIAN_H_ +#define _MEDIAN_H_ + +namespace Mixer { template struct Median; } + + +template +struct Mixer::Median +{ + T sorted[N] { }; + + unsigned n = 0; + + void capture(T v) + { + if (n >= N) + return; + + unsigned pos = 0; + + while ((pos < n) && (v > sorted[pos])) + pos++; + + n++; + for (unsigned i = n; i > pos; i--) + sorted[i] = sorted[i - 1]; + + sorted[pos] = v; + } + + T median() const { return sorted[n/2]; } + + T jitter() const + { + if (n < 2) + return { }; + + return max(median() - sorted[0], sorted[n - 1] - median()); + } + + void print(Output &out) const + { + for (unsigned i = 0; i < n; i++) { + Genode::print(out, sorted[i]); + if (i + 1 < n) + Genode::print(out, ", "); + } + } +}; + +#endif /* _MEDIAN_H_ */ + diff --git a/repos/os/src/server/record_play_mixer/mix_signal.h b/repos/os/src/server/record_play_mixer/mix_signal.h new file mode 100644 index 0000000000..eec7573e7c --- /dev/null +++ b/repos/os/src/server/record_play_mixer/mix_signal.h @@ -0,0 +1,237 @@ +/* + * \brief Mix signal + * \author Norman Feske + * \date 2023-18-13 + */ + +/* + * Copyright (C) 2023 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. + */ + +#ifndef _MIX_SIGNAL_H_ +#define _MIX_SIGNAL_H_ + +/* local includes */ +#include + +namespace Mixer { class Mix_signal; } + + +struct Mixer::Mix_signal : Audio_signal +{ + Allocator &_alloc; + + Volume _volume { }; + + struct Input : Interface + { + Volume volume; + + Input(Xml_node const &node) : volume(Volume::from_xml(node)) { } + }; + + struct Named_signal_input : Input + { + using Name = Audio_signal::Name; + + Name const name; + + struct { Sample_producer *_sample_producer_ptr = nullptr; }; + + Named_signal_input(Xml_node const &node) + : + Input(node), name(node.attribute_value("name", Name())) + { } + + void with_sample_producer(auto const &fn) + { + if (_sample_producer_ptr) + fn(*_sample_producer_ptr, volume); + } + }; + + struct Play_session_input : Input + { + using Label = String<64>; + using Suffix = String<64>; + + Allocator &_alloc; + + Label const label; + Suffix const suffix; + unsigned const jitter_us; + + struct Sample_producer_ptr { Sample_producer *ptr = nullptr; }; + + using Registered_sample_producer_ptr = Registered_no_delete; + + Registry _sample_producer_ptrs { }; + + Play_session_input(Xml_node const &node, Allocator &alloc) + : + Input(node), _alloc(alloc), + label (node.attribute_value("label", Label())), + suffix(node.attribute_value("label_suffix", Suffix())), + jitter_us(us_from_ms_attr(node, "jitter_ms", 0.0)) + { } + + ~Play_session_input() { detach_all_producers(); } + + void try_attach(Play_session &session) + { + auto matches = [&] (Session_label const &session_label) -> bool + { + if (label.valid() && label == session_label) + return true; + + if (!suffix.valid()) + return false; + + if (session_label.length() < suffix.length()) + return false; + + size_t const len = suffix.length() - 1; + char const * const label_suffix_cstring = + session_label.string() + session_label.length() - 1 - len; + + return (strcmp(label_suffix_cstring, suffix.string(), len) == 0); + }; + + if (!matches(session.label())) + return; + + session.expect_jitter_us(jitter_us); + + new (_alloc) Registered_sample_producer_ptr(_sample_producer_ptrs, + Sample_producer_ptr { &session }); + } + + void detach_all_producers() + { + _sample_producer_ptrs.for_each([&] (Registered_sample_producer_ptr &ptr) { + destroy(_alloc, &ptr); }); + } + + void for_each_sample_producer(auto const &fn) + { + _sample_producer_ptrs.for_each([&] (Sample_producer_ptr &ptr) { + if (ptr.ptr) + fn(*ptr.ptr, volume); }); + } + }; + + Registry> _named_signal_inputs { }; + Registry> _play_session_inputs { }; + + Sample_buffer<512> _input_buffer { }; + + bool _input_buffer_used = false; + bool _warned_once = false; + + /* + * Helper to protect against nested call of 'produce_sample_data' + */ + struct Used_guard + { + bool &_used; + Used_guard(bool &used) : _used(used) { _used = true; } + ~Used_guard() { _used = false; } + }; + + Mix_signal(Xml_node const &node, Allocator &alloc) + : + Audio_signal(node.attribute_value("name", Name())), + _alloc(alloc) + { } + + /** + * Audio_signal interface + */ + void update(Xml_node const &node) override + { + _volume = Volume::from_xml(node); + + _named_signal_inputs.for_each([&] (Registered &input) { + destroy(_alloc, &input); }); + + _play_session_inputs.for_each([&] (Registered &input) { + destroy(_alloc, &input); }); + + node.for_each_sub_node([&] (Xml_node const &input_node) { + + if (input_node.has_type("signal")) + new (_alloc) Registered(_named_signal_inputs, input_node); + + if (input_node.has_type("play")) + new (_alloc) Registered(_play_session_inputs, input_node, _alloc); + }); + } + + /** + * Audio_signal interface + */ + void bind_inputs(List_model &named_signals, + Play_sessions &play_sessions) override + { + _named_signal_inputs.for_each([&] (Named_signal_input &input) { + named_signals.for_each([&] (Audio_signal &named_signal) { + if (named_signal.name == input.name) { + input._sample_producer_ptr = &named_signal; } }); }); + + _play_session_inputs.for_each([&] (Play_session_input &input) { + input.detach_all_producers(); + play_sessions.for_each([&] (Play_session &play_session) { + input.try_attach(play_session); }); }); + } + + /** + * Sample_producer interface + */ + bool produce_sample_data(Time_window const tw, Float_range_ptr &samples) override + { + if (_input_buffer_used) { + if (!_warned_once) + error("attempt to feed output (", name, ") as input to the same node"); + _warned_once = true; + return false; + } + + samples.clear(); + + Used_guard guard(_input_buffer_used); + + auto for_each_sample_producer = [&] (auto const &fn) + { + _named_signal_inputs.for_each([&] (Named_signal_input &input) { + input.with_sample_producer([&] (Sample_producer &p, Volume v) { + fn(p, v); }); }); + + _play_session_inputs.for_each([&] (Play_session_input &input) { + input.for_each_sample_producer([&] (Sample_producer &p, Volume v) { + fn(p, v); }); }); + }; + + bool result = false; + + for_each_sub_window<_input_buffer.CAPACITY>(tw, samples, + [&] (Time_window sub_tw, Float_range_ptr &dst) { + for_each_sample_producer([&] (Sample_producer &producer, Volume volume) { + + /* render input into '_input_buffer", mix result into 'dst' */ + Float_range_ptr input_dst(_input_buffer.values, dst.num_floats); + input_dst.clear(); + result |= producer.produce_sample_data(sub_tw, input_dst); + input_dst.scale(volume.value); + dst.add(input_dst); + }); + }); + + samples.scale(_volume.value); + return result; + } +}; + +#endif /* _MIX_SIGNAL_H_ */ diff --git a/repos/os/src/server/record_play_mixer/play_session.h b/repos/os/src/server/record_play_mixer/play_session.h new file mode 100644 index 0000000000..2ed054223f --- /dev/null +++ b/repos/os/src/server/record_play_mixer/play_session.h @@ -0,0 +1,477 @@ +/* + * \brief Play service of the audio mixer + * \author Norman Feske + * \date 2023-12-13 + */ + +/* + * Copyright (C) 2023 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. + */ + +#ifndef _PLAY_SESSION_H_ +#define _PLAY_SESSION_H_ + +/* Genode includes */ +#include +#include +#include +#include + +/* local includes */ +#include +#include + +namespace Mixer { class Play_root; } + + +class Mixer::Play_session : public Session_object, + public Sample_producer, + private Registry::Element +{ + public: + + struct Operations : virtual Clock_operations + { + virtual void bind_play_sessions_to_audio_signals() = 0; + virtual void wakeup_record_clients() = 0; + }; + + private: + + Attached_ram_dataspace _ds; + + Shared_buffer const &_buffer = *_ds.local_addr(); + + Operations &_operations; + + Play::Seq _latest_seq { }; + Play::Seq _stopped_seq { }; /* latest seq number at stop time */ + + bool _stopped() const { return _latest_seq.value() == _stopped_seq.value(); } + + using Scheduler = Time_window_scheduler; + + Scheduler _scheduler { }; + + unsigned _expected_jitter_us = 0; + + /* + * Cached meta data fetched from shared buffer + */ + struct Slot + { + Clock start, end; + unsigned sample_start; + unsigned num_samples; + Play::Seq seq; + + unsigned duration_us = end.us_since(start); + + bool _valid() const { return duration_us > 0 && num_samples > 1; } + + unsigned dt() const + { + return _valid() ? duration_us / num_samples : 0; + } + + bool contains(Clock t) const + { + return _valid() && !start.later_than(t) + && t.earlier_than(end); + } + + void with_index_for_t(Clock t, auto const &fn) const + { + if (!contains(t)) + return; + + unsigned const rel_t = t.us_since(start); + unsigned const index = (rel_t*num_samples) / duration_us; + + fn(index); + } + + void with_time_window_at_index(unsigned i, auto const &fn) const + { + if (i >= num_samples || !_valid()) + return; + + Clock const t_start = start.after_us((i*duration_us) / num_samples); + + fn(t_start, dt()); + } + + float u_for_t(unsigned index, Clock t) const + { + float result = 0.5f; + with_time_window_at_index(index, [&] (Clock const t_start, unsigned dt) { + result = float(t.us_since(t_start))/float(dt); }); + + auto clamped = [] (float v) { return min(1.0f, max(0.0f, v)); }; + + return clamped(result); + } + + void print(Output &out) const + { + Genode::print(out, Time_window { start.us(), end.us() }, " seq=", seq.value()); + } + }; + + Slot _slots[Shared_buffer::NUM_SLOTS] { }; + + /** + * Coordinate of a sample within the shared buffer + */ + struct Position + { + unsigned slot_id; + unsigned index; /* relative to the slot's 'sample_start' */ + + Position next(Play_session const &session) const + { + if (index + 1 < session._slots[slot_id].num_samples) + return { + .slot_id = slot_id, + .index = index + 1, + }; + + /* proceed to next slot */ + return { + .slot_id = (slot_id + 1) % Shared_buffer::NUM_SLOTS, + .index = 0, + }; + } + }; + + struct Probe + { + float v[4] { }; + float u = 0.0f; + + Probe(Play_session const &session, Position pos, Clock t) + { + /* + * Technically, the 'u' value ought to be computed between t1 + * and t2 (not between t0 and t1). Since the sample values are + * taken in steps of dt, the u values are the same except when + * dt is not constant (when crossing slot boundaries). However, + * even in this case, u_01 approximates u_12. + */ + u = session._slots[pos.slot_id].u_for_t(pos.index, t); + + for (unsigned i = 0; i < 4; i++, pos = pos.next(session)) { + unsigned const index = session._slots[pos.slot_id].sample_start + pos.index; + v[i] = session._buffer.samples[index % Shared_buffer::MAX_SAMPLES]; + } + } + + void print(Output &out) const + { + Genode::print(out, " ", v[0], " ", v[1], " (u:", Right_aligned(6, u), ")" + " ", v[2], " ", v[3]); + } + }; + + enum class Probe_result { OK, MISSING, AMBIGUOUS }; + + Probe_result _with_start_position_at(Clock t, auto fn) const + { + Position pos { }; + + unsigned matching_slots = 0; + + for (unsigned slot_id = 0; slot_id < Shared_buffer::NUM_SLOTS; slot_id++) { + _slots[slot_id].with_index_for_t(t, [&] (unsigned index) { + matching_slots++; + pos = Position { .slot_id = slot_id, + .index = index, }; }); } + + if (matching_slots == 1u) { + fn(pos); + return Probe_result::OK; + } + + if (matching_slots == 0u) + return Probe_result::MISSING; + + return Probe_result::AMBIGUOUS; + } + + Probe_result _with_interpolated_sample_value(Clock const t, auto const &fn) const + { + return _with_start_position_at(t, [&] (Position pos) { + + Probe probe(*this, pos, t); + + /* b-spline blending functions (u and v denote position v1 <-> v2) */ + float const u = probe.u, v = 1.0f - u, + uu = u*u, uuu = u*uu, + vv = v*v, vvv = v*vv; + + float const b0 = vvv/6.0f, + b1 = uuu/2.0f - uu + 4.0f/6.0f, + b2 = vvv/2.0f - vv + 4.0f/6.0f, + b3 = uuu/6.0f; + + float const avg = b0*probe.v[0] + b1*probe.v[1] + b2*probe.v[2] + b3*probe.v[3]; + + fn(avg); + }); + } + + public: + + Play_session(Play_sessions &sessions, + Env &env, + Resources const &resources, + Label const &label, + Diag const &diag, + Operations &operations) + : + Session_object(env.ep(), resources, label, diag), + Registry::Element(sessions, *this), + _ds(env.ram(), env.rm(), Play::Session::DATASPACE_SIZE), + _operations(operations) + { + _operations.bind_play_sessions_to_audio_signals(); + } + + void global_jitter_us(unsigned us) { _expected_jitter_us = us; }; + + void expect_jitter_us(unsigned us) + { + _expected_jitter_us = max(_expected_jitter_us, us); + } + + /** + * Sample_producer interface + */ + bool produce_sample_data(Time_window tw, Float_range_ptr &samples) override + { + bool result = false; + + /* + * Make local copy of meta data from shared buffer to ensure + * operating on consistent values during 'produce_sample_data'. + * Import slot meta data only if not currently modified by the + * client. + */ + for (unsigned i = 0; i < Shared_buffer::NUM_SLOTS; i++) { + + Play::Session::Shared_buffer::Slot const &src = _buffer.slots[i]; + Slot &dst = _slots[i]; + + dst = { }; + + Play::Seq const acquired_seq = src.acquired_seq; + + Slot const slot { .start = Clock { src.time_window.start }, + .end = Clock { src.time_window.end }, + .sample_start = src.sample_start.index, + .num_samples = src.num_samples.value(), + .seq = src.acquired_seq }; + + Play::Seq const committed_seq = src.committed_seq; + + if (acquired_seq.value() == committed_seq.value()) { + dst = slot; + + if (_latest_seq < slot.seq) + _latest_seq = slot.seq; + } + } + + auto anything_scheduled = [&] + { + for (unsigned i = 0; i < Shared_buffer::NUM_SLOTS; i++) + if (_slots[i].num_samples) + return true; + return false; + }; + + if (!anything_scheduled()) + return false; + + for_each_sub_window<1>(tw, samples, [&] (Time_window sub_tw, Float_range_ptr &dst) { + + Clock const t { sub_tw.start }; + + auto probe_result = _with_interpolated_sample_value(t, + [&] (float v) { + dst.start[0] = v; + result = true; }); + + if (probe_result == Probe_result::OK) + return; + + if (_operations.once_in_a_while()) { + + bool earlier_than_avail_samples = false, + later_than_avail_samples = false; + + for (unsigned i = 0; i < Shared_buffer::NUM_SLOTS; i++) { + if (_slots[i].duration_us) { + if (t.earlier_than(_slots[i].start)) + earlier_than_avail_samples = true; + + if (_slots[i].end.earlier_than(t)) + later_than_avail_samples = true; + } + } + + if (probe_result == Probe_result::MISSING) { + if (earlier_than_avail_samples) { + warning("required sample value is no longer available"); + warning("(jitter config or period too high?)"); + } + else if (later_than_avail_samples && !_stopped()) { + warning("required sample is not yet available"); + warning("(increase 'jitter_ms' config attribute?)"); + } + } + + if (probe_result == Probe_result::AMBIGUOUS) + warning("ambiguous sample value for t=", float(t.us())/1000); + } + }); + + return result; + } + + + /**************************** + ** Play session interface ** + ****************************/ + + Dataspace_capability dataspace() { return _ds.cap(); } + + Play::Time_window schedule(Play::Time_window previous, + Play::Duration duration, + Play::Num_samples num_samples) + { + using Time_window = Play::Time_window; + + if (!duration.valid() || num_samples.value() == 0) + return { }; + + /* playback just started, reset scheduler */ + if (previous.start == previous.end) + _scheduler = { }; + + _scheduler.track_activity({ + .time = _operations.current_clock_value(), + .num_samples = num_samples.value() + }); + + if (_scheduler.learned_jitter_ms() > _expected_jitter_us/1000) { + if (_operations.once_in_a_while()) { + warning("jitter of ", _scheduler.learned_jitter_ms(), " ms is higher than expected"); + warning("(increase 'jitter_ms' attribute of node?)"); + } + } + + Scheduler::Config const config { + .period_us = duration.us, + .jitter_us = _expected_jitter_us + }; + + Scheduler::Play_window_result const window = + _scheduler.play_window(config, previous, num_samples); + + if (!_scheduler.consecutive()) + _operations.wakeup_record_clients(); + + return window.convert( + [&] (Time_window const &tw) -> Time_window { return tw; }, + [&] (Scheduler::Play_window_error e) -> Time_window { + Scheduler::Stats const stats = _scheduler.stats(); + unsigned const period_us = stats.median_period_us; + + switch (e) { + + case Scheduler::Play_window_error::JITTER_TOO_LARGE: + + if (_operations.once_in_a_while()) + warning("jitter too large for period of ", + float(period_us)/1000.0f, " ms"); + + return { + .start = previous.end, + .end = Clock{previous.end}.after_us(1000).us() + }; + + case Scheduler::Play_window_error::INACTIVE: + /* cannot happen because of 'track_activity' call above */ + error("attempt to allocate play window w/o activity"); + } + return { }; + } + ); + } + + void stop() + { + /* remember latest seq number at stop time */ + for (unsigned i = 0; i < Shared_buffer::NUM_SLOTS; i++) + if (_latest_seq < _buffer.slots[i].committed_seq) + _latest_seq = _buffer.slots[i].committed_seq; + + _stopped_seq = _latest_seq; + + /* discard period-tracking state */ + _scheduler = { }; + } +}; + + +class Mixer::Play_root : public Root_component +{ + private: + + Env &_env; + Play_sessions &_sessions; + Play_session::Operations &_operations; + + protected: + + Play_session *_create_session(const char *args) override + { + if (session_resources_from_args(args).ram_quota.value < Play::Session::DATASPACE_SIZE) + throw Insufficient_ram_quota(); + + return new (md_alloc()) + Play_session(_sessions, + _env, + session_resources_from_args(args), + session_label_from_args(args), + session_diag_from_args(args), + _operations); + } + + void _upgrade_session(Play_session *s, const char *args) override + { + s->upgrade(ram_quota_from_args(args)); + s->upgrade(cap_quota_from_args(args)); + } + + void _destroy_session(Play_session *session) override + { + Genode::destroy(md_alloc(), session); + _operations.bind_play_sessions_to_audio_signals(); + } + + public: + + Play_root(Env &env, Allocator &md_alloc, Play_sessions &sessions, + Play_session::Operations &operations) + : + Root_component(&env.ep().rpc_ep(), &md_alloc), + _env(env), _sessions(sessions), _operations(operations) + { } +}; + +#endif /* _PLAY_SESSION_H_ */ diff --git a/repos/os/src/server/record_play_mixer/record_session.h b/repos/os/src/server/record_play_mixer/record_session.h new file mode 100644 index 0000000000..facb9d53fb --- /dev/null +++ b/repos/os/src/server/record_play_mixer/record_session.h @@ -0,0 +1,265 @@ +/* + * \brief Record service of the audio mixer + * \author Norman Feske + * \date 2023-12-13 + */ + +/* + * Copyright (C) 2023 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. + */ + +#ifndef _RECORD_SESSION_H_ +#define _RECORD_SESSION_H_ + +/* Genode includes */ +#include +#include +#include + +/* local includes */ +#include +#include + +namespace Mixer { + + class Record_session; + using Record_sessions = Registry; + class Record_root; +} + + +class Mixer::Record_session : public Session_object, + private Registry::Element +{ + public: + + struct Operations : virtual Clock_operations + { + virtual void bind_sample_producers_to_record_sessions() = 0; + }; + + private: + + /* + * Noncopyable + */ + Record_session(Record_session const &); + Record_session &operator = (Record_session const &); + + Attached_ram_dataspace _ds; + Signal_context_capability _wakeup_sigh { }; + + using Scheduler = Time_window_scheduler; + + Scheduler::Config _config { }; + Scheduler _scheduler { }; + + float _volume { }; + + Time_window _previous { }; + + Constructible _stalled { }; + + Sample_producer *_sample_producer_ptr = nullptr; + + Operations &_operations; + + bool _produce_scaled_sample_data(Time_window tw, Float_range_ptr &samples_ptr) + { + if (!_sample_producer_ptr) + return false; + + if (!_sample_producer_ptr->produce_sample_data(tw, samples_ptr)) + return false; + + samples_ptr.scale(_volume); + return true; + } + + public: + + Record_session(Record_sessions &sessions, + Env &env, + Resources const &resources, + Label const &label, + Diag const &diag, + Operations &operations) + : + Session_object(env.ep(), resources, label, diag), + Registry::Element(sessions, *this), + _ds(env.ram(), env.rm(), Record::Session::DATASPACE_SIZE), + _operations(operations) + { + _operations.bind_sample_producers_to_record_sessions(); + } + + void wakeup() + { + if (!_scheduler.consecutive() && _wakeup_sigh.valid()) + Signal_transmitter(_wakeup_sigh).submit(); + } + + void assign_sample_producer(Sample_producer &s) { _sample_producer_ptr = &s; } + + void release_sample_producer() { _sample_producer_ptr = nullptr; } + + void apply_config(Xml_node const &config, Scheduler::Config global) + { + _volume = float(config.attribute_value("volume", 1.0)); + + _config = global; + + auto override_us_from_ms_attr = [&] (auto const &attr, auto &value) + { + if (config.has_attribute(attr)) + value = us_from_ms_attr(config, attr, 0.0); + }; + + override_us_from_ms_attr("period_ms", _config.period_us); + override_us_from_ms_attr("jitter_ms", _config.jitter_us); + } + + + /****************************** + ** Record session interface ** + ******************************/ + + Dataspace_capability dataspace() { return _ds.cap(); } + + void wakeup_sigh(Signal_context_capability sigh) + { + _wakeup_sigh = sigh; + wakeup(); /* initial wakeup */ + } + + Record::Session::Record_result record(Record::Num_samples num_samples) + { + Clock const now = _operations.current_clock_value(); + + _scheduler.track_activity({ + .time = now, + .num_samples = num_samples.value() + }); + + if (_scheduler.learned_jitter_ms() > _config.jitter_us/1000) { + if (_operations.once_in_a_while()) { + warning("jitter of ", _scheduler.learned_jitter_ms(), " ms is higher than expected"); + warning("(increase 'jitter_ms' attribute of record node?)"); + } + } + + Time_window time_window { }; + + _scheduler.record_window(_config, _previous).with_result( + [&] (Time_window const &tw) { + time_window = tw; + }, + [&] (Scheduler::Record_window_error e) { + Scheduler::Stats const stats = _scheduler.stats(); + unsigned const period_us = stats.median_period_us; + + switch (e) { + + case Scheduler::Record_window_error::JITTER_TOO_LARGE: + + if (_operations.once_in_a_while()) + warning("jitter too large for period of ", + float(period_us)/1000.0f, " ms"); + + time_window = Time_window { + .start = _previous.end, + .end = Clock{_previous.end}.after_us(1000).us() + }; + break; + + case Scheduler::Record_window_error::INACTIVE: + /* cannot happen because of 'track_activity' call above */ + error("attempt to allocate record window w/o activity"); + } + } + ); + + Float_range_ptr samples_ptr(_ds.local_addr(), num_samples.value()); + + if (_produce_scaled_sample_data(time_window, samples_ptr)) { + _stalled.destruct(); + + } else { + + samples_ptr.clear(); + + /* remember when samples become unavailable */ + if (!_stalled.constructed()) + _stalled.construct(now); + + /* tell the client to stop recording after some time w/o samples */ + if (now.later_than(_stalled->after_us(250*1000))) { + _scheduler = { }; + _stalled.destruct(); + return Depleted(); + } + } + + _previous = time_window; + return time_window; + } + + void record_at(Record::Time_window time_window, Record::Num_samples num_samples) + { + Float_range_ptr samples_ptr(_ds.local_addr(), num_samples.value()); + + if (!_produce_scaled_sample_data(time_window, samples_ptr)) + samples_ptr.clear(); + } +}; + + +class Mixer::Record_root : public Root_component +{ + private: + + Env &_env; + Record_sessions &_sessions; + Record_session::Operations &_operations; + + protected: + + Record_session *_create_session(const char *args) override + { + if (session_resources_from_args(args).ram_quota.value < Record::Session::DATASPACE_SIZE) + throw Insufficient_ram_quota(); + + return new (md_alloc()) + Record_session(_sessions, + _env, + session_resources_from_args(args), + session_label_from_args(args), + session_diag_from_args(args), + _operations); + } + + void _upgrade_session(Record_session *s, const char *args) override + { + s->upgrade(ram_quota_from_args(args)); + s->upgrade(cap_quota_from_args(args)); + } + + void _destroy_session(Record_session *session) override + { + Genode::destroy(md_alloc(), session); + } + + public: + + Record_root(Env &env, Allocator &md_alloc, Record_sessions &sessions, + Record_session::Operations &operations) + : + Root_component(&env.ep().rpc_ep(), &md_alloc), + _env(env), _sessions(sessions), _operations(operations) + { } +}; + +#endif /* _RECORD_SESSION_H_ */ diff --git a/repos/os/src/server/record_play_mixer/target.mk b/repos/os/src/server/record_play_mixer/target.mk new file mode 100644 index 0000000000..c3c4f17a19 --- /dev/null +++ b/repos/os/src/server/record_play_mixer/target.mk @@ -0,0 +1,4 @@ +TARGET = record_play_mixer +SRC_CC = main.cc +LIBS = base +INC_DIR += $(PRG_DIR) diff --git a/repos/os/src/server/record_play_mixer/time_window_scheduler.h b/repos/os/src/server/record_play_mixer/time_window_scheduler.h new file mode 100644 index 0000000000..3faee8d9ca --- /dev/null +++ b/repos/os/src/server/record_play_mixer/time_window_scheduler.h @@ -0,0 +1,255 @@ +/* + * \brief Jitter-aware time-window scheduler + * \author Norman Feske + * \date 2024-01-11 + */ + +/* + * 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. + */ + +#ifndef _TIME_WINDOW_SCHEDULER_H_ +#define _TIME_WINDOW_SCHEDULER_H_ + +/* Genode includes */ +#include + +/* local includes */ +#include +#include + +namespace Mixer { class Time_window_scheduler; } + + +class Mixer::Time_window_scheduler +{ + public: + + struct Entry + { + Clock time; + unsigned num_samples; + }; + + struct Config + { + unsigned period_us; /* period assumed before measurements are available */ + unsigned jitter_us; /* expected lower limit of jitter */ + }; + + struct Stats + { + unsigned rate_hz; + unsigned median_period_us; + unsigned predicted_now_us; + unsigned jitter_us; + + bool valid() const { return median_period_us > 0u; } + + void print(Output &out) const + { + Genode::print(out, "rate_hz=", rate_hz, + " median_period_us=", median_period_us, + " predicted_now_us=", predicted_now_us, + " jitter_us=", jitter_us); + } + }; + + private: + + static constexpr unsigned N = 5; + + Entry _entries[N] { }; + + unsigned _curr_index = 0; + unsigned _num_entries = 0; + + unsigned _learned_jitter_us = 5000; + + void _with_nth_entry(unsigned n, auto const &fn) const + { + unsigned const i = (_curr_index + N - n) % N; + if (n < _num_entries) + fn(_entries[i]); + } + + void _for_each_period(auto const &fn) const + { + for (unsigned i = 0; i + 1 < _num_entries; i++) + _with_nth_entry(i, [&] (Entry const &curr) { + _with_nth_entry(i + 1, [&] (Entry const &prev) { + fn(prev, curr); }); }); + } + + Stats _calc_stats() const + { + unsigned sum_period_us = 0, + num_periods = 0; + + unsigned long sum_ages_us = 0, /* relative to latest */ + sum_samples = 0; + + Entry latest { }; + _with_nth_entry(0, [&] (Entry const e) { latest = e; }); + + Median median_period_us { }; + + _for_each_period([&] (Entry const prev, Entry const curr) { + + unsigned const period_us = curr.time.us_since(prev.time); + + median_period_us.capture(period_us); + + num_periods++; + sum_ages_us += latest.time.us_since(prev.time); + sum_period_us += period_us; + sum_samples += prev.num_samples; + }); + + if (num_periods == 0) + return { }; + + unsigned const avg_age_us = unsigned(sum_ages_us/num_periods); + + return { + .rate_hz = (sum_period_us == 0) ? 0 + : unsigned((sum_samples*1000*1000)/sum_period_us), + + .median_period_us = median_period_us.median(), + + .predicted_now_us = latest.time.before_us(avg_age_us) + .after_us(sum_period_us/2).us(), + + .jitter_us = median_period_us.jitter(), + }; + } + + void _learn_jitter(Stats const &stats) + { + _learned_jitter_us = (99*_learned_jitter_us)/100; + + if (stats.jitter_us > _learned_jitter_us) + _learned_jitter_us = stats.jitter_us; + } + + /* + * Compute delay on account to the (pre-)fetching the four probe + * values. Noticeable with extremely low sample rates and large + * periods. + */ + unsigned _prefetch_us(unsigned sample_rate_hz) const + { + if (sample_rate_hz == 0) + return 0; + + unsigned const sample_distance_ns = (1000*1000*1000)/sample_rate_hz; + + return (4*sample_distance_ns)/1000; + } + + public: + + Time_window_scheduler() { } + + void track_activity(Entry entry) + { + _curr_index = (_curr_index + 1) % N; + _num_entries = min(_num_entries + 1, N); + _entries[_curr_index] = entry; + } + + bool consecutive() const { return (_num_entries > 1); } + + Stats stats() const { return _calc_stats(); } + + unsigned learned_jitter_ms() const { return _learned_jitter_us/1000; } + + enum class Play_window_error { INACTIVE, JITTER_TOO_LARGE }; + + using Play_window_result = Attempt; + + Play_window_result play_window(Config config, + Play::Time_window previous, + Play::Num_samples num_samples) + { + if (_num_entries == 0) + return Play_window_error::INACTIVE; + + if (!consecutive()) { + unsigned const rate_hz = num_samples.value() + ? config.period_us/num_samples.value() + : 0; + unsigned const prefetch_us = _prefetch_us(rate_hz); + Entry const now = _entries[_curr_index]; + return Play::Time_window { + .start = now.time.after_us(prefetch_us).us(), + .end = now.time.after_us(config.period_us + prefetch_us).us() + }; + } + + Stats const stats = _calc_stats(); + + _learn_jitter(stats); + + unsigned const jitter_us = max(_learned_jitter_us, config.jitter_us); + unsigned const delay_us = jitter_us + _prefetch_us(stats.rate_hz); + + Clock const start { previous.end }, + end { stats.predicted_now_us + stats.median_period_us + delay_us }; + + if (Clock::range_valid(start, end)) + return Play::Time_window { start.us(), end.us() }; + + return Play_window_error::JITTER_TOO_LARGE; + } + + enum class Record_window_error { INACTIVE, JITTER_TOO_LARGE }; + + using Record_window_result = Attempt; + + Record_window_result record_window(Config config, Record::Time_window previous) + { + if (_num_entries == 0) + return Record_window_error::INACTIVE; + + if (!consecutive()) { + Entry const now = _entries[_curr_index]; + return Record::Time_window { + .start = now.time.before_us(config.period_us + + config.jitter_us).us(), + .end = now.time.us() + }; + } + + Stats const stats = _calc_stats(); + + _learn_jitter(stats); + + unsigned const jitter_us = max(_learned_jitter_us, config.jitter_us); + + Clock const start { previous.end }, + end { Clock{stats.predicted_now_us}.before_us(jitter_us).us() }; + + if (Clock::range_valid(start, end)) + return Record::Time_window { start.us(), end.us() }; + + return Record_window_error::JITTER_TOO_LARGE; + } + + void print(Output &out) const + { + Stats const stats = _calc_stats(); + + Genode::print(out, "now=", _entries[_curr_index].time.us()/1000, + " (predicted ", stats.predicted_now_us/1000, ")" + " period=", float(stats.median_period_us)/1000.0f, + " jitter=", float(stats.jitter_us)/1000.0f, + " (learned ", float(_learned_jitter_us)/1000.0f, ")", + " prefetch=", float(_prefetch_us(stats.rate_hz))/1000.0f); + } +}; + +#endif /* _TIME_WINDOW_SCHEDULER_H_ */ diff --git a/repos/os/src/server/record_play_mixer/types.h b/repos/os/src/server/record_play_mixer/types.h new file mode 100644 index 0000000000..a35c3663e9 --- /dev/null +++ b/repos/os/src/server/record_play_mixer/types.h @@ -0,0 +1,214 @@ +/* + * \brief Types used by the mixer + * \author Norman Feske + * \date 2023-12-13 + */ + +/* + * Copyright (C) 2023 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. + */ + +#ifndef _TYPES_H_ +#define _TYPES_H_ + +/* Genode includes */ +#include +#include +#include +#include +#include + +namespace Mixer { + + using namespace Genode; + + /** + * Circular clock in microseconds, wrapping after 4 seconds + */ + class Clock + { + private: + + unsigned _us = 0; + + static constexpr unsigned LIMIT = (1 << 22), + MASK = LIMIT - 1; + + static constexpr unsigned _masked(unsigned v) { return v & MASK; } + + static constexpr bool _positive(unsigned v) + { + return (v > 0) && (v < LIMIT/2); + } + + public: + + explicit Clock(unsigned us) : _us(_masked(us)) { }; + + Clock() { } + + unsigned us() const { return _masked(_us); }; + unsigned us_since(Clock past) const { return _masked(_us - past._us); } + + Clock after_us (unsigned us) const { return Clock { _masked(_us + us) }; } + Clock before_us(unsigned us) const { return Clock { _masked(_us - us) }; } + + bool earlier_than(Clock const &other) const + { + return (other.us() < us()) ? _positive(other.us() + LIMIT - us()) + : _positive(other.us() - us()); + } + + bool later_than(Clock const &other) const + { + return other.earlier_than(*this); + } + + static bool range_valid(Clock const &start, Clock const &end) + { + return _positive(end.us_since(start)); + } + }; + + struct Clock_operations : Interface + { + virtual Clock current_clock_value() = 0; + + /** + * Return true if the time is right for latent diagnostic output + * + * Used for limiting the rate of log messages on account of wrong + * audio parameters. + */ + virtual bool once_in_a_while() = 0; + }; + + struct Float_range_ptr : Noncopyable + { + struct { + float *start; + unsigned num_floats; + }; + + Float_range_ptr(float *start, unsigned num_floats) + : start(start), num_floats(num_floats) { } + + void clear() + { + for (size_t i = 0; i < num_floats; i++) + start[i] = 0.0f; + } + + void add(Float_range_ptr const &other) + { + size_t const limit = min(num_floats, other.num_floats); + for (unsigned i = 0; i < limit; i++) + start[i] += other.start[i]; + } + + void scale(float const factor) + { + for (size_t i = 0; i < num_floats; i++) + start[i] *= factor; + } + }; + + template + struct Sample_buffer + { + float values[N] { }; + static constexpr unsigned CAPACITY = N; + }; + + using Time_window = Record::Time_window; + + struct Volume + { + float value; + + static Volume from_xml(Xml_node const &node) + { + return { float(node.attribute_value("volume", 1.0)) }; + } + }; + + struct Sample_producer : Noncopyable, Interface + { + virtual bool produce_sample_data(Time_window, Float_range_ptr &) = 0; + }; + + class Play_session; + using Play_sessions = Registry; + + /** + * Call 'fn' for each sub window of a maximum of N samples + */ + template + void for_each_sub_window(Time_window const tw, Float_range_ptr &samples, auto const &fn) + { + if (samples.num_floats == 0) + return; + + Clock const start { tw.start }, + end { tw.end }; + + unsigned pos = 0; /* position within 'samples' */ + + auto remain = [&] { return samples.num_floats - pos; }; + + /* time per sample in 1/1024 microseconds */ + uint32_t const ascent { (end.us_since(start) << 10) / samples.num_floats }; + + while (remain() > 0) { + + /* samples to process in this iteration */ + unsigned const num_samples = min(remain(), N); + + /* window of 'samples' buffer to process in this iteration */ + Float_range_ptr sub_samples(samples.start + pos, num_samples); + + /* sub window of 'tw' that corresponds to the current iteration */ + unsigned const start_offset_us = pos * ascent, + end_offset_us = (pos + num_samples) * ascent; + + Time_window const sub_window(start.after_us(start_offset_us >> 10).us(), + start.after_us(end_offset_us >> 10).us()); + + fn(sub_window, sub_samples); + + pos += num_samples; + } + } + + unsigned us_from_ms_attr(Xml_node const &node, auto const &attr, double default_value) + { + return unsigned(1000*node.attribute_value(attr, default_value)); + } +} + + +namespace Genode { + + void print(Output &out, Mixer::Time_window const &window) + { + using namespace Mixer; + print(out, window.start/1000, "...", window.end/1000, + " (", Clock{window.end}.us_since(Clock{window.start})/1000, ")"); + } +} + + +static bool operator < (Play::Seq const &l, Play::Seq const &r) +{ + unsigned const LIMIT = Play::Seq::LIMIT; + + auto pos = [] (unsigned v) { return (v > 0) && (v < LIMIT/2); }; + + return (r.value() < l.value()) ? pos(r.value() + LIMIT - l.value()) + : pos(r.value() - l.value()); +} + +#endif /* _TYPES_H_ */ diff --git a/repos/os/src/server/record_rom/main.cc b/repos/os/src/server/record_rom/main.cc new file mode 100644 index 0000000000..4654c7346c --- /dev/null +++ b/repos/os/src/server/record_rom/main.cc @@ -0,0 +1,290 @@ +/* + * \brief Expose recorded data as ROM module + * \author Norman Feske + * \date 2023-01-16 + */ + +/* + * Copyright (C) 2023 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Record_rom { + using namespace Genode; + struct Main; +} + + +struct Record_rom::Main : Dynamic_rom_session::Xml_producer +{ + Env &_env; + + unsigned _period_ms { }; + + Heap _heap { _env.ram(), _env.rm() }; + + Timer::Connection _timer { _env }; + + Attached_rom_dataspace _config { _env, "config" }; + + Signal_handler
_timer_handler { _env.ep(), *this, &Main::_handle_timer }; + Signal_handler
_wakeup_handler { _env.ep(), *this, &Main::_handle_wakeup }; + Signal_handler
_config_handler { _env.ep(), *this, &Main::_handle_config }; + + using Samples_ptr = Record::Connection::Samples_ptr; + + struct Captured_audio + { + static constexpr unsigned SIZE_LOG2 = 10, SIZE = 1 << SIZE_LOG2, MASK = SIZE - 1; + + float _samples[SIZE] { }; + + unsigned _pos = 0; + unsigned _count = 0; + + void insert(float value) + { + _pos = (_pos + 1) & MASK; + _samples[_pos] = value; + _count = min(SIZE, _count + 1); + } + + void insert(Samples_ptr const &samples) + { + for (unsigned i = 0; i < samples.num_samples; i++) + insert(samples.start[i]); + } + + float past_value(unsigned past) const + { + return _samples[(_pos - past) & MASK]; + } + + unsigned count() const { return _count; } + }; + + struct Channel : private List_model>::Element + { + friend class List_model>; + friend class List>; + + using Label = String<20>; + Label const label; + + static Label _label_from_xml(Xml_node const &node) + { + return node.attribute_value("label", Label()); + } + + struct Attr + { + unsigned sample_rate_hz; + + static Attr from_xml(Xml_node const &node, Attr const defaults) + { + return Attr { + .sample_rate_hz = node.attribute_value("sample_rate_hz", defaults.sample_rate_hz), + }; + } + }; + + Attr _attr { }; + + Record::Connection _record; + + Captured_audio _capture { }; + + Channel(Env &env, Xml_node const &node, Signal_context_capability wakeup_sigh) + : + label(_label_from_xml(node)), _record(env, label) + { + _record.wakeup_sigh(wakeup_sigh); + } + + virtual ~Channel() { }; + + void update(Xml_node const &node, Attr const defaults) + { + _attr = Attr::from_xml(node, defaults); + } + + void generate(Xml_generator &xml) const + { + unsigned const num_values = min(_capture.count(), 1000u); + for (unsigned i = 0; i < num_values; i++) + xml.append_content(_capture.past_value(num_values - i), "\n"); + } + + Record::Num_samples num_samples(unsigned period_ms) const + { + return { unsigned((_attr.sample_rate_hz*period_ms)/1000) }; + } + + struct Capture_result + { + Record::Time_window tw; + bool depleted; + }; + + Capture_result capture(Record::Num_samples const num_samples) + { + Capture_result result { }; + _record.record(num_samples, + [&] (Record::Time_window tw, Samples_ptr const &samples) { + result = { .tw = tw, .depleted = false }; + _capture.insert(samples); + }, + [&] { /* audio data depleted */ + result = { .tw = { }, .depleted = true }; + for (unsigned i = 0; i < num_samples.value(); i++) + _capture.insert(0.0f); + } + ); + return result; + } + + void capture_at(Record::Time_window tw, + Record::Num_samples num_samples) + { + _record.record_at(tw, num_samples, + [&] (Samples_ptr const &samples) { + _capture.insert(samples); }); + } + + /* + * List_model::Element + */ + static bool type_matches(Xml_node const &node) + { + return node.has_type("record"); + } + + bool matches(Xml_node const &node) const + { + return _label_from_xml(node) == label; + } + }; + + List_model> _channels { }; + + Registry> _channel_registry { }; /* for non-const for_each */ + + void _handle_wakeup() + { + _timer.trigger_periodic(1000*_period_ms); + } + + void _handle_config() + { + _config.update(); + + Xml_node const config = _config.xml(); + + _period_ms = config.attribute_value("period_ms", 20u); + + /* channel defaults obtained from top-level config node */ + Channel::Attr const channel_defaults = Channel::Attr::from_xml(config, { + .sample_rate_hz = 44100u, }); + + _channels.update_from_xml(config, + [&] (Xml_node const &node) -> Registered & { + return *new (_heap) + Registered(_channel_registry, _env, node, + _wakeup_handler); }, + [&] (Registered &channel) { + destroy(_heap, &channel); }, + [&] (Channel &channel, Xml_node const &node) { + channel.update(node, channel_defaults); } + ); + + _handle_wakeup(); + } + + void _capture_channels() + { + /* + * The first channel drives the time window that is reused for all + * other channels to attain time-synchronized data. + */ + bool first = true; + Channel::Capture_result capture { }; + _channel_registry.for_each([&] (Registered &channel) { + if (first) + capture = channel.capture(channel.num_samples(_period_ms)); + else + channel.capture_at(capture.tw, channel.num_samples(_period_ms)); + first = false; + }); + + if (capture.depleted) + _timer.trigger_periodic(0); + } + + /** + * Dynamic_rom_session::Xml_producer + */ + void produce_xml(Xml_generator &xml) override + { + _channels.for_each([&] (Channel const &channel) { + xml.node("channel", [&] { + xml.attribute("label", channel.label); + xml.attribute("rate_hz", channel._attr.sample_rate_hz); + channel.generate(xml); + }); + }); + } + + void _handle_timer() + { + _capture_channels(); + } + + struct Rom_root : Root_component + { + Env &_env; + Main &_main; + + Dynamic_rom_session *_create_session(const char *) override + { + using namespace Genode; + + return new (md_alloc()) + Dynamic_rom_session(_env.ep(), _env.ram(), _env.rm(), _main); + } + + Rom_root(Env &env, Allocator &md_alloc, Main &main) + : + Root_component(&env.ep().rpc_ep(), &md_alloc), + _env(env), _main(main) + { } + + } _rom_root { _env, _heap, *this }; + + Main(Env &env) : Xml_producer("recording"), _env(env) + { + _timer.sigh(_timer_handler); + _config.sigh(_config_handler); + _handle_config(); + + env.parent().announce(_env.ep().manage(_rom_root)); + } +}; + + +void Component::construct(Genode::Env &env) +{ + static Record_rom::Main main(env); +} diff --git a/repos/os/src/server/record_rom/target.mk b/repos/os/src/server/record_rom/target.mk new file mode 100644 index 0000000000..2854efc891 --- /dev/null +++ b/repos/os/src/server/record_rom/target.mk @@ -0,0 +1,3 @@ +TARGET = record_rom +SRC_CC = main.cc +LIBS = base diff --git a/repos/os/src/test/audio_play/main.cc b/repos/os/src/test/audio_play/main.cc new file mode 100644 index 0000000000..16a0258326 --- /dev/null +++ b/repos/os/src/test/audio_play/main.cc @@ -0,0 +1,97 @@ +/* + * \brief Play audio sample (stereo, interleaved, 32-bit floating point) + * \author Norman Feske + * \date 2024-02-14 + */ + +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include + +namespace Audio_play { + + using namespace Genode; + + struct Main; +} + + +struct Audio_play::Main +{ + Env &_env; + + Heap _heap { _env.ram(), _env.rm() }; + + Attached_rom_dataspace _config { _env, "config" }; + + Vfs::Global_file_system_factory _fs_factory { _heap }; + Vfs::Simple_env _vfs_env { _env, _heap, _config.xml().sub_node("vfs") }; + Directory _root_dir { _vfs_env }; + + Directory::Path const _sample_path = + _config.xml().attribute_value("sample_path", Directory::Path()); + + File_content const _sample_data { + _heap, _root_dir, _sample_path, { _env.pd().avail_ram().value } }; + + Play::Connection _left { _env, "left" }, + _right { _env, "right" }; + + Play::Time_window _time_window { }; + + unsigned _pos = 0; + + unsigned const _period_ms = 5, + _sample_rate_hz = 44100, + _frames_per_period = (_period_ms*_sample_rate_hz)/1000; + + Timer::Connection _timer { _env }; + + Signal_handler
_timer_handler { _env.ep(), *this, &Main::_handle_timer }; + + struct Frame { float left, right; } __attribute__((packed)); + + static_assert(sizeof(Frame) == 8); + + void _for_each_frame_of_period(auto const &fn) const + { + _sample_data.bytes([&] (char const * const ptr, size_t const num_bytes) { + if (num_bytes >= sizeof(Frame)) + for (unsigned i = 0; i < _frames_per_period; i++) + fn(*(Frame const *)(ptr + ((_pos + i*sizeof(Frame)) % num_bytes))); }); + } + + void _handle_timer() + { + _time_window = _left.schedule_and_enqueue(_time_window, { _period_ms*1000 }, + [&] (auto &submit) { + _for_each_frame_of_period([&] (Frame const frame) { + submit(frame.left); }); }); + + _right.enqueue(_time_window, + [&] (auto &submit) { + _for_each_frame_of_period([&] (Frame const frame) { + submit(frame.right); }); }); + + _pos = unsigned(_pos + _frames_per_period*sizeof(Frame)); + } + + Main(Env &env) : _env(env) + { + _timer.sigh(_timer_handler); + _timer.trigger_periodic(_period_ms*1000); + } +}; + + +void Component::construct(Genode::Env &env) { static Audio_play::Main main(env); } diff --git a/repos/os/src/test/audio_play/target.mk b/repos/os/src/test/audio_play/target.mk new file mode 100644 index 0000000000..4f41b90d30 --- /dev/null +++ b/repos/os/src/test/audio_play/target.mk @@ -0,0 +1,3 @@ +TARGET = test-audio_play +SRC_CC = main.cc +LIBS = base vfs