diff --git a/applications/gui2/src/modules/camera.cpp b/applications/gui2/src/modules/camera.cpp
index a47d35f1a8fbfb28410add25b5b5140ecd8d28e9..c362d6a485fb508227ecb430a87d9c41953f000e 100644
--- a/applications/gui2/src/modules/camera.cpp
+++ b/applications/gui2/src/modules/camera.cpp
@@ -3,6 +3,7 @@
 
 #include "../views/camera3d.hpp"
 #include <ftl/rgbd/capabilities.hpp>
+#include <ftl/streams/renderer.hpp>
 #include <chrono>
 
 #include <opencv2/imgproc.hpp>
@@ -330,6 +331,18 @@ std::string Camera::getActiveSourceURI() {
 	return "";
 }
 
+ftl::audio::StereoMixerF<100> *Camera::mixer() {
+	if (mixer_) return mixer_;
+	if (movable_) {
+		auto *rend = io->feed()->getRenderer(frame_id_);
+		if (rend) {
+			mixer_ = &(rend->mixer());
+			return mixer_;
+		}
+	}
+	return nullptr;
+}
+
 bool Camera::isRecording() {
 	return io->feed()->isRecording();
 }
diff --git a/applications/gui2/src/modules/camera.hpp b/applications/gui2/src/modules/camera.hpp
index d4da36f3b391b484711960a332ffb06f0bcd4549..bdcb6dcaa2de1097d144b423f3178b10a52ac021 100644
--- a/applications/gui2/src/modules/camera.hpp
+++ b/applications/gui2/src/modules/camera.hpp
@@ -7,6 +7,7 @@
 #include <ftl/render/colouriser.hpp>
 #include <ftl/render/overlay.hpp>
 #include <ftl/codecs/touch.hpp>
