os: record-and-play session interfaces and mixer

- New session interfaces:
  - os/include/play_session   (for audio playing   / mic-input driver)
  - os/include/record_session (for audio recording / audio-output driver)
- Mixer at os/src/record_play_mixer providing both play and record services
- Simple waveform player at os/src/app/waveform_player
- Simple audio-signal capturing component at os/src/app/record_rom
- Simple oscilloscpe at gems/src/app/rom_osci (using record_rom)
- Simple test-audio_play for playing raw stereo f32 data

The _gems/run/waveform_player.run_ script illustrates the use of the new
components and interfaces.

Issue #5097
This commit is contained in:
Norman Feske 2024-02-14 17:41:05 +01:00 committed by Christian Helmuth
parent 914508bf7a
commit 07669ac991
39 changed files with 3633 additions and 0 deletions

View File

@ -0,0 +1,2 @@
SRC_DIR = src/app/rom_osci
include $(GENODE_DIR)/repos/base/recipes/src/content.inc

View File

@ -0,0 +1 @@
2024-01-19 8490b1bc29f4a69c27c9dce66b6630e984310e2b

View File

@ -0,0 +1,10 @@
base
os
blit
gems
framebuffer_session
input_session
gui_session
timer_session
nitpicker_gfx
polygon_gfx

View File

