diff --git a/repos/os/run/mixer.run b/repos/os/run/mixer.run
index ac0c18e61c..4de5ba9e31 100644
--- a/repos/os/run/mixer.run
+++ b/repos/os/run/mixer.run
@@ -8,6 +8,8 @@ set build_components {
drivers/timer
drivers/audio
server/mixer
+ server/dynamic_rom
+ server/report_rom
test/audio_out
}
@@ -45,42 +47,128 @@ set config {
append_platform_drv_config
append config {
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
+
- sample.raw
- vogel.f32
+ client1.f32
-
-}
+
+
+
+
+
+ client2.f32
+
+
+
+
+
+
+ }
install_config $config
+if {[expr ![file exists bin/client1.f32] || ![file exists bin/client2.f32]]} {
+ puts ""
+ puts "The sample files are missing. Please take a look at repos/dde_bsd/README"
+ puts "and create 'client1.f32' and 'client2.f32'. Afterwards put them into './bin'."
+ puts ""
+ exit 1
+}
+
#
# Boot modules
@@ -88,13 +176,10 @@ install_config $config
# generic modules
set boot_modules {
- core init
- timer
- audio_drv
- test-audio_out
- sample.raw
- vogel.f32
- mixer
+ core init timer
+ report_rom dynamic_rom
+ audio_drv test-audio_out
+ mixer client1.f32 client2.f32
}
# platform-specific components
diff --git a/repos/os/src/server/mixer/README b/repos/os/src/server/mixer/README
new file mode 100644
index 0000000000..5da5ed0247
--- /dev/null
+++ b/repos/os/src/server/mixer/README
@@ -0,0 +1,63 @@
+The mixer component implements a simple Audio_out session mixer. Input
+packets from multiple sources are mixed together into one output packet.
+
+
+The mixer can be tested by executing the 'repos/os/run/mixer.run' run
+script.
+
+
+Configuration
+=============
+
+The mixer gets its configuration via a ROM module called 'mixer.config'.
+The following configuration snippet illustrates its structure:
+
+!
+!
+!
+!
+!
+!
+!
+!
+!
+
+The '' node is used to set up the initial settings for new clients.
+According to this configuration every new client will start with a volume
+level set to 25 and is not muted. The initial output volume level is set
+to 75 (the volume level ranges from 0 to 100). The '' node
+contains all (pre-)configured channels. Each '' node has several
+mandatory attributes: 'type' specifies its type and is either 'input'
+or 'output', the 'label' attribute contains the label of a client for an input
+node and the label 'master' for an output node, 'number' specifies the channel
+number (0 for left and 1 for right), the 'volume' attribute sets the volume
+level and 'muted' marks the channel as muted. In addition, there are optional
+read-only channel attributes which are mainly used by the channel list report.
+
+
+Channel list report
+===================
+
+The mixer reports all available channels in its 'channel_list' report.
+The report contains a `' node that is similar to the one
+used in the 'mixer.config':
+
+!
+!
+!
+!
+!
+!
+!
+!
+
+Each channel node features all mandatory attributes as well as a few optional
+ones. The 'name' attribute contains the name of the channel. It is the
+alphanumeric description of the numeric 'number' attribute. The 'active'
+attribute indicates whether a channel is currently playing or not.
+
+A 'channel_list' report may by used to create a new configuration for the
+mixer. Every time the available channels change, e.g. when a new client
+appears, a new report is generated by the mixer. In return this report can
+then be used to configure the volume level of the new client. A new report
+is also generated after a new configuration has been applied by the mixer.
diff --git a/repos/os/src/server/mixer/mixer.cc b/repos/os/src/server/mixer/mixer.cc
index c4e0c7f962..af453e0535 100644
--- a/repos/os/src/server/mixer/mixer.cc
+++ b/repos/os/src/server/mixer/mixer.cc
@@ -4,9 +4,16 @@
* \author Josef Soentgen
* \date 2012-12-20
*
- * The mixer impelements the audio session on the server side. For each channel
+ * The mixer implements the audio session on the server side. For each channel
* (currently 'left' and 'right' only) it supports multiple client sessions and
- * mixes its input into a single client audio session.
+ * mixes all input sessions into a single client audio output session.
+ *
+ *
+ * There is a session list (Mixer::Session_channel) for each output channel that
+ * contains multiple input sessions (Audio_out::Session_elem). For every packet
+ * in the output queue the mixer sums the corresponding packets from all input
+ * sessions up. The volume level of an input packet is applied in a linear way
+ * (sample_value * volume_level) and the output packet is clipped at [1.0,-1.0].
*/
/*
@@ -17,49 +24,65 @@
*/
/* Genode includes */
+#include
+#include
+#include
#include
#include
#include
#include
+#include
#include
#include
#include
#include
-static bool const verbose = false;
+static bool verbose = false;
+#define PLOGV(...) do { if (verbose) PLOG(__VA_ARGS__); } while (0)
-enum Channel_number { INVALID = -1, LEFT, RIGHT, MAX_CHANNELS };
-static struct Names {
- char const *name;
- Channel_number number;
-} names[] = {
- { "left", LEFT }, { "front left", LEFT },
- { "right", RIGHT }, { "front right", RIGHT },
- { nullptr, MAX_CHANNELS }
+typedef Mixer::Channel Channel;
+
+
+enum {
+ LEFT = Channel::Number::LEFT,
+ RIGHT = Channel::Number::RIGHT,
+ MAX_CHANNELS = Channel::Number::MAX_CHANNELS,
+ MAX_VOLUME = Channel::Volume_level::MAX,
};
-static Channel_number channel_number_from_string(char const *name)
+static struct Names {
+ char const *name;
+ Channel::Number number;
+} names[] = {
+ { "left", (Channel::Number)LEFT },
+ { "front left", (Channel::Number)LEFT },
+ { "right", (Channel::Number)RIGHT },
+ { "front right", (Channel::Number)RIGHT },
+ { nullptr, (Channel::Number)MAX_CHANNELS }
+};
+
+
+static Channel::Number number_from_string(char const *name)
{
for (Names *n = names; n->name; ++n)
if (!Genode::strcmp(name, n->name))
return n->number;
-
- return INVALID;
+ return Channel::Number::INVALID;
}
-static char const *channel_string_from_number(Channel_number ch)
+static char const *string_from_number(Channel::Number ch)
{
for (Names *n = names; n->name; ++n)
if (ch == n->number)
return n->name;
-
return nullptr;
}
+
/**
* Helper method for walking over arrays
*/
@@ -68,28 +91,31 @@ static void for_each_index(int max_index, FUNC const &func) {
for (int i = 0; i < max_index; i++) func(i); }
-
namespace Audio_out
{
class Session_elem;
class Session_component;
class Root;
class Mixer;
+
+ enum { MAX_CHANNEL_NAME_LEN = 16, MAX_LABEL_LEN = 128 };
+ typedef Genode::String Label;
}
-enum { MAX_CHANNEL_NAME_LEN = 16, MAX_LABEL_LEN = 128 };
-
-
/**
- * Each session component is part of a list
+ * The actual session element
+ *
+ * It is part of the Audio_out::Session_component implementation but since
+ * it is also used by the mixer we defined it here.
*/
struct Audio_out::Session_elem : Audio_out::Session_rpc_object,
Genode::List::Element
{
- typedef Genode::String Label;
-
- Label label;
+ Label label;
+ Channel::Number number;
+ float volume { 0.f };
+ bool muted { true };
Session_elem(char const *label, Genode::Signal_context_capability data_cap)
: Session_rpc_object(data_cap), label(label) { }
@@ -106,11 +132,26 @@ class Audio_out::Mixer
{
private:
+ /*
+ * Signal handler
+ */
Genode::Signal_rpc_member _dispatcher;
+ Genode::Signal_rpc_member _dispatcher_config;
- Connection _left; /* left output */
- Connection _right; /* right output */
+ /*
+ * Mixer output Audio_out connection
+ */
+ Connection _left;
+ Connection _right;
Connection *_out[MAX_CHANNELS];
+ float _out_volume[MAX_CHANNELS];
+
+ /*
+ * Default settings used as fallback for new sessions
+ */
+ float _default_out_volume { 0.f };
+ float _default_volume { 0.f };
+ bool _default_muted { true };
/**
* Remix all exception
@@ -120,7 +161,7 @@ class Audio_out::Mixer
/**
* A channel contains multiple session components
*/
- struct Channel : public Genode::List
+ struct Session_channel : public Genode::List
{
void insert(Session_elem *session) { List::insert(session); }
void remove(Session_elem *session) { List::remove(session); }
@@ -135,20 +176,86 @@ class Audio_out::Mixer
/**
* Helper method for walking over session in a channel
*/
- template void _for_each_channel(FUNC const &func) {
- for (int i = LEFT; i < MAX_CHANNELS; i++) func(&_channels[i]); }
+ template void _for_each_channel(FUNC const &func)
+ {
+ for (int i = LEFT; i < MAX_CHANNELS; i++)
+ func((Channel::Number)i, &_channels[i]);
+ }
+ /*
+ * Channel reporter
+ *
+ * Each session in a channel is reported as an input node.
+ */
+ Genode::Reporter reporter { "channel_list" };
+
+ /**
+ * Report available channels
+ *
+ * This method is called if a new session is added or an old one
+ * removed as well as when the mixer configuration changes.
+ */
+ void _report_channels()
+ {
+ reporter.enabled(true);
+
+ try {
+ Genode::Reporter::Xml_generator xml(reporter, [&] () {
+ /* output channels */
+ for_each_index(MAX_CHANNELS, [&] (int const i) {
+ Channel::Number const num = (Channel::Number)i;
+ char const * const name = string_from_number(num);
+ int const vol = (int)(MAX_VOLUME * _out_volume[i]);
+
+ xml.node("channel", [&] () {
+ xml.attribute("type", "output");
+ xml.attribute("label", "master");
+ xml.attribute("name", name);
+ xml.attribute("number", (int)num);
+ xml.attribute("volume", (int)vol);
+ xml.attribute("muted", (long)0);
+ });
+ });
+
+ /* input channels */
+ _for_each_channel([&] (Channel::Number num, Session_channel *sc) {
+ sc->for_each_session([&] (Session_elem const &session) {
+ char const * const name = string_from_number(num);
+ int const vol = (int)(MAX_VOLUME * session.volume);
+
+ xml.node("channel", [&] () {
+ xml.attribute("type", "input");
+ xml.attribute("label", session.label.string());
+ xml.attribute("name", name);
+ xml.attribute("number", session.number);
+ xml.attribute("active", session.active());
+ xml.attribute("volume", (int)vol);
+ xml.attribute("muted", session.muted);
+ });
+ });
+ });
+ });
+ } catch (...) { PWRN("could report current channels"); }
+ }
+
+ /*
+ * Check if any of the available session is currently active, i.e.,
+ * playing.
+ */
bool _check_active()
{
bool active = false;
- _for_each_channel([&] (Channel *channel) {
- channel->for_each_session([&] (Session_elem const &session) {
+ _for_each_channel([&] (Channel::Number ch, Session_channel *sc) {
+ sc->for_each_session([&] (Session_elem const &session) {
active |= session.active();
});
});
return active;
}
+ /*
+ * Advance the stream of the session to a new position
+ */
void _advance_session(Session_elem *session, unsigned pos)
{
if (session->stopped()) return;
@@ -167,6 +274,9 @@ class Audio_out::Mixer
if (full) session->alloc_submit();
}
+ /*
+ * Advance the position of each session to match the output position
+ */
void _advance_position()
{
for_each_index(MAX_CHANNELS, [&] (int i) {
@@ -177,62 +287,80 @@ class Audio_out::Mixer
});
}
- void _mix_packet(Packet *out, Packet *in, bool clear)
+ /*
+ * Mix input packet into output packet
+ *
+ * Packets are mixed in a linear way with min/max clipping.
+ */
+ void _mix_packet(Packet *out, Packet *in, bool clear,
+ float const out_vol, float const vol)
{
if (clear) {
- out->content(in->content(), Audio_out::PERIOD);
- } else {
for_each_index(Audio_out::PERIOD, [&] (int const i) {
- out->content()[i] += in->content()[i];
+ out->content()[i] = (in->content()[i] * vol);
if (out->content()[i] > 1) out->content()[i] = 1;
if (out->content()[i] < -1) out->content()[i] = -1;
+
+ out->content()[i] *= out_vol;
+ });
+ } else {
+ for_each_index(Audio_out::PERIOD, [&] (int const i) {
+ out->content()[i] += (in->content()[i] * vol);
+
+ if (out->content()[i] > 1) out->content()[i] = 1;
+ if (out->content()[i] < -1) out->content()[i] = -1;
+
+ out->content()[i] *= out_vol;
});
}
- /*
- * Mark the packet as processed by invalidating it
- */
+ /* mark the packet as processed by invalidating it */
in->invalidate();
}
- bool _mix_channel(Channel_number nr, unsigned out_pos, unsigned offset)
+ /*
+ * Mix all session of one channel
+ */
+ bool _mix_channel(bool remix, Channel::Number nr, unsigned out_pos, unsigned offset)
{
- Stream * const stream = _out[nr]->stream();
- Packet * const out = stream->get(out_pos + offset);
- Channel * const channel = &_channels[nr];
+ Stream * const stream = _out[nr]->stream();
+ Packet * const out = stream->get(out_pos + offset);
+ Session_channel * const sc = &_channels[nr];
+
+ float const out_vol = _out_volume[nr];
bool clear = true;
- bool mix_all = false;
+ bool mix_all = remix;
bool const out_valid = out->valid();
Genode::retry(
- [&] () {
- channel->for_each_session([&] (Session_elem &session) {
- if (session.stopped()) return;
+ /*
+ * Mix the input packet at the given position of every input
+ * session to one output packet.
+ */
+ [&] {
+ sc->for_each_session([&] (Session_elem &session) {
+ if (session.stopped() || session.muted) return;
Packet *in = session.get_packet(offset);
- /*
- * When there already is an out packet, start over and mix
- * everything.
- */
+ /* remix again if input has changed for already mixed packet */
if (in->valid() && out_valid && !mix_all) throw Remix_all();
/* skip if packet has been processed or was already played */
if ((!in->valid() && !mix_all) || in->played()) return;
- _mix_packet(out, in, clear);
-
- if (verbose)
- PLOG("mix: ch %u in %u -> out %u all %d o: %u",
- nr, session.stream()->packet_position(in),
- stream->packet_position(out), mix_all, offset);
+ _mix_packet(out, in, clear, out_vol, session.volume);
clear = false;
});
},
- [&] () {
+ /*
+ * An input packet of an already mixed output packet has
+ * changed, we have to remix all input packets again.
+ */
+ [&] {
clear = true;
mix_all = true;
});
@@ -240,26 +368,33 @@ class Audio_out::Mixer
return !clear;
}
- void _mix()
+ /*
+ * Mix input packets
+ *
+ * \param remix force remix of already mixed packets
+ */
+ void _mix(bool remix = false)
{
unsigned pos[MAX_CHANNELS];
- pos[LEFT] = _out[LEFT]->stream()->pos();
+ pos[LEFT] = _out[LEFT]->stream()->pos();
pos[RIGHT] = _out[RIGHT]->stream()->pos();
/*
- * Look for packets that are valid, mix channels in an alternating
+ * Look for packets that are valid and mix channels in an alternating
* way.
*/
for_each_index(Audio_out::QUEUE_SIZE, [&] (int const i) {
bool mix_one = true;
for_each_index(MAX_CHANNELS, [&] (int const j) {
- mix_one = _mix_channel((Channel_number)j, pos[j], i);
+ mix_one = _mix_channel(remix, (Channel::Number)j, pos[j], i);
});
- if (mix_one) {
- _out[LEFT]->submit(_out[LEFT]->stream()->get(pos[LEFT] + i));
- _out[RIGHT]->submit(_out[RIGHT]->stream()->get(pos[RIGHT] + i));
- }
+ /* all channels mixed, submit to output queue */
+ if (mix_one)
+ for_each_index(MAX_CHANNELS, [&] (int const j) {
+ Packet *p = _out[j]->stream()->get(pos[j] + i);
+ _out[j]->submit(p);
+ });
});
}
@@ -273,81 +408,213 @@ class Audio_out::Mixer
_mix();
}
+ /**
+ * Set default values for various options
+ */
+ void _set_default_config(Genode::Xml_node const &node)
+ {
+ try {
+ Genode::Xml_node default_node = node.sub_node("default");
+ long v = 0;
+
+ v = default_node.attribute_value("out_volume", 0);
+ _default_out_volume = (float)v / MAX_VOLUME;
+
+ v = default_node.attribute_value("volume", 0);
+ _default_volume = (float)v / MAX_VOLUME;
+
+ v = default_node.attribute_value("muted", 1);
+ _default_muted = v ;
+ } catch (...) { PWRN("could not read mixer default values"); }
+ }
+
+ /**
+ * Handle ROM update signals
+ */
+ void _handle_config_update(unsigned sig_num)
+ {
+ using namespace Genode;
+
+ config()->reload();
+
+ try {
+ Xml_node config_node = config()->xml_node();
+ try { verbose = config_node.attribute("verbose").has_value("yes"); }
+ catch (...) { verbose = false; }
+
+ _set_default_config(config_node);
+
+ Xml_node channel_list_node = config_node.sub_node("channel_list");
+
+ channel_list_node.for_each_sub_node([&] (Xml_node const &node) {
+ Channel ch(node);
+
+ if (ch.type == Channel::Type::INPUT) {
+ _for_each_channel([&] (Channel::Number, Session_channel *sc) {
+ sc->for_each_session([&] (Session_elem &session) {
+ if (session.number != ch.number) return;
+ if (session.label != ch.label) return;
+
+ session.volume = (float)ch.volume / MAX_VOLUME;
+ session.muted = ch.muted;
+
+ PLOGV("label: '%s' nr: %d vol: %d muted: %d",
+ ch.label.string(), (int)ch.number,
+ (int)(MAX_VOLUME*ch.volume), ch.muted);
+ });
+ });
+ }
+ else if (ch.type == Channel::Type::OUTPUT) {
+ for_each_index(MAX_CHANNELS, [&] (int const i) {
+ if (ch.number != i) return;
+
+ _out_volume[i] = (float)ch.volume / MAX_VOLUME;
+
+ PLOGV("label: '%s' nr: %d vol: %d muted: %d",
+ "master", (int)ch.number,
+ (int)(MAX_VOLUME*ch.volume), ch.muted);
+ });
+ }
+ });
+ } catch (...) { PWRN("mixer config was invalid"); }
+
+ /*
+ * Report back any changes so a front-end can update its state
+ */
+ _report_channels();
+
+ /*
+ * The configuration has changed, remix already mixed packets
+ * in the mixer output queue.
+ */
+ _mix(true);
+ }
+
public:
+ /**
+ * Constructor
+ */
Mixer(Server::Entrypoint &ep)
:
_dispatcher(ep, *this, &Audio_out::Mixer::_handle),
+ _dispatcher_config(ep, *this, &Audio_out::Mixer::_handle_config_update),
_left("left", false, true),
_right("right", false, true)
{
_out[LEFT] = &_left;
_out[RIGHT] = &_right;
+
+ _out_volume[LEFT] = _default_out_volume;
+ _out_volume[RIGHT] = _default_out_volume;
+
+ Genode::config()->sigh(_dispatcher_config);
+ _handle_config_update(0);
+
+ _report_channels();
}
+ /**
+ * Start output stream
+ */
void start()
{
_out[LEFT]->progress_sigh(_dispatcher);
for_each_index(MAX_CHANNELS, [&] (int const i) { _out[i]->start(); });
}
+ /**
+ * Stop output stream
+ */
void stop()
{
for_each_index(MAX_CHANNELS, [&] (int const i) { _out[i]->stop(); });
_out[LEFT]->progress_sigh(Genode::Signal_context_capability());
}
- unsigned pos(Channel_number channel) const { return _out[channel]->stream()->pos(); }
+ /**
+ * Get current playback position of output stream
+ */
+ unsigned pos(Channel::Number channel) const { return _out[channel]->stream()->pos(); }
- void add_session(Channel_number ch, Session_elem &session)
+ /**
+ * Add input session
+ */
+ void add_session(Channel::Number ch, Session_elem &session)
{
PLOG("add label: \"%s\" channel: \"%s\" nr: %u",
- session.label.string(), channel_string_from_number(ch), ch);
+ session.label.string(), string_from_number(ch), ch);
+
+ session.volume = _default_volume;
+ session.muted = _default_muted;
+
_channels[ch].insert(&session);
+ _report_channels();
}
- void remove_session(Channel_number ch, Session_elem &session)
+ /**
+ * Remove input session
+ */
+ void remove_session(Channel::Number ch, Session_elem &session)
{
PLOG("remove label: \"%s\" channel: \"%s\" nr: %u",
- session.label.string(), channel_string_from_number(ch), ch);
+ session.label.string(), string_from_number(ch), ch);
+
_channels[ch].remove(&session);
+ _report_channels();
}
+ /**
+ * Get signal context that handles data avaiable as well as progress signal
+ */
Genode::Signal_context_capability sig_cap() { return _dispatcher; }
+
+ /**
+ * Report current channels
+ */
+ void report_channels() { _report_channels(); }
};
+/**************************************
+ ** Audio_out session implementation **
+ **************************************/
+
class Audio_out::Session_component : public Audio_out::Session_elem
{
private:
- Channel_number _channel;
- Mixer &_mixer;
+ Mixer &_mixer;
public:
- Session_component(char const *label,
- Channel_number channel,
- Mixer &mixer)
- : Session_elem(label, mixer.sig_cap()), _channel(channel), _mixer(mixer)
+ Session_component(char const *label,
+ Channel::Number number,
+ Mixer &mixer)
+ : Session_elem(label, mixer.sig_cap()), _mixer(mixer)
{
- _mixer.add_session(_channel, *this);
+ Session_elem::number = number;
+ _mixer.add_session(Session_elem::number, *this);
}
~Session_component()
{
if (Session_rpc_object::active()) stop();
- _mixer.remove_session(_channel, *this);
+ _mixer.remove_session(Session_elem::number, *this);
}
void start()
{
Session_rpc_object::start();
- /* sync audio position with mixer */
- stream()->pos(_mixer.pos(_channel));
+ stream()->pos(_mixer.pos(Session_elem::number));
+ _mixer.report_channels();
}
- void stop() { Session_rpc_object::stop(); }
+ void stop()
+ {
+ Session_rpc_object::stop();
+ _mixer.report_channels();
+ }
};
@@ -389,12 +656,12 @@ class Audio_out::Root : public Audio_out::Root_component
throw Root::Quota_exceeded();
}
- Channel_number ch = channel_number_from_string(channel_name);
- if (ch == INVALID)
+ Channel::Number ch = number_from_string(channel_name);
+ if (ch == Channel::Number::INVALID)
throw Root::Invalid_args();
Session_component *session = new (md_alloc())
- Session_component(label, (Channel_number)ch, _mixer);
+ Session_component(label, (Channel::Number)ch, _mixer);
if (++_sessions == 1) _mixer.start();
return session;
diff --git a/repos/os/src/server/mixer/target.mk b/repos/os/src/server/mixer/target.mk
index 73b2287cdb..59dc81e7e7 100644
--- a/repos/os/src/server/mixer/target.mk
+++ b/repos/os/src/server/mixer/target.mk
@@ -1,3 +1,3 @@
TARGET = mixer
SRC_CC = mixer.cc
-LIBS = base server
+LIBS = base config server