+#include <ftl/audio/mixer.hpp>
 
 namespace ftl {
 namespace gui2 {
@@ -49,6 +50,7 @@ public:
 
 	ftl::render::Colouriser* colouriser() { return colouriser_.get(); };
 	ftl::overlay::Overlay* overlay() { return overlay_.get(); }
+	ftl::audio::StereoMixerF<100> *mixer();
 
 	void drawOverlay(NVGcontext *ctx);
 
@@ -90,6 +92,7 @@ private:
 	std::map<ftl::data::Message,std::string> messages_;
 
 	CameraView* view = nullptr;
+	ftl::audio::StereoMixerF<100> *mixer_ = nullptr;
 
 	MUTEX mtx_;
 
diff --git a/applications/gui2/src/views/camera.cpp b/applications/gui2/src/views/camera.cpp
index 565381ebc11f6512d6f12c8b0e9a1e92db4eec5c..76ff8efab5f7959b8e44003624fe585318f41f3a 100644
--- a/applications/gui2/src/views/camera.cpp
+++ b/applications/gui2/src/views/camera.cpp
@@ -204,7 +204,7 @@ MediaPanel::MediaPanel(nanogui::Widget *parent, ftl::gui2::Camera* ctrl) :
 	this->setTheme(theme);
 
 	// Volume control
-	button_volume = new ftl::gui2::VolumeButton(this);
+	button_volume = new ftl::gui2::VolumeButton(this, ctrl_->mixer());
 	button_volume->setValue(ctrl_->volume());
 	button_volume->setCallback([ctrl = ctrl_](float v){ ctrl->setVolume(v); });
 
diff --git a/applications/gui2/src/widgets/soundctrl.cpp b/applications/gui2/src/widgets/soundctrl.cpp
index 87ca183a51d255da37ce1bdbe34ef601738f47b6..4a8d6a08d76569206ad34a8b5d5a618f19b9547d 100644
--- a/applications/gui2/src/widgets/soundctrl.cpp
+++ b/applications/gui2/src/widgets/soundctrl.cpp
@@ -9,8 +9,8 @@ using ftl::gui2::PopupButton;
 using ftl::gui2::VolumeButton;
 using ftl::gui2::Screen;
 
-VolumeButton::VolumeButton(nanogui::Widget *parent) :
-	ftl::gui2::PopupButton(parent, "", ENTYPO_ICON_SOUND) {
+VolumeButton::VolumeButton(nanogui::Widget *parent, ftl::audio::StereoMixerF<100> *mixer) :
+	ftl::gui2::PopupButton(parent, "", ENTYPO_ICON_SOUND), mixer_(mixer) {
 	setChevronIcon(-1);
 
 	muted_ = false;
@@ -27,6 +27,42 @@ VolumeButton::VolumeButton(nanogui::Widget *parent) :
 		setValue(value);
 		if (cb_) { cb_(value); }
 	});
+
+	if (mixer) {
+		auto *mixbut = new nanogui::Button(mPopup, "Mixer", ENTYPO_ICON_SOUND_MIX);
+		mPopup->setAnchorHeight(70);
+
+		auto *mixer_widget = new nanogui::Widget(mPopup);
+		mixer_widget->setLayout(new nanogui::GroupLayout(0, 6, 14, 0));
+		mixer_widget->setVisible(false);
+
+		// Add mixer slider for each track in mixer.
+		for (int t=0; t<mixer->tracks(); ++t) {
+			auto *label = new nanogui::Label(mixer_widget, mixer->name(t));
+			label->setFontSize(12);
+			auto *mixslider = new nanogui::Slider(mixer_widget);
+			mixslider->setHighlightColor(dynamic_cast<Screen*>(screen())->getColor("highlight1"));
+			mixslider->setHeight(20);
+			mixslider->setValue(mixer->gain(t));
+			mixslider->setHighlightedRange({0.0f, mixer->gain(t)});
+
+			mixslider->setCallback([this,t,mixslider](float value) {
+				mixslider->setValue(value);
+				mixslider->setHighlightedRange({0.0f, value});
+				mixer_->setGain(t, value);
+			});
+		}
+
+		mixbut->setCallback([this,mixer_widget]() {
+			mixer_widget->setVisible(!mixer_widget->visible());
+			if (mixer_widget->visible()) {
+				mPopup->setAnchorHeight(70+mixer_widget->childCount()*20);
+			} else {
+				mPopup->setAnchorHeight(70);
+			}
+			screen()->performLayout();
+		});
+	}
 }
 
 VolumeButton::~VolumeButton() {
diff --git a/applications/gui2/src/widgets/soundctrl.hpp b/applications/gui2/src/widgets/soundctrl.hpp
index 1db40ec5c6fb0bfc3feff3e52007b7612e44f254..5495edd5c985dd9923c85f4ba1b7264cbdebbb25 100644
--- a/applications/gui2/src/widgets/soundctrl.hpp
+++ b/applications/gui2/src/widgets/soundctrl.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <nanogui/entypo.h>
+#include <ftl/audio/mixer.hpp>
 
 #include "popupbutton.hpp"
 
@@ -9,7 +10,7 @@ namespace gui2 {
 
 class VolumeButton : public ftl::gui2::PopupButton {
 public:
-	VolumeButton(nanogui::Widget *parent);
+	VolumeButton(nanogui::Widget *parent, ftl::audio::StereoMixerF<100> *mixer);
 	virtual ~VolumeButton();
 
 	// callback, new value passed in argument
@@ -38,6 +39,8 @@ private:
 	nanogui::Slider* slider_;
 	std::function<void(float)> cb_;
 
+	ftl::audio::StereoMixerF<100> *mixer_;
+
 	float scroll_step_ = 0.02f;
 	float value_;
 	bool muted_;
diff --git a/components/audio/src/speaker.cpp b/components/audio/src/speaker.cpp
index 89f8654540942954efa11b0823d2ae3db93ef69e..7db2e84263aed389b12b643b42f73987cc0e04ac 100644
--- a/components/audio/src/speaker.cpp
+++ b/components/audio/src/speaker.cpp
@@ -155,7 +155,7 @@ void Speaker::setDelay(int64_t ms) {
 void Speaker::setVolume(float value) {
 	// TODO: adjust volume using system mixer
 	volume_ = std::max(0.0f, std::min(1.0f, value));
-	buffer_->setGain(volume_);
+	if (buffer_) buffer_->setGain(volume_);
 }
 
 float Speaker::volume() {