@ -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 {
<config prio_levels="4">
<parent-provides>
<service name="ROM"/>
<service name="CPU"/>
<service name="PD"/>
<service name="RM"/>
<service name="LOG"/>
<service name="IRQ"/>
<service name="IO_MEM"/>
<service name="IO_PORT"/>
<service name="TRACE"/>
</parent-provides>
<default-route>
<any-service> <parent/> <any-child/> </any-service>
</default-route>
<default caps="100"/>
<start name="timer">
<resource name="RAM" quantum="1M"/>
<provides> <service name="Timer"/> </provides>
</start>
<start name="report_rom" priority="-1">
<resource name="RAM" quantum="2M"/>
<provides> <service name="ROM"/> <service name="Report"/> </provides>
<config>
</config>
</start>
<start name="drivers" caps="1500" managing_system="yes" priority="-1">
<resource name="RAM" quantum="64M"/>
<binary name="init"/>
<route>
<service name="ROM" label="config"> <parent label="drivers.config"/> </service>
<service name="Timer"> <child name="timer"/> </service>
<service name="Capture"> <child name="nitpicker"/> </service>
<service name="Event"> <child name="nitpicker"/> </service>
<any-service> <parent/> </any-service>
</route>
</start>
<start name="nitpicker" priority="-2">
<resource name="RAM" quantum="4M"/>
<provides>
<service name="Gui"/> <service name="Capture"/> <service name="Event"/>
</provides>
<config>
<capture/> <event/>
<domain name="pointer" layer="1" content="client" label="no" origin="pointer" />
<domain name="default" layer="3" content="client" label="no" hover="always" />
<policy label_prefix="pointer" domain="pointer"/>
<default-policy domain="default"/>
</config>
</start>
<start name="pointer" priority="-1">
<resource name="RAM" quantum="1M"/>
</start>
<start name="fonts_fs" caps="300" priority="-1">
<resource name="RAM" quantum="8M"/>
<binary name="vfs"/>
<route>
<service name="ROM" label="config"> <parent label="fonts_fs.config"/> </service>
<any-service> <parent/> </any-service>
</route>
<provides> <service name="File_system"/> </provides>
</start>
<start name="mixer">
<resource name="RAM" quantum="2M"/>
<binary name="record_play_mixer"/>
<provides> <service name="Record"/> <service name="Play"/> </provides>
<config jitter_ms="} [jitter_ms] {">
<mix name="left"> <play label_suffix="left"/> </mix>
<mix name="right"> <play label_suffix="right"/> </mix>
<mix name="lefty">
<signal name="left" volume="0.7"/>
<signal name="right" volume="0.3"/>
</mix>
<mix name="righty">
<signal name="left" volume="0.3"/>
<signal name="right" volume="0.7"/>
</mix>
<policy label_suffix="left" record="left"/>
<policy label_suffix="lefty" record="lefty"/>
<policy label_suffix="righty" record="righty"/>
<policy label_suffix="right" record="right"/>
</config>
</start>
<start name="waveform_config" priority="-2">
<binary name="dynamic_rom"/>
<resource name="RAM" quantum="2M"/>
<provides> <service name="ROM"/> </provides>
<config verbose="yes">
<rom name="config">
<inline>
<config period_ms="} [play_period_ms] {">
<play label="left" wave="sine" hz="1000" sample_rate_hz="44100"/>
<play label="right" wave="square" hz="1000" sample_rate_hz="33333"/>
</config>
</inline>
<sleep milliseconds="2000" />
<inline>
<config period_ms="} [play_period_ms] {">
<play label="left" wave="sine" hz="10000" sample_rate_hz="44100"/>
<play label="right" wave="square" hz="10000" sample_rate_hz="33333"/>
</config>
</inline>
<sleep milliseconds="2000" />
<inline>
<config period_ms="} [play_period_ms] {">
<play label="left" wave="sine" hz="0" sample_rate_hz="44100"/>
<play label="right" wave="square" hz="0" sample_rate_hz="33333"/>
</config>
</inline>
<sleep milliseconds="2000" />
</rom>
</config>
</start>
<start name="waveform_player" priority="0">
<resource name="RAM" quantum="1M"/>
<route>
<service name="ROM" label="config"> <child name="waveform_config"/> </service>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
<start name="record_rom" priority="0">
<resource name="RAM" quantum="8M"/>
<provides> <service name="ROM"/> </provides>
<config period_ms="} [record_period_ms] {" sample_rate_hz="44100">
<record label="left"/>
<record label="lefty"/>
<record label="righty"/>
<record label="right"/>
</config>
</start>
<start name="rom_osci" priority="-1">
<resource name="RAM" quantum="8M"/>
<config width="640" height="480" xpos="200" ypos="100" fps="20" v_scale="0.15" phase_lock="yes">
<channel label="left" color="#ff6633" v_pos="0.2"/>
<channel label="lefty" color="#cc7777" v_pos="0.4"/>
<channel label="righty" color="#9988bb" v_pos="0.6"/>
<channel label="right" color="#6699ff" v_pos="0.8"/>
</config>
<route>
<service name="ROM" label="recording"> <child name="record_rom"/> </service>
<any-service> <parent/> <any-child/> </any-service>
</route>
</start>
</config>}
build_boot_image [build_artifacts]
append qemu_args " -nographic "
run_genode_until forever

View File

