mirror of
https://github.com/genodelabs/genode.git
synced 2025-01-18 02:40:08 +00:00
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:
parent
914508bf7a
commit
07669ac991
2
repos/gems/recipes/src/rom_osci/content.mk
Normal file
2
repos/gems/recipes/src/rom_osci/content.mk
Normal file
@ -0,0 +1,2 @@
|
||||
SRC_DIR = src/app/rom_osci
|
||||
include $(GENODE_DIR)/repos/base/recipes/src/content.inc
|
1
repos/gems/recipes/src/rom_osci/hash
Normal file
1
repos/gems/recipes/src/rom_osci/hash
Normal file
@ -0,0 +1 @@
|
||||
2024-01-19 8490b1bc29f4a69c27c9dce66b6630e984310e2b
|
10
repos/gems/recipes/src/rom_osci/used_apis
Normal file
10
repos/gems/recipes/src/rom_osci/used_apis
Normal file
@ -0,0 +1,10 @@
|
||||
base
|
||||
os
|
||||
blit
|
||||
gems
|
||||
framebuffer_session
|
||||
input_session
|
||||
gui_session
|
||||
timer_session
|
||||
nitpicker_gfx
|
||||
polygon_gfx
|
198
repos/gems/run/waveform_player.run
Normal file
198
repos/gems/run/waveform_player.run
Normal 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
|
||||
|
340
repos/gems/src/app/rom_osci/main.cc
Normal file
340
repos/gems/src/app/rom_osci/main.cc
Normal 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);
|
||||
}
|
3
repos/gems/src/app/rom_osci/target.mk
Normal file
3
repos/gems/src/app/rom_osci/target.mk
Normal file
@ -0,0 +1,3 @@
|
||||
TARGET = rom_osci
|
||||
SRC_CC = main.cc
|
||||
LIBS = base blit
|
171
repos/os/include/play_session/connection.h
Normal file
171
repos/os/include/play_session/connection.h
Normal 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_ */
|
102
repos/os/include/play_session/play_session.h
Normal file
102
repos/os/include/play_session/play_session.h
Normal 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_ */
|
96
repos/os/include/record_session/connection.h
Normal file
96
repos/os/include/record_session/connection.h
Normal 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_ */
|
79
repos/os/include/record_session/record_session.h
Normal file
79
repos/os/include/record_session/record_session.h
Normal 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_ */
|
2
repos/os/recipes/api/play_session/content.mk
Normal file
2
repos/os/recipes/api/play_session/content.mk
Normal file
@ -0,0 +1,2 @@
|
||||
MIRRORED_FROM_REP_DIR := include/play_session
|
||||
include $(REP_DIR)/recipes/api/session.inc
|
1
repos/os/recipes/api/play_session/hash
Normal file
1
repos/os/recipes/api/play_session/hash
Normal file
@ -0,0 +1 @@
|
||||
2024-01-19 3e0d1397ae70a3d2a9c297d9e8b32eb750889c24
|
2
repos/os/recipes/api/record_session/content.mk
Normal file
2
repos/os/recipes/api/record_session/content.mk
Normal file
@ -0,0 +1,2 @@
|
||||
MIRRORED_FROM_REP_DIR := include/record_session
|
||||
include $(REP_DIR)/recipes/api/session.inc
|
1
repos/os/recipes/api/record_session/hash
Normal file
1
repos/os/recipes/api/record_session/hash
Normal file
@ -0,0 +1 @@
|
||||
2024-01-19 3a9fdc374119a6b1b43fcb6a80ccbea571793f55
|
2
repos/os/recipes/src/record_play_mixer/content.mk
Normal file
2
repos/os/recipes/src/record_play_mixer/content.mk
Normal file
@ -0,0 +1,2 @@
|
||||
SRC_DIR = src/server/record_play_mixer
|
||||
include $(GENODE_DIR)/repos/base/recipes/src/content.inc
|
1
repos/os/recipes/src/record_play_mixer/hash
Normal file
1
repos/os/recipes/src/record_play_mixer/hash
Normal file
@ -0,0 +1 @@
|
||||
2024-01-19 2c71de8cc006a5ed9455d8c052584db4a7a238f2
|
6
repos/os/recipes/src/record_play_mixer/used_apis
Normal file
6
repos/os/recipes/src/record_play_mixer/used_apis
Normal file
@ -0,0 +1,6 @@
|
||||
base
|
||||
os
|
||||
record_session
|
||||
play_session
|
||||
timer_session
|
||||
report_session
|
2
repos/os/recipes/src/record_rom/content.mk
Normal file
2
repos/os/recipes/src/record_rom/content.mk
Normal file
@ -0,0 +1,2 @@
|
||||
SRC_DIR = src/server/record_rom
|
||||
include $(GENODE_DIR)/repos/base/recipes/src/content.inc
|
1
repos/os/recipes/src/record_rom/hash
Normal file
1
repos/os/recipes/src/record_rom/hash
Normal file
@ -0,0 +1 @@
|
||||
2024-01-19 0b59428f86704633fdf5e0507759e512e18f6b20
|
4
repos/os/recipes/src/record_rom/used_apis
Normal file
4
repos/os/recipes/src/record_rom/used_apis
Normal file
@ -0,0 +1,4 @@
|
||||
base
|
||||
os
|
||||
timer_session
|
||||
record_session
|
2
repos/os/recipes/src/waveform_player/content.mk
Normal file
2
repos/os/recipes/src/waveform_player/content.mk
Normal file
@ -0,0 +1,2 @@
|
||||
SRC_DIR = src/app/waveform_player
|
||||
include $(GENODE_DIR)/repos/base/recipes/src/content.inc
|
1
repos/os/recipes/src/waveform_player/hash
Normal file
1
repos/os/recipes/src/waveform_player/hash
Normal file
@ -0,0 +1 @@
|
||||
2024-01-19 851b68f4d966f6671ff5dbbd008365df547a7728
|
4
repos/os/recipes/src/waveform_player/used_apis
Normal file
4
repos/os/recipes/src/waveform_player/used_apis
Normal file
@ -0,0 +1,4 @@
|
||||
base
|
||||
os
|
||||
timer_session
|
||||
play_session
|
330
repos/os/src/app/waveform_player/main.cc
Normal file
330
repos/os/src/app/waveform_player/main.cc
Normal 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); }
|
3
repos/os/src/app/waveform_player/target.mk
Normal file
3
repos/os/src/app/waveform_player/target.mk
Normal file
@ -0,0 +1,3 @@
|
||||
TARGET = waveform_player
|
||||
SRC_CC = main.cc
|
||||
LIBS = base
|
78
repos/os/src/server/record_play_mixer/README
Normal file
78
repos/os/src/server/record_play_mixer/README
Normal 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.
|
66
repos/os/src/server/record_play_mixer/audio_signal.h
Normal file
66
repos/os/src/server/record_play_mixer/audio_signal.h
Normal 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_ */
|
215
repos/os/src/server/record_play_mixer/main.cc
Normal file
215
repos/os/src/server/record_play_mixer/main.cc
Normal 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); }
|
65
repos/os/src/server/record_play_mixer/median.h
Normal file
65
repos/os/src/server/record_play_mixer/median.h
Normal 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_ */
|
||||
|
237
repos/os/src/server/record_play_mixer/mix_signal.h
Normal file
237
repos/os/src/server/record_play_mixer/mix_signal.h
Normal 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_ */
|
477
repos/os/src/server/record_play_mixer/play_session.h
Normal file
477
repos/os/src/server/record_play_mixer/play_session.h
Normal 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_ */
|
265
repos/os/src/server/record_play_mixer/record_session.h
Normal file
265
repos/os/src/server/record_play_mixer/record_session.h
Normal 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_ */
|
4
repos/os/src/server/record_play_mixer/target.mk
Normal file
4
repos/os/src/server/record_play_mixer/target.mk
Normal file
@ -0,0 +1,4 @@
|
||||
TARGET = record_play_mixer
|
||||
SRC_CC = main.cc
|
||||
LIBS = base
|
||||
INC_DIR += $(PRG_DIR)
|
255
repos/os/src/server/record_play_mixer/time_window_scheduler.h
Normal file
255
repos/os/src/server/record_play_mixer/time_window_scheduler.h
Normal 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_ */
|
214
repos/os/src/server/record_play_mixer/types.h
Normal file
214
repos/os/src/server/record_play_mixer/types.h
Normal 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_ */
|
290
repos/os/src/server/record_rom/main.cc
Normal file
290
repos/os/src/server/record_rom/main.cc
Normal 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);
|
||||
}
|
3
repos/os/src/server/record_rom/target.mk
Normal file
3
repos/os/src/server/record_rom/target.mk
Normal file
@ -0,0 +1,3 @@
|
||||
TARGET = record_rom
|
||||
SRC_CC = main.cc
|
||||
LIBS = base
|
97
repos/os/src/test/audio_play/main.cc
Normal file
97
repos/os/src/test/audio_play/main.cc
Normal 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); }
|
3
repos/os/src/test/audio_play/target.mk
Normal file
3
repos/os/src/test/audio_play/target.mk
Normal file
@ -0,0 +1,3 @@
|
||||
TARGET = test-audio_play
|
||||
SRC_CC = main.cc
|
||||
LIBS = base vfs
|
Loading…
Reference in New Issue
Block a user