summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.cpp35
-rw-r--r--src/model/audiodevice.cpp3
-rw-r--r--src/model/audiodevice.h38
-rw-r--r--src/model/pulsedevice.cpp101
-rw-r--r--src/model/pulsedevice.h40
-rw-r--r--src/model/pulsemodel.cpp271
-rw-r--r--src/model/pulsemodel.h58
-rw-r--r--src/ui/devicewidget.cpp69
-rw-r--r--src/ui/devicewidget.h34
-rw-r--r--src/ui/mixerwindow.cpp85
-rw-r--r--src/ui/mixerwindow.h43
11 files changed, 777 insertions, 0 deletions
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..6a8c6a8
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,35 @@
+#include <tdeapplication.h>
+#include <tdecmdlineargs.h>
+#include <tdeaboutdata.h>
+#include <tdelocale.h>
+
+#include "model/pulsemodel.h"
+#include "ui/mixerwindow.h"
+
+static TDECmdLineOptions options[] = { TDECmdLineLastOption };
+
+int main( int argc, char **argv )
+{
+ TDEAboutData about(
+ "tmix", I18N_NOOP("TMix"),
+ "0.1",
+ I18N_NOOP("Trinity audio mixer"),
+ TDEAboutData::License_GPL_V2,
+ "(C) 2024 Trinity Desktop Project"
+ );
+
+ TDECmdLineArgs::init( argc, argv, &about );
+ TDECmdLineArgs::addCmdLineOptions( options );
+ TDEApplication app;
+
+ PulseModel model;
+ if ( !model.open() ) {
+ // TODO: show error dialog
+ return 1;
+ }
+
+ MixerWindow win( &model );
+ win.show();
+
+ return app.exec();
+}
diff --git a/src/model/audiodevice.cpp b/src/model/audiodevice.cpp
new file mode 100644
index 0000000..6a1debb
--- /dev/null
+++ b/src/model/audiodevice.cpp
@@ -0,0 +1,3 @@
+#include "audiodevice.h"
+
+#include "audiodevice.moc"
diff --git a/src/model/audiodevice.h b/src/model/audiodevice.h
new file mode 100644
index 0000000..e753fc9
--- /dev/null
+++ b/src/model/audiodevice.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <tqobject.h>
+#include <tqstring.h>
+
+class TQWidget;
+
+class AudioDevice : public TQObject
+{
+ TQ_OBJECT
+
+public:
+ enum Category {
+ Output, // sinks, hardware output
+ Input, // sources, hardware input
+ Playback, // sink inputs (per-app playback streams)
+ Recording, // source outputs (per-app recording streams)
+ };
+
+ AudioDevice( TQObject *parent = 0 ) : TQObject(parent) {}
+ virtual ~AudioDevice() {}
+
+ virtual TQString name() const = 0;
+ virtual Category category() const = 0;
+
+ virtual int volume() const = 0; // 0-100
+ virtual bool muted() const = 0;
+ virtual void setVolume( int v ) = 0;
+ virtual void setMuted( bool m ) = 0;
+
+ // Creates the widget for this device. Caller takes ownership.
+ virtual TQWidget *createWidget( TQWidget *parent ) = 0;
+
+signals:
+ void volumeChanged( int v );
+ void muteChanged( bool m );
+ void nameChanged( const TQString &name );
+};
diff --git a/src/model/pulsedevice.cpp b/src/model/pulsedevice.cpp
new file mode 100644
index 0000000..e0f84ab
--- /dev/null
+++ b/src/model/pulsedevice.cpp
@@ -0,0 +1,101 @@
+#include "pulsedevice.h"
+#include "../ui/devicewidget.h"
+#include <tqwidget.h>
+
+PulseDevice::PulseDevice( AudioDevice::Category cat, uint32_t paIndex, TQObject *parent )
+ : AudioDevice(parent),
+ m_category(cat), m_paIndex(paIndex),
+ m_volume(0), m_muted(false),
+ m_context(0), m_mainloop(0)
+{
+}
+
+void PulseDevice::setPAContext( pa_context *ctx, pa_threaded_mainloop *mainloop )
+{
+ m_context = ctx;
+ m_mainloop = mainloop;
+}
+
+void PulseDevice::update( const TQString &name, int volume, bool muted )
+{
+ bool nameChange = ( name != m_name );
+ bool volumeChange = ( volume != m_volume );
+ bool muteChange = ( muted != m_muted );
+
+ m_name = name;
+ m_volume = volume;
+ m_muted = muted;
+
+ if ( nameChange ) emit nameChanged( m_name );
+ if ( volumeChange ) emit volumeChanged( m_volume );
+ if ( muteChange ) emit muteChanged( m_muted );
+}
+
+// ---- write back to PA -------------------------------------------------------
+
+static pa_volume_t percentToPA( int pct )
+{
+ if ( pct <= 0 ) return 0;
+ if ( pct >= 100 ) return PA_VOLUME_NORM;
+ return (pa_volume_t)( (double)pct / 100.0 * PA_VOLUME_NORM + 0.5 );
+}
+
+void PulseDevice::setVolume( int v )
+{
+ if ( !m_context ) return;
+ v = v < 0 ? 0 : v > 100 ? 100 : v;
+
+ pa_cvolume cv;
+ pa_cvolume_set( &cv, 2, percentToPA(v) );
+
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = 0;
+ switch ( m_category ) {
+ case Output:
+ op = pa_context_set_sink_volume_by_index( m_context, m_paIndex, &cv, 0, 0 );
+ break;
+ case Input:
+ op = pa_context_set_source_volume_by_index( m_context, m_paIndex, &cv, 0, 0 );
+ break;
+ case Playback:
+ op = pa_context_set_sink_input_volume( m_context, m_paIndex, &cv, 0, 0 );
+ break;
+ case Recording:
+ op = pa_context_set_source_output_volume( m_context, m_paIndex, &cv, 0, 0 );
+ break;
+ }
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+void PulseDevice::setMuted( bool m )
+{
+ if ( !m_context ) return;
+
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = 0;
+ int mute = m ? 1 : 0;
+ switch ( m_category ) {
+ case Output:
+ op = pa_context_set_sink_mute_by_index( m_context, m_paIndex, mute, 0, 0 );
+ break;
+ case Input:
+ op = pa_context_set_source_mute_by_index( m_context, m_paIndex, mute, 0, 0 );
+ break;
+ case Playback:
+ op = pa_context_set_sink_input_mute( m_context, m_paIndex, mute, 0, 0 );
+ break;
+ case Recording:
+ op = pa_context_set_source_output_mute( m_context, m_paIndex, mute, 0, 0 );
+ break;
+ }
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+TQWidget *PulseDevice::createWidget( TQWidget *parent )
+{
+ return new DeviceWidget( this, parent );
+}
+
+#include "pulsedevice.moc"
diff --git a/src/model/pulsedevice.h b/src/model/pulsedevice.h
new file mode 100644
index 0000000..fbdf985
--- /dev/null
+++ b/src/model/pulsedevice.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <pulse/pulseaudio.h>
+#include "audiodevice.h"
+
+class PulseDevice : public AudioDevice
+{
+ TQ_OBJECT
+
+public:
+ PulseDevice( AudioDevice::Category cat, uint32_t paIndex, TQObject *parent = 0 );
+ ~PulseDevice() {}
+
+ TQString name() const { return m_name; }
+ Category category() const { return m_category; }
+ int volume() const { return m_volume; }
+ bool muted() const { return m_muted; }
+ uint32_t paIndex() const { return m_paIndex; }
+
+ void setVolume( int v );
+ void setMuted( bool m );
+
+ // Called by PulseModel when PA reports an update — updates in place and emits signals.
+ void update( const TQString &name, int volume, bool muted );
+
+ TQWidget *createWidget( TQWidget *parent );
+
+ // PA context needed to write volume back — set once by PulseModel after construction.
+ void setPAContext( pa_context *ctx, pa_threaded_mainloop *mainloop );
+
+private:
+ AudioDevice::Category m_category;
+ uint32_t m_paIndex;
+ TQString m_name;
+ int m_volume; // 0-100
+ bool m_muted;
+
+ pa_context *m_context;
+ pa_threaded_mainloop *m_mainloop;
+};
diff --git a/src/model/pulsemodel.cpp b/src/model/pulsemodel.cpp
new file mode 100644
index 0000000..de745bc
--- /dev/null
+++ b/src/model/pulsemodel.cpp
@@ -0,0 +1,271 @@
+#include "pulsemodel.h"
+#include "pulsedevice.h"
+
+#include <tqapplication.h>
+
+// Custom event posted from the PA thread to the main thread.
+static const int PA_EVENT = TQEvent::User + 1;
+
+struct PAEvent : public TQCustomEvent {
+ enum Kind { DeviceAdded, DeviceRemoved, DeviceUpdated };
+ Kind kind;
+ AudioDevice::Category cat;
+ uint32_t paIndex;
+ TQString name;
+ int volume;
+ bool muted;
+
+ PAEvent( Kind k, AudioDevice::Category c, uint32_t idx,
+ const TQString &n = TQString(), int vol = 0, bool m = false )
+ : TQCustomEvent(PA_EVENT), kind(k), cat(c), paIndex(idx),
+ name(n), volume(vol), muted(m) {}
+};
+
+// ---- helpers ----------------------------------------------------------------
+
+static int paVolumeToPercent( const pa_cvolume &cv )
+{
+ pa_volume_t avg = pa_cvolume_avg( &cv );
+ return (int)( (double)avg / PA_VOLUME_NORM * 100.0 + 0.5 );
+}
+
+// ---- ctor / dtor ------------------------------------------------------------
+
+PulseModel::PulseModel( TQObject *parent )
+ : TQObject(parent), m_mainloop(0), m_context(0)
+{
+ m_sinks.setAutoDelete( true );
+ m_sources.setAutoDelete( true );
+ m_sinkInputs.setAutoDelete( true );
+ m_sourceOutputs.setAutoDelete( true );
+}
+
+PulseModel::~PulseModel()
+{
+ close();
+}
+
+// ---- connect / disconnect ---------------------------------------------------
+
+bool PulseModel::open()
+{
+ m_mainloop = pa_threaded_mainloop_new();
+ if ( !m_mainloop ) return false;
+
+ pa_mainloop_api *api = pa_threaded_mainloop_get_api( m_mainloop );
+ m_context = pa_context_new( api, "tmix" );
+ if ( !m_context ) {
+ pa_threaded_mainloop_free( m_mainloop );
+ m_mainloop = 0;
+ return false;
+ }
+
+ pa_context_set_state_callback( m_context, contextStateCb, this );
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_threaded_mainloop_start( m_mainloop );
+ pa_context_connect( m_context, 0, PA_CONTEXT_NOFLAGS, 0 );
+ pa_threaded_mainloop_unlock( m_mainloop );
+ return true;
+}
+
+void PulseModel::close()
+{
+ if ( m_context ) {
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_context_disconnect( m_context );
+ pa_context_unref( m_context );
+ m_context = 0;
+ pa_threaded_mainloop_unlock( m_mainloop );
+ }
+ if ( m_mainloop ) {
+ pa_threaded_mainloop_stop( m_mainloop );
+ pa_threaded_mainloop_free( m_mainloop );
+ m_mainloop = 0;
+ }
+ m_sinks.clear();
+ m_sources.clear();
+ m_sinkInputs.clear();
+ m_sourceOutputs.clear();
+}
+
+// ---- public query -----------------------------------------------------------
+
+TQPtrList<AudioDevice> PulseModel::devices( AudioDevice::Category cat ) const
+{
+ TQPtrList<AudioDevice> result;
+ const TQPtrList<PulseDevice> *src = 0;
+ switch ( cat ) {
+ case AudioDevice::Output: src = &m_sinks; break;
+ case AudioDevice::Input: src = &m_sources; break;
+ case AudioDevice::Playback: src = &m_sinkInputs; break;
+ case AudioDevice::Recording: src = &m_sourceOutputs; break;
+ }
+ if ( src )
+ for ( TQPtrListIterator<PulseDevice> it(*src); *it; ++it )
+ result.append( *it );
+ return result;
+}
+
+// ---- PA thread callbacks (called from PA thread — only post events) ---------
+
+void PulseModel::contextStateCb( pa_context *c, void *userdata )
+{
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ switch ( pa_context_get_state(c) ) {
+ case PA_CONTEXT_READY:
+ pa_context_set_subscribe_callback( c, subscribeCb, self );
+ pa_context_subscribe( c,
+ (pa_subscription_mask_t)(
+ PA_SUBSCRIPTION_MASK_SINK |
+ PA_SUBSCRIPTION_MASK_SOURCE |
+ PA_SUBSCRIPTION_MASK_SINK_INPUT |
+ PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT ),
+ 0, 0 );
+ self->enumerateAll();
+ break;
+ case PA_CONTEXT_FAILED:
+ case PA_CONTEXT_TERMINATED:
+ break;
+ default:
+ break;
+ }
+}
+
+void PulseModel::enumerateAll()
+{
+ // Called from PA thread (inside context state callback at READY).
+ pa_operation_unref( pa_context_get_sink_info_list( m_context, sinkInfoCb, this ) );
+ pa_operation_unref( pa_context_get_source_info_list( m_context, sourceInfoCb, this ) );
+ pa_operation_unref( pa_context_get_sink_input_info_list( m_context, sinkInputInfoCb, this ) );
+ pa_operation_unref( pa_context_get_source_output_info_list( m_context, sourceOutputInfoCb, this ) );
+}
+
+void PulseModel::sinkInfoCb( pa_context *, const pa_sink_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAEvent(
+ PAEvent::DeviceAdded, AudioDevice::Output, info->index,
+ TQString::fromUtf8( info->description ),
+ paVolumeToPercent( info->volume ), info->mute != 0 ) );
+}
+
+void PulseModel::sourceInfoCb( pa_context *, const pa_source_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ // Skip monitor sources — they're PA internals, not real inputs.
+ if ( info->monitor_of_sink != PA_INVALID_INDEX ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAEvent(
+ PAEvent::DeviceAdded, AudioDevice::Input, info->index,
+ TQString::fromUtf8( info->description ),
+ paVolumeToPercent( info->volume ), info->mute != 0 ) );
+}
+
+void PulseModel::sinkInputInfoCb( pa_context *, const pa_sink_input_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAEvent(
+ PAEvent::DeviceAdded, AudioDevice::Playback, info->index,
+ TQString::fromUtf8( info->name ),
+ paVolumeToPercent( info->volume ), info->mute != 0 ) );
+}
+
+void PulseModel::sourceOutputInfoCb( pa_context *, const pa_source_output_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAEvent(
+ PAEvent::DeviceAdded, AudioDevice::Recording, info->index,
+ TQString::fromUtf8( info->name ),
+ paVolumeToPercent( info->volume ), info->mute != 0 ) );
+}
+
+void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata )
+{
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+
+ pa_subscription_event_type_t facility = (pa_subscription_event_type_t)( t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK );
+ pa_subscription_event_type_t evtype = (pa_subscription_event_type_t)( t & PA_SUBSCRIPTION_EVENT_TYPE_MASK );
+
+ AudioDevice::Category cat;
+ switch ( facility ) {
+ case PA_SUBSCRIPTION_EVENT_SINK: cat = AudioDevice::Output; break;
+ case PA_SUBSCRIPTION_EVENT_SOURCE: cat = AudioDevice::Input; break;
+ case PA_SUBSCRIPTION_EVENT_SINK_INPUT: cat = AudioDevice::Playback; break;
+ case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: cat = AudioDevice::Recording; break;
+ default: return;
+ }
+
+ if ( evtype == PA_SUBSCRIPTION_EVENT_REMOVE ) {
+ TQApplication::postEvent( self, new PAEvent( PAEvent::DeviceRemoved, cat, idx ) );
+ return;
+ }
+
+ // NEW or CHANGE — re-query to get current state.
+ pa_operation *op = 0;
+ switch ( facility ) {
+ case PA_SUBSCRIPTION_EVENT_SINK:
+ op = pa_context_get_sink_info_by_index( c, idx, sinkInfoCb, self );
+ break;
+ case PA_SUBSCRIPTION_EVENT_SOURCE:
+ op = pa_context_get_source_info_by_index( c, idx, sourceInfoCb, self );
+ break;
+ case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
+ op = pa_context_get_sink_input_info( c, idx, sinkInputInfoCb, self );
+ break;
+ case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
+ op = pa_context_get_source_output_info( c, idx, sourceOutputInfoCb, self );
+ break;
+ default: break;
+ }
+ if ( op ) pa_operation_unref( op );
+}
+
+// ---- main thread event handler ----------------------------------------------
+
+void PulseModel::customEvent( TQCustomEvent *e )
+{
+ if ( e->type() != PA_EVENT ) return;
+ PAEvent *ev = static_cast<PAEvent *>(e);
+
+ TQPtrList<PulseDevice> *list = 0;
+ switch ( ev->cat ) {
+ case AudioDevice::Output: list = &m_sinks; break;
+ case AudioDevice::Input: list = &m_sources; break;
+ case AudioDevice::Playback: list = &m_sinkInputs; break;
+ case AudioDevice::Recording: list = &m_sourceOutputs; break;
+ }
+
+ if ( ev->kind == PAEvent::DeviceRemoved ) {
+ PulseDevice *dev = findDevice( *list, ev->paIndex );
+ if ( dev ) {
+ emit deviceRemoved( dev );
+ list->remove( dev ); // autoDelete=true, so dev is deleted here
+ }
+ return;
+ }
+
+ // DeviceAdded or DeviceUpdated (both come through the same info callbacks).
+ PulseDevice *dev = findDevice( *list, ev->paIndex );
+ if ( dev ) {
+ dev->update( ev->name, ev->volume, ev->muted );
+ } else {
+ dev = new PulseDevice( ev->cat, ev->paIndex, this );
+ dev->setPAContext( m_context, m_mainloop );
+ dev->update( ev->name, ev->volume, ev->muted );
+ list->append( dev );
+ emit deviceAdded( dev );
+ }
+}
+
+PulseDevice *PulseModel::findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex )
+{
+ for ( TQPtrListIterator<PulseDevice> it(list); *it; ++it )
+ if ( (*it)->paIndex() == paIndex )
+ return *it;
+ return 0;
+}
+
+#include "pulsemodel.moc"
diff --git a/src/model/pulsemodel.h b/src/model/pulsemodel.h
new file mode 100644
index 0000000..b96f2b1
--- /dev/null
+++ b/src/model/pulsemodel.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <tqobject.h>
+#include <tqptrlist.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "audiodevice.h"
+
+class PulseDevice;
+
+class PulseModel : public TQObject
+{
+ TQ_OBJECT
+
+public:
+ explicit PulseModel( TQObject *parent = 0 );
+ ~PulseModel();
+
+ bool open();
+ void close();
+
+ TQPtrList<AudioDevice> devices( AudioDevice::Category cat ) const;
+
+protected:
+ void customEvent( TQCustomEvent *e );
+
+signals:
+ void deviceAdded( AudioDevice *dev );
+ void deviceRemoved( AudioDevice *dev );
+ void ready(); // emitted once initial enumeration is complete
+
+private:
+ static void contextStateCb( pa_context *c, void *userdata );
+ static void sinkInfoCb( pa_context *c, const pa_sink_info *info, int eol, void *userdata );
+ static void sourceInfoCb( pa_context *c, const pa_source_info *info, int eol, void *userdata );
+ static void sinkInputInfoCb( pa_context *c, const pa_sink_input_info *info, int eol, void *userdata );
+ static void sourceOutputInfoCb( pa_context *c, const pa_source_output_info *info, int eol, void *userdata );
+ static void subscribeCb( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata );
+
+ void enumerateAll();
+ void addOrUpdateSink( const pa_sink_info *info );
+ void addOrUpdateSource( const pa_source_info *info );
+ void addOrUpdateSinkInput( const pa_sink_input_info *info );
+ void addOrUpdateSourceOutput( const pa_source_output_info *info );
+ void removeDevice( AudioDevice::Category cat, uint32_t paIndex );
+
+ pa_threaded_mainloop *m_mainloop;
+ pa_context *m_context;
+
+ // Keyed by PA index within each category — stable objects, updated in place.
+ TQPtrList<PulseDevice> m_sinks;
+ TQPtrList<PulseDevice> m_sources;
+ TQPtrList<PulseDevice> m_sinkInputs;
+ TQPtrList<PulseDevice> m_sourceOutputs;
+
+ PulseDevice *findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex );
+};
diff --git a/src/ui/devicewidget.cpp b/src/ui/devicewidget.cpp
new file mode 100644
index 0000000..420d5fd
--- /dev/null
+++ b/src/ui/devicewidget.cpp
@@ -0,0 +1,69 @@
+#include "devicewidget.h"
+#include "../model/audiodevice.h"
+
+#include <tqlabel.h>
+#include <tqlayout.h>
+#include <tqslider.h>
+#include <tqtoolbutton.h>
+
+DeviceWidget::DeviceWidget( AudioDevice *device, TQWidget *parent )
+ : TQWidget(parent), m_device(device)
+{
+ TQVBoxLayout *layout = new TQVBoxLayout( this, 4, 2 );
+
+ m_label = new TQLabel( device->name(), this );
+ m_label->setAlignment( TQLabel::AlignHCenter );
+
+ m_slider = new TQSlider( 0, 100, 5, device->volume(), TQt::Vertical, this );
+ m_slider->setTickmarks( TQSlider::NoMarks );
+
+ m_muteButton = new TQToolButton( this );
+ m_muteButton->setToggleButton( true );
+ m_muteButton->setTextLabel( "M" );
+ m_muteButton->setOn( device->muted() );
+
+ layout->addWidget( m_label );
+ layout->addWidget( m_slider, 1 );
+ layout->addWidget( m_muteButton, 0, TQt::AlignHCenter );
+
+ // slider → device
+ connect( m_slider, TQ_SIGNAL(valueChanged(int)), this, TQ_SLOT(onVolumeChanged(int)) );
+ connect( m_muteButton, TQ_SIGNAL(clicked()), this, TQ_SLOT(onMuteToggled()) );
+
+ // device → widget
+ connect( device, TQ_SIGNAL(volumeChanged(int)), this, TQ_SLOT(onDeviceVolume(int)) );
+ connect( device, TQ_SIGNAL(muteChanged(bool)), this, TQ_SLOT(onDeviceMute(bool)) );
+ connect( device, TQ_SIGNAL(nameChanged(const TQString&)), this, TQ_SLOT(onDeviceName(const TQString&)) );
+}
+
+void DeviceWidget::onVolumeChanged( int v )
+{
+ m_device->setVolume( v );
+}
+
+void DeviceWidget::onMuteToggled()
+{
+ m_device->setMuted( !m_device->muted() );
+}
+
+void DeviceWidget::onDeviceVolume( int v )
+{
+ // Block signal to avoid feedback loop back to device.
+ m_slider->blockSignals( true );
+ m_slider->setValue( v );
+ m_slider->blockSignals( false );
+}
+
+void DeviceWidget::onDeviceMute( bool m )
+{
+ m_muteButton->blockSignals( true );
+ m_muteButton->setOn( m );
+ m_muteButton->blockSignals( false );
+}
+
+void DeviceWidget::onDeviceName( const TQString &name )
+{
+ m_label->setText( name );
+}
+
+#include "devicewidget.moc"
diff --git a/src/ui/devicewidget.h b/src/ui/devicewidget.h
new file mode 100644
index 0000000..4a6cead
--- /dev/null
+++ b/src/ui/devicewidget.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <tqwidget.h>
+#include <tqstring.h>
+
+class AudioDevice;
+class TQSlider;
+class TQToolButton;
+class TQLabel;
+class TQVBoxLayout;
+
+class DeviceWidget : public TQWidget
+{
+ TQ_OBJECT
+
+public:
+ DeviceWidget( AudioDevice *device, TQWidget *parent = 0 );
+ ~DeviceWidget() {}
+
+ AudioDevice *device() const { return m_device; }
+
+private slots:
+ void onVolumeChanged( int v ); // slider → device
+ void onMuteToggled(); // button → device
+ void onDeviceVolume( int v ); // device → slider
+ void onDeviceMute( bool m ); // device → button
+ void onDeviceName( const TQString &name );
+
+private:
+ AudioDevice *m_device;
+ TQSlider *m_slider;
+ TQToolButton *m_muteButton;
+ TQLabel *m_label;
+};
diff --git a/src/ui/mixerwindow.cpp b/src/ui/mixerwindow.cpp
new file mode 100644
index 0000000..91cea65
--- /dev/null
+++ b/src/ui/mixerwindow.cpp
@@ -0,0 +1,85 @@
+#include "mixerwindow.h"
+#include "../model/pulsemodel.h"
+
+#include <tqlayout.h>
+#include <tqscrollview.h>
+#include <ktabwidget.h>
+#include <tdelocale.h>
+
+static MixerWindow::Tab makeTab( KTabWidget *tabs, const TQString &label )
+{
+ MixerWindow::Tab t;
+ TQScrollView *scroll = new TQScrollView( tabs );
+ scroll->setResizePolicy( TQScrollView::AutoOneFit );
+ scroll->setHScrollBarMode( TQScrollView::Auto );
+ scroll->setVScrollBarMode( TQScrollView::AlwaysOff );
+ scroll->setFrameStyle( TQFrame::NoFrame );
+
+ t.page = new TQWidget( scroll->viewport() );
+ scroll->addChild( t.page );
+ t.layout = new TQHBoxLayout( t.page, 6, 4 );
+
+ tabs->addTab( scroll, label );
+ return t;
+}
+
+MixerWindow::MixerWindow( PulseModel *model, TQWidget *parent )
+ : TQWidget(parent), m_model(model)
+{
+ setCaption( i18n("TMix") );
+
+ TQVBoxLayout *top = new TQVBoxLayout( this, 4, 0 );
+ m_tabs = new KTabWidget( this );
+ top->addWidget( m_tabs );
+
+ m_output = makeTab( m_tabs, i18n("Output") );
+ m_input = makeTab( m_tabs, i18n("Input") );
+ m_playback = makeTab( m_tabs, i18n("Playback") );
+ m_recording = makeTab( m_tabs, i18n("Recording") );
+
+ connect( model, TQ_SIGNAL(deviceAdded(AudioDevice*)), this, TQ_SLOT(onDeviceAdded(AudioDevice*)) );
+ connect( model, TQ_SIGNAL(deviceRemoved(AudioDevice*)), this, TQ_SLOT(onDeviceRemoved(AudioDevice*)) );
+
+ // Populate with any devices already known at construction time.
+ AudioDevice::Category cats[] = {
+ AudioDevice::Output, AudioDevice::Input,
+ AudioDevice::Playback, AudioDevice::Recording
+ };
+ for ( int i = 0; i < 4; i++ ) {
+ TQPtrList<AudioDevice> devs = model->devices( cats[i] );
+ for ( TQPtrListIterator<AudioDevice> it(devs); *it; ++it )
+ onDeviceAdded( *it );
+ }
+}
+
+MixerWindow::Tab &MixerWindow::tabForCategory( AudioDevice::Category cat )
+{
+ switch ( cat ) {
+ case AudioDevice::Output: return m_output;
+ case AudioDevice::Input: return m_input;
+ case AudioDevice::Playback: return m_playback;
+ case AudioDevice::Recording: return m_recording;
+ }
+ return m_output;
+}
+
+void MixerWindow::onDeviceAdded( AudioDevice *dev )
+{
+ Tab &t = tabForCategory( dev->category() );
+ TQWidget *w = dev->createWidget( t.page );
+ t.layout->addWidget( w );
+ w->show();
+ m_widgets.insert( dev, w );
+}
+
+void MixerWindow::onDeviceRemoved( AudioDevice *dev )
+{
+ TQWidget *w = m_widgets[dev];
+ if ( !w ) return;
+ Tab &t = tabForCategory( dev->category() );
+ t.layout->remove( w );
+ m_widgets.remove( dev );
+ delete w;
+}
+
+#include "mixerwindow.moc"
diff --git a/src/ui/mixerwindow.h b/src/ui/mixerwindow.h
new file mode 100644
index 0000000..f6b7be8
--- /dev/null
+++ b/src/ui/mixerwindow.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <tqwidget.h>
+#include <tqmap.h>
+
+#include "../model/audiodevice.h"
+
+class PulseModel;
+class KTabWidget;
+class TQHBoxLayout;
+
+class MixerWindow : public TQWidget
+{
+ TQ_OBJECT
+
+public:
+ explicit MixerWindow( PulseModel *model, TQWidget *parent = 0 );
+ ~MixerWindow() {}
+
+private slots:
+ void onDeviceAdded( AudioDevice *dev );
+ void onDeviceRemoved( AudioDevice *dev );
+
+public:
+ struct Tab {
+ TQWidget *page;
+ TQHBoxLayout *layout;
+ };
+
+private:
+
+ Tab &tabForCategory( AudioDevice::Category cat );
+
+ PulseModel *m_model;
+ KTabWidget *m_tabs;
+
+ Tab m_output;
+ Tab m_input;
+ Tab m_playback;
+ Tab m_recording;
+
+ TQMap<AudioDevice*, TQWidget*> m_widgets;
+};