@ -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 <base/env.h>
#include <base/component.h>
#include <base/attached_rom_dataspace.h>
#include <base/heap.h>
#include <base/registry.h>
#include <util/list_model.h>
#include <gui_session/connection.h>
#include <timer_session/connection.h>
#include <input/event.h>
#include <os/pixel_rgb888.h>
#include <polygon_gfx/line_painter.h>
#include <gems/gui_buffer.h>
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 <typename T>
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> _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<Command::Geometry>(_handle, Rect(position, size));
_gui.enqueue<Command::To_front>(_handle, Gui::Session::View_handle());
_gui.execute();
}
~View() { _gui.destroy_view(_handle); }
};
Constructible<View> _view { };
Attached_rom_dataspace _config { _env, "config" };
Attached_rom_dataspace _recording { _env, "recording" };
Signal_handler<Main> _timer_handler { _env.ep(), *this, &Main::_handle_timer };
Signal_handler<Main> _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<double>(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<Registered<Channel>>::Element
{
friend class List_model<Registered<Channel>>;
friend class List<Registered<Channel>>;
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<Registered<Channel>> _channels { };
Registry<Registered<Channel>> _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<Channel> & {
return *new (_heap) Registered<Channel>(_channel_registry, node); },
[&] (Registered<Channel> &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> &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);
}

View File

@ -0,0 +1,3 @@
TARGET = rom_osci
SRC_CC = main.cc
LIBS = base blit

View File

@ -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 <play_session/play_session.h>
#include <base/connection.h>
#include <base/attached_dataspace.h>
#include <base/sleep.h>
namespace Play { struct Connection; }
struct Play::Connection : Genode::Connection<Session>, Rpc_client<Session>
{
private:
static constexpr Ram_quota RAM_QUOTA = { DATASPACE_SIZE + 4096 };
Attached_dataspace _ds;
Shared_buffer &_buffer = *_ds.local_addr<Shared_buffer>();
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<Session>(env, label, RAM_QUOTA, Args()),
Rpc_client<Session>(cap()),
_ds(env.rm(), call<Rpc_dataspace>())
{
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<Rpc_schedule>(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<Rpc_stop>(); }
};
#endif /* _INCLUDE__RECORD_SESSION__CONNECTION_H_ */

View File

@ -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 <dataspace/capability.h>
#include <base/rpc_server.h>
#include <session/session.h>
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_ */

View File

@ -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 <record_session/record_session.h>
#include <base/connection.h>
#include <base/attached_dataspace.h>
#include <base/sleep.h>
namespace Record { struct Connection; }
struct Record::Connection : Genode::Connection<Session>, Rpc_client<Session>
{
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<Session>(env, label, RAM_QUOTA, Args()),
Rpc_client<Session>(cap()),
_ds(env.rm(), call<Rpc_dataspace>())
{
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<Rpc_wakeup_sigh>(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<Rpc_record>(n).with_result(
[&] (Time_window const &tw) {
fn(tw, Samples_ptr(_ds.local_addr<float const>(), 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<Rpc_record_at>(tw, n);
fn(Samples_ptr(_ds.local_addr<float const>(), n.value()));
}
};
#endif /* _INCLUDE__RECORD_SESSION__CONNECTION_H_ */

View File

@ -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 <dataspace/capability.h>
#include <base/rpc_server.h>
#include <session/session.h>
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<Time_window, Depleted>;
/*********************
** 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_ */

View File

@ -0,0 +1,2 @@
MIRRORED_FROM_REP_DIR := include/play_session
include $(REP_DIR)/recipes/api/session.inc

View File

@ -0,0 +1 @@
2024-01-19 3e0d1397ae70a3d2a9c297d9e8b32eb750889c24

View File

@ -0,0 +1,2 @@
MIRRORED_FROM_REP_DIR := include/record_session
include $(REP_DIR)/recipes/api/session.inc

View File

@ -0,0 +1 @@
2024-01-19 3a9fdc374119a6b1b43fcb6a80ccbea571793f55

View File

@ -0,0 +1,2 @@
SRC_DIR = src/server/record_play_mixer
include $(GENODE_DIR)/repos/base/recipes/src/content.inc

View File

@ -0,0 +1 @@
2024-01-19 2c71de8cc006a5ed9455d8c052584db4a7a238f2

View File

@ -0,0 +1,6 @@
base
os
record_session
play_session
timer_session
report_session

View File

@ -0,0 +1,2 @@
SRC_DIR = src/server/record_rom
include $(GENODE_DIR)/repos/base/recipes/src/content.inc

View File

@ -0,0 +1 @@
2024-01-19 0b59428f86704633fdf5e0507759e512e18f6b20

View File

@ -0,0 +1,4 @@
base
os
timer_session
record_session

View File

@ -0,0 +1,2 @@
SRC_DIR = src/app/waveform_player
include $(GENODE_DIR)/repos/base/recipes/src/content.inc

View File

@ -0,0 +1 @@
2024-01-19 851b68f4d966f6671ff5dbbd008365df547a7728

View File

@ -0,0 +1,4 @@
base
os
timer_session
play_session

View File

@ -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 <util/list_model.h>
#include <base/registry.h>
#include <base/heap.h>
#include <base/component.h>
#include <base/attached_rom_dataspace.h>
#include <timer_session/connection.h>
#include <play_session/connection.h>
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<Main> _config_handler {
_env.ep(), *this, &Main::_handle_config };
Signal_handler<Main> _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<Registered<Channel>>::Element
{
friend class List_model<Registered<Channel>>;
friend class List<Registered<Channel>>;
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<Registered<Channel>> _channels { };
Registry<Registered<Channel>> _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> &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) {
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<Channel> & {
return *new (_heap)
Registered<Channel>(_channel_registry, _env, node); },
[&] (Registered<Channel> &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); }

View File

@ -0,0 +1,3 @@
TARGET = waveform_player
SRC_CC = main.cc
LIBS = base

View File

@ -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:
! <config jitter_ms="5">
!
! <mix name="left"> <play label_suffix="left"/> </mix>
! <mix name="right"> <play label_suffix="right"/> </mix>
!
! <policy label_suffix="left" record="left"/>
! <policy label_suffix="right" record="right"/>
!
! </config>
This configuration defines two signals "left" and "right" that are mixed from
the audio input of the matching <play> clients. In the example, each play
session labeled as "left" is mixed into the "left" signal. Each <mix> node
can host an arbitrary number of <play> nodes. The same <play> policy can appear
at multiple <mix> nodes.
A <policy> node assigns a signal to a record client. In the example, a record
client labeled "left" is connected to the <mix> signal "left".
The mixer allows for the cascading of <mix> nodes. For example, the following
signal "lefty" is a mix if the two signals "left" and "right", weighted by
respective volume attributes.
! <mix name="lefty">
! <signal name="left" volume="0.7"/>
! <signal name="right" volume="0.3"/>
! </mix>
The 'volume' can be specified for <policy>, <play>, and <signal> nodes and
defines a factor that is multiplied with each sample value.
The 'jitter_ms' value at the <config> 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 <play> and
<policy> 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 <config> node and one
present in a <policy> 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.

View File

@ -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 <util/list_model.h>
/* local includes */
#include <types.h>
namespace Mixer { class Audio_signal; }
class Mixer::Audio_signal : List_model<Audio_signal>::Element, public Sample_producer
{
public:
using Name = String<32>;
static constexpr auto mix_type_name = "mix";
private:
friend class List_model<Audio_signal>;
friend class List<Audio_signal>;
public:
Name const name;
Audio_signal(Name const name) : name(name) { };
virtual void update(Xml_node const &) { }
virtual void bind_inputs(List_model<Audio_signal> &, 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_ */

View File

@ -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 <base/component.h>
#include <base/heap.h>
#include <base/attached_rom_dataspace.h>
#include <base/sleep.h>
#include <os/reporter.h>
#include <os/session_policy.h>
#include <timer_session/connection.h>
/* local includes */
#include <play_session.h>
#include <record_session.h>
#include <mix_signal.h>
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> _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_signal> _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<Main> _config_handler
{ _env.ep(), *this, &Main::_handle_config };
Signal_handler<Main> _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); }

View File

@ -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 <typename T, unsigned N> struct Median; }
template <typename T, unsigned N>
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_ */

View File

@ -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 <audio_signal.h>
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<Sample_producer_ptr>;
Registry<Registered_sample_producer_ptr> _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<Registered<Named_signal_input>> _named_signal_inputs { };
Registry<Registered<Play_session_input>> _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<Named_signal_input> &input) {
destroy(_alloc, &input); });
_play_session_inputs.for_each([&] (Registered<Play_session_input> &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_input>(_named_signal_inputs, input_node);
if (input_node.has_type("play"))
new (_alloc) Registered<Play_session_input>(_play_session_inputs, input_node, _alloc);
});
}
/**
* Audio_signal interface
*/
void bind_inputs(List_model<Audio_signal> &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 <mix> 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_ */

View File

@ -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 <util/formatted_output.h>
#include <root/component.h>
#include <base/session_object.h>
#include <play_session/play_session.h>
/* local includes */
#include <types.h>
#include <time_window_scheduler.h>
namespace Mixer { class Play_root; }
class Mixer::Play_session : public Session_object<Play::Session, Play_session>,
public Sample_producer,
private Registry<Play_session>::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<Shared_buffer>();
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<Play_session>::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 <play> 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>(
[&] (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<Play_session>
{
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<Play_session>(&env.ep().rpc_ep(), &md_alloc),
_env(env), _sessions(sessions), _operations(operations)
{ }
};
#endif /* _PLAY_SESSION_H_ */

View File

@ -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 <root/component.h>
#include <base/session_object.h>
#include <record_session/record_session.h>
/* local includes */
#include <audio_signal.h>
#include <time_window_scheduler.h>
namespace Mixer {
class Record_session;
using Record_sessions = Registry<Record_session>;
class Record_root;
}
class Mixer::Record_session : public Session_object<Record::Session, Record_session>,
private Registry<Record_session>::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<Clock> _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<Record_session>::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 <policy> 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<float>(), 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<float>(), num_samples.value());
if (!_produce_scaled_sample_data(time_window, samples_ptr))
samples_ptr.clear();
}
};
class Mixer::Record_root : public Root_component<Record_session>
{
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<Record_session>(&env.ep().rpc_ep(), &md_alloc),
_env(env), _sessions(sessions), _operations(operations)
{ }
};
#endif /* _RECORD_SESSION_H_ */

View File

@ -0,0 +1,4 @@
TARGET = record_play_mixer
SRC_CC = main.cc
LIBS = base
INC_DIR += $(PRG_DIR)

View File

@ -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 <play_session/play_session.h>
/* local includes */
#include <types.h>
#include <median.h>
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<unsigned, N> 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::Time_window, Play_window_error>;
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::Time_window, Record_window_error>;
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_ */

View File

@ -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 <util/string.h>
#include <util/list_model.h>
#include <base/session_label.h>
#include <base/attached_ram_dataspace.h>
#include <record_session/record_session.h>
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 <unsigned N>
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<Play_session>;
/**
* Call 'fn' for each sub window of a maximum of N samples
*/
template <unsigned N>
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_ */

View File

@ -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 <base/env.h>
#include <base/component.h>
#include <base/attached_rom_dataspace.h>
#include <base/heap.h>
#include <base/registry.h>
#include <util/list_model.h>
#include <record_session/connection.h>
#include <timer_session/connection.h>
#include <os/dynamic_rom_session.h>
#include <root/component.h>
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<Main> _timer_handler { _env.ep(), *this, &Main::_handle_timer };
Signal_handler<Main> _wakeup_handler { _env.ep(), *this, &Main::_handle_wakeup };
Signal_handler<Main> _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<Registered<Channel>>::Element
{
friend class List_model<Registered<Channel>>;
friend class List<Registered<Channel>>;
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<Registered<Channel>> _channels { };
Registry<Registered<Channel>> _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<Channel> & {
return *new (_heap)
Registered<Channel>(_channel_registry, _env, node,
_wakeup_handler); },
[&] (Registered<Channel> &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> &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<Dynamic_rom_session>
{
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<Dynamic_rom_session>(&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);
}

View File

@ -0,0 +1,3 @@
TARGET = record_rom
SRC_CC = main.cc
LIBS = base

View File

@ -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 <play_session/connection.h>
#include <timer_session/connection.h>
#include <base/attached_rom_dataspace.h>
#include <base/component.h>
#include <base/heap.h>
#include <os/vfs.h>
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<Main> _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); }

View File

@ -0,0 +1,3 @@
TARGET = test-audio_play
SRC_CC = main.cc
LIBS = base vfs