summaryrefslogtreecommitdiff
path: root/src/model
diff options
context:
space:
mode:
authorCalvin Morrison <calvin@pobox.com>2026-05-15 10:10:04 -0400
committerCalvin Morrison <calvin@pobox.com>2026-05-15 10:10:04 -0400
commite776bc768cf9afca1867200e25d64d315cd72a3e (patch)
tree6745527b939c9d37147d7dc98e8664437ee433f6 /src/model
parent4e602e78cdfc210ab7781668df2a88afb923258b (diff)
Full mixer implementation — tray, popup, prefs, devices tab with port indicators
Adds the complete tmix feature set built since the initial skeleton: balance knob, level meters, KLed mute button, system tray with scroll-wheel volume and recording indicator, tray popup, preferences dialog, right-click context menus, single-instance enforcement, scroll area, window geometry persistence, and Devices tab with PA card profile switcher and live port availability indicators (2-column layout, in-place updates on plug/unplug). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/model')
-rw-r--r--src/model/audiodevice.h7
-rw-r--r--src/model/pulsedevice.cpp227
-rw-r--r--src/model/pulsedevice.h42
-rw-r--r--src/model/pulsemodel.cpp451
-rw-r--r--src/model/pulsemodel.h66
5 files changed, 730 insertions, 63 deletions
diff --git a/src/model/audiodevice.h b/src/model/audiodevice.h
index e753fc9..854ba35 100644
--- a/src/model/audiodevice.h
+++ b/src/model/audiodevice.h
@@ -21,12 +21,16 @@ public:
virtual ~AudioDevice() {}
virtual TQString name() const = 0;
+ virtual TQString iconName() const = 0;
virtual Category category() const = 0;
virtual int volume() const = 0; // 0-100
virtual bool muted() const = 0;
+ virtual int pan() const = 0; // -50 (left) .. 0 .. +50 (right)
+
virtual void setVolume( int v ) = 0;
virtual void setMuted( bool m ) = 0;
+ virtual void setPan( int p ) = 0;
// Creates the widget for this device. Caller takes ownership.
virtual TQWidget *createWidget( TQWidget *parent ) = 0;
@@ -34,5 +38,8 @@ public:
signals:
void volumeChanged( int v );
void muteChanged( bool m );
+ void panChanged( int p );
+ void levelChanged( float level );
void nameChanged( const TQString &name );
+ void recordingActiveChanged( bool active );
};
diff --git a/src/model/pulsedevice.cpp b/src/model/pulsedevice.cpp
index e0f84ab..de778b9 100644
--- a/src/model/pulsedevice.cpp
+++ b/src/model/pulsedevice.cpp
@@ -1,37 +1,168 @@
#include "pulsedevice.h"
+#include "pulsemodel.h"
#include "../ui/devicewidget.h"
-#include <tqwidget.h>
+
+#include <tqapplication.h>
+#include <string.h>
+
+// ---- peak-level event (PA thread → main thread) -----------------------------
+
+static const int PA_LEVEL_EVENT = TQEvent::User + 2;
+
+struct PALevelEvent : public TQCustomEvent {
+ float level;
+ PALevelEvent( float l ) : TQCustomEvent(PA_LEVEL_EVENT), level(l) {}
+};
+
+// ---- ctor / dtor ------------------------------------------------------------
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)
+ m_category(cat), m_paIndex(paIndex), m_sinkIndex(PA_INVALID_INDEX),
+ m_volume(0), m_muted(false), m_pan(0), m_channels(2),
+ m_context(0), m_mainloop(0),
+ m_monitorStream(0), m_model(0), m_recordingCount(0)
+{
+}
+
+PulseDevice::~PulseDevice()
{
+ // detach() should have been called by PulseModel before destruction;
+ // this is a safety net for cases where it wasn't.
+ if ( m_monitorStream ) {
+ pa_stream_set_read_callback( m_monitorStream, 0, 0 );
+ pa_stream_unref( m_monitorStream );
+ m_monitorStream = 0;
+ }
}
+// ---- context wiring ---------------------------------------------------------
+
void PulseDevice::setPAContext( pa_context *ctx, pa_threaded_mainloop *mainloop )
{
m_context = ctx;
m_mainloop = mainloop;
+ startMonitoring();
+}
+
+void PulseDevice::detach()
+{
+ if ( m_monitorStream && m_mainloop ) {
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_stream_set_read_callback( m_monitorStream, 0, 0 );
+ pa_stream_disconnect( m_monitorStream );
+ pa_stream_unref( m_monitorStream );
+ m_monitorStream = 0;
+ pa_threaded_mainloop_unlock( m_mainloop );
+ }
+ m_context = 0;
+ m_mainloop = 0;
}
-void PulseDevice::update( const TQString &name, int volume, bool muted )
+// ---- update -----------------------------------------------------------------
+
+void PulseDevice::update( const TQString &name, int volume, bool muted,
+ uint8_t channels, int pan,
+ const TQString &monitorName, const TQString &iconName )
{
bool nameChange = ( name != m_name );
bool volumeChange = ( volume != m_volume );
bool muteChange = ( muted != m_muted );
+ bool panChange = ( pan != m_pan );
+
+ m_name = name;
+ m_iconName = iconName;
+ m_volume = volume;
+ m_muted = muted;
+ m_channels = channels ? channels : 2;
+ m_pan = pan;
- m_name = name;
- m_volume = volume;
- m_muted = muted;
+ if ( m_monitorName.isEmpty() && !monitorName.isEmpty() ) {
+ m_monitorName = monitorName;
+ if ( m_context ) startMonitoring();
+ }
if ( nameChange ) emit nameChanged( m_name );
if ( volumeChange ) emit volumeChanged( m_volume );
if ( muteChange ) emit muteChanged( m_muted );
+ if ( panChange ) emit panChanged( m_pan );
}
-// ---- write back to PA -------------------------------------------------------
+// ---- peak monitoring --------------------------------------------------------
+
+void PulseDevice::startMonitoring()
+{
+ if ( !m_context || !m_mainloop || m_monitorName.isEmpty() || m_monitorStream )
+ return;
+
+ pa_sample_spec spec;
+ spec.format = PA_SAMPLE_FLOAT32NE;
+ spec.rate = 25;
+ spec.channels = 1;
+
+ pa_threaded_mainloop_lock( m_mainloop );
+
+ pa_proplist *props = pa_proplist_new();
+ pa_proplist_sets( props, PA_PROP_MEDIA_ROLE, "abstract" );
+ m_monitorStream = pa_stream_new_with_proplist( m_context, "tmix-peak", &spec, 0, props );
+ pa_proplist_free( props );
+ if ( m_monitorStream ) {
+ pa_stream_set_read_callback( m_monitorStream, monitorReadCb, this );
+
+ // For sink inputs, filter peak detection to just this stream.
+ if ( m_category == Playback )
+ pa_stream_set_monitor_stream( m_monitorStream, m_paIndex );
+
+ pa_buffer_attr attr;
+ memset( &attr, 0xff, sizeof(attr) );
+ attr.fragsize = sizeof(float);
+
+ pa_stream_connect_record( m_monitorStream,
+ m_monitorName.utf8().data(), &attr,
+ (pa_stream_flags_t)(PA_STREAM_PEAK_DETECT |
+ PA_STREAM_ADJUST_LATENCY |
+ PA_STREAM_DONT_MOVE) );
+ }
+
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+void PulseDevice::monitorReadCb( pa_stream *s, size_t /*nbytes*/, void *userdata )
+{
+ PulseDevice *self = static_cast<PulseDevice*>(userdata);
+ const void *data = 0;
+ size_t len = 0;
+
+ if ( pa_stream_peek( s, &data, &len ) < 0 ) return;
+
+ float level = 0.0f;
+ if ( data && len >= sizeof(float) )
+ level = *static_cast<const float*>(data);
+ if ( level < 0.0f ) level = 0.0f;
+ if ( level > 1.0f ) level = 1.0f;
+
+ pa_stream_drop( s );
+
+ TQApplication::postEvent( self, new PALevelEvent(level) );
+}
+
+void PulseDevice::customEvent( TQCustomEvent *e )
+{
+ if ( e->type() == PA_LEVEL_EVENT )
+ emit levelChanged( static_cast<PALevelEvent*>(e)->level );
+}
+
+void PulseDevice::adjustRecordingCount( int delta )
+{
+ bool wasBefore = ( m_recordingCount > 0 );
+ m_recordingCount += delta;
+ if ( m_recordingCount < 0 ) m_recordingCount = 0;
+ bool isNow = ( m_recordingCount > 0 );
+ if ( isNow != wasBefore )
+ emit recordingActiveChanged( isNow );
+}
+
+// ---- helpers ----------------------------------------------------------------
static pa_volume_t percentToPA( int pct )
{
@@ -40,29 +171,60 @@ static pa_volume_t percentToPA( int pct )
return (pa_volume_t)( (double)pct / 100.0 * PA_VOLUME_NORM + 0.5 );
}
+static pa_cvolume buildCVolume( uint8_t channels, pa_volume_t vol, int pan )
+{
+ pa_cvolume cv;
+ cv.channels = channels;
+ if ( channels >= 2 ) {
+ double lf = ( pan >= 0 ) ? ( 50.0 - pan ) / 50.0 : 1.0;
+ double rf = ( pan <= 0 ) ? ( 50.0 + pan ) / 50.0 : 1.0;
+ cv.values[0] = (pa_volume_t)( vol * lf );
+ cv.values[1] = (pa_volume_t)( vol * rf );
+ for ( uint8_t i = 2; i < channels; i++ )
+ cv.values[i] = vol;
+ } else {
+ pa_cvolume_set( &cv, channels, vol );
+ }
+ return cv;
+}
+
+// ---- write back to PA -------------------------------------------------------
+
void PulseDevice::setVolume( int v )
{
if ( !m_context ) return;
v = v < 0 ? 0 : v > 100 ? 100 : v;
+ m_volume = v;
+ emit volumeChanged( v );
- pa_cvolume cv;
- pa_cvolume_set( &cv, 2, percentToPA(v) );
+ pa_cvolume cv = buildCVolume( m_channels, percentToPA(v), m_pan );
+ 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::setPan( int p )
+{
+ if ( !m_context ) return;
+ p = p < -50 ? -50 : p > 50 ? 50 : p;
+ m_pan = p;
+ emit panChanged( p );
+
+ pa_cvolume cv = buildCVolume( m_channels, percentToPA(m_volume), p );
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;
+ 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 );
@@ -71,23 +233,14 @@ void PulseDevice::setVolume( int v )
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;
+ 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 );
@@ -95,7 +248,7 @@ void PulseDevice::setMuted( bool m )
TQWidget *PulseDevice::createWidget( TQWidget *parent )
{
- return new DeviceWidget( this, parent );
+ return new DeviceWidget( this, m_model, parent );
}
#include "pulsedevice.moc"
diff --git a/src/model/pulsedevice.h b/src/model/pulsedevice.h
index fbdf985..e974508 100644
--- a/src/model/pulsedevice.h
+++ b/src/model/pulsedevice.h
@@ -3,38 +3,68 @@
#include <pulse/pulseaudio.h>
#include "audiodevice.h"
+class PulseModel;
+
class PulseDevice : public AudioDevice
{
TQ_OBJECT
public:
PulseDevice( AudioDevice::Category cat, uint32_t paIndex, TQObject *parent = 0 );
- ~PulseDevice() {}
+ ~PulseDevice();
TQString name() const { return m_name; }
+ TQString iconName() const { return m_iconName; }
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; }
+ int pan() const { return m_pan; }
+ uint32_t paIndex() const { return m_paIndex; }
+ uint32_t sinkIndex() const { return m_sinkIndex; }
+ TQString monitorName() const { return m_monitorName; }
+ TQString paName() const { return m_paName; }
+
+ void setPaName( const TQString &n ) { m_paName = n; }
+ void setSinkIndex( uint32_t idx ) { m_sinkIndex = idx; }
+ void setModel( PulseModel *m ) { m_model = m; }
+ void adjustRecordingCount( int delta );
void setVolume( int v );
void setMuted( bool m );
+ void setPan( int p );
- // Called by PulseModel when PA reports an update — updates in place and emits signals.
- void update( const TQString &name, int volume, bool muted );
+ void update( const TQString &name, int volume, bool muted,
+ uint8_t channels, int pan,
+ const TQString &monitorName, const TQString &iconName );
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 );
+ void detach(); // called by PulseModel before tearing down the mainloop
+
+protected:
+ void customEvent( TQCustomEvent *e );
private:
+ void startMonitoring();
+ static void monitorReadCb( pa_stream *s, size_t nbytes, void *userdata );
+
AudioDevice::Category m_category;
uint32_t m_paIndex;
+ uint32_t m_sinkIndex;
TQString m_name;
- int m_volume; // 0-100
+ TQString m_iconName;
+ int m_volume;
bool m_muted;
+ int m_pan;
+ uint8_t m_channels;
pa_context *m_context;
pa_threaded_mainloop *m_mainloop;
+
+ TQString m_monitorName;
+ pa_stream *m_monitorStream;
+ TQString m_paName;
+ PulseModel *m_model;
+ int m_recordingCount;
};
diff --git a/src/model/pulsemodel.cpp b/src/model/pulsemodel.cpp
index de745bc..a677edc 100644
--- a/src/model/pulsemodel.cpp
+++ b/src/model/pulsemodel.cpp
@@ -3,8 +3,17 @@
#include <tqapplication.h>
-// Custom event posted from the PA thread to the main thread.
-static const int PA_EVENT = TQEvent::User + 1;
+// Custom events posted from the PA thread to the main thread.
+static const int PA_EVENT = TQEvent::User + 1;
+static const int PA_SERVER_EVENT = TQEvent::User + 3;
+static const int PA_CARD_EVENT = TQEvent::User + 4;
+
+struct PAServerEvent : public TQCustomEvent {
+ TQString defaultSinkName;
+ TQString defaultSourceName;
+ PAServerEvent( const TQString &sink, const TQString &source )
+ : TQCustomEvent(PA_SERVER_EVENT), defaultSinkName(sink), defaultSourceName(source) {}
+};
struct PAEvent : public TQCustomEvent {
enum Kind { DeviceAdded, DeviceRemoved, DeviceUpdated };
@@ -14,19 +23,123 @@ struct PAEvent : public TQCustomEvent {
TQString name;
int volume;
bool muted;
+ uint8_t channels;
+ int pan;
+ TQString monitorName;
+ TQString iconName;
+ TQString paName; // raw PA name (sinks/sources only, for default tracking)
+ uint32_t parentIndex; // for Playback: parent sink PA index
PAEvent( Kind k, AudioDevice::Category c, uint32_t idx,
- const TQString &n = TQString(), int vol = 0, bool m = false )
+ const TQString &n = TQString(), int vol = 0, bool m = false,
+ uint8_t ch = 2, int p = 0, const TQString &mon = TQString(),
+ const TQString &icon = TQString(), uint32_t parent = PA_INVALID_INDEX,
+ const TQString &pname = TQString() )
: TQCustomEvent(PA_EVENT), kind(k), cat(c), paIndex(idx),
- name(n), volume(vol), muted(m) {}
+ name(n), volume(vol), muted(m), channels(ch), pan(p),
+ monitorName(mon), iconName(icon), paName(pname), parentIndex(parent) {}
+};
+
+struct PACardEvent : public TQCustomEvent {
+ struct Profile { TQString name; TQString description; bool available; };
+ struct Port {
+ TQString name; TQString description;
+ int available; int direction; uint32_t type; TQString availGroup;
+ };
+ bool removed;
+ uint32_t index;
+ TQString name;
+ TQString description;
+ TQString activeProfile;
+ TQString vendor;
+ TQString product;
+ TQString formFactor;
+ TQString busType;
+ TQValueList<Profile> profiles;
+ TQValueList<Port> ports;
+
+ explicit PACardEvent( uint32_t idx )
+ : TQCustomEvent(PA_CARD_EVENT), removed(true), index(idx) {}
+ PACardEvent( uint32_t idx, const TQString &n, const TQString &d, const TQString &ap )
+ : TQCustomEvent(PA_CARD_EVENT), removed(false), index(idx),
+ name(n), description(d), activeProfile(ap) {}
};
// ---- helpers ----------------------------------------------------------------
+static bool isGenericMediaName( const TQString &s )
+{
+ TQString l = s.lower().stripWhiteSpace();
+ return l == "audio stream" || l == "alsa playback" || l == "playback"
+ || l == "audio output" || l == "output" || l.isEmpty();
+}
+
+static TQString paCleanAppName( const char *raw )
+{
+ if ( !raw || !*raw ) return TQString();
+ TQString s = TQString::fromUtf8( raw );
+
+ // Strip "ALSA plug-in [Foo]" wrapper → "Foo"
+ int lb = s.find('['), rb = s.findRev(']');
+ if ( lb != -1 && rb > lb )
+ s = s.mid( lb + 1, rb - lb - 1 ).stripWhiteSpace();
+
+ // Strip trailing "app" (amarokapp → amarok), then capitalise first letter
+ if ( s.endsWith("app") && s.length() > 3 )
+ s = s.left( s.length() - 3 );
+ if ( !s.isEmpty() )
+ s[0] = s[0].upper();
+
+ return s;
+}
+
+static TQString paStreamName( pa_proplist *pl, const char *fallback )
+{
+ TQString appName = paCleanAppName( pa_proplist_gets(pl, PA_PROP_APPLICATION_NAME) );
+ const char *mn = pa_proplist_gets(pl, PA_PROP_MEDIA_NAME);
+ TQString mediaName = mn ? TQString::fromUtf8(mn).stripWhiteSpace() : TQString();
+
+ if ( !appName.isEmpty() && !isGenericMediaName(mediaName) && !mediaName.isEmpty() )
+ return appName + " - " + mediaName;
+ if ( !appName.isEmpty() )
+ return appName;
+ if ( !isGenericMediaName(mediaName) && !mediaName.isEmpty() )
+ return mediaName;
+ return fallback ? TQString::fromUtf8(fallback) : TQString();
+}
+
+static TQString paAppIcon( pa_proplist *pl )
+{
+ const char *s;
+ if ( (s = pa_proplist_gets(pl, PA_PROP_APPLICATION_ICON_NAME)) && *s )
+ return TQString::fromUtf8(s);
+ if ( (s = pa_proplist_gets(pl, "application.process.binary")) && *s ) {
+ TQString bin = TQString::fromUtf8(s).lower();
+ // Some TDE apps register binary as "fooapp" but icon as "foo"
+ if ( bin.endsWith("app") )
+ return bin.left( bin.length() - 3 );
+ return bin;
+ }
+ if ( (s = pa_proplist_gets(pl, PA_PROP_APPLICATION_NAME)) && *s )
+ return TQString::fromUtf8(s).lower();
+ return TQString();
+}
+
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 );
+ pa_volume_t mx = pa_cvolume_max( &cv );
+ return (int)( (double)mx / PA_VOLUME_NORM * 100.0 + 0.5 );
+}
+
+// Derive pan -50..+50 from the first two channels of a cvolume.
+static int cvolumeToPan( const pa_cvolume &cv )
+{
+ if ( cv.channels < 2 ) return 0;
+ double l = cv.values[0];
+ double r = cv.values[1];
+ double mx = ( l > r ) ? l : r;
+ if ( mx < 1.0 ) return 0;
+ return (int)( ( r - l ) / mx * 50.0 );
}
// ---- ctor / dtor ------------------------------------------------------------
@@ -70,6 +183,12 @@ bool PulseModel::open()
void PulseModel::close()
{
+ // Detach monitor streams on all devices before tearing down the mainloop.
+ TQPtrList<PulseDevice> *allLists[4] = { &m_sinks, &m_sources, &m_sinkInputs, &m_sourceOutputs };
+ for ( int i = 0; i < 4; i++ )
+ for ( TQPtrListIterator<PulseDevice> it(*allLists[i]); *it; ++it )
+ (*it)->detach();
+
if ( m_context ) {
pa_threaded_mainloop_lock( m_mainloop );
pa_context_disconnect( m_context );
@@ -119,8 +238,11 @@ void PulseModel::contextStateCb( pa_context *c, void *userdata )
PA_SUBSCRIPTION_MASK_SINK |
PA_SUBSCRIPTION_MASK_SOURCE |
PA_SUBSCRIPTION_MASK_SINK_INPUT |
- PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT ),
+ PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT |
+ PA_SUBSCRIPTION_MASK_SERVER |
+ PA_SUBSCRIPTION_MASK_CARD ),
0, 0 );
+ pa_operation_unref( pa_context_get_server_info( c, serverInfoCb, self ) );
self->enumerateAll();
break;
case PA_CONTEXT_FAILED:
@@ -138,16 +260,32 @@ void PulseModel::enumerateAll()
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 ) );
+ pa_operation_unref( pa_context_get_card_info_list( m_context, cardInfoCb, this ) );
+}
+
+void PulseModel::serverInfoCb( pa_context *, const pa_server_info *info, void *userdata )
+{
+ if ( !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAServerEvent(
+ TQString::fromUtf8( info->default_sink_name ? info->default_sink_name : "" ),
+ TQString::fromUtf8( info->default_source_name ? info->default_source_name : "" ) ) );
}
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 *ev = new PAEvent(
PAEvent::DeviceAdded, AudioDevice::Output, info->index,
TQString::fromUtf8( info->description ),
- paVolumeToPercent( info->volume ), info->mute != 0 ) );
+ paVolumeToPercent( info->volume ), info->mute != 0,
+ info->volume.channels, cvolumeToPan( info->volume ),
+ TQString::fromUtf8( info->monitor_source_name ),
+ TQString::fromUtf8( pa_proplist_gets( info->proplist, PA_PROP_DEVICE_ICON_NAME ) ? : "" ),
+ PA_INVALID_INDEX,
+ TQString::fromUtf8( info->name ) );
+ TQApplication::postEvent( self, ev );
}
void PulseModel::sourceInfoCb( pa_context *, const pa_source_info *info, int eol, void *userdata )
@@ -159,7 +297,12 @@ void PulseModel::sourceInfoCb( pa_context *, const pa_source_info *info, int eol
TQApplication::postEvent( self, new PAEvent(
PAEvent::DeviceAdded, AudioDevice::Input, info->index,
TQString::fromUtf8( info->description ),
- paVolumeToPercent( info->volume ), info->mute != 0 ) );
+ paVolumeToPercent( info->volume ), info->mute != 0,
+ info->volume.channels, cvolumeToPan( info->volume ),
+ TQString::fromUtf8( info->name ),
+ TQString::fromUtf8( pa_proplist_gets( info->proplist, PA_PROP_DEVICE_ICON_NAME ) ? : "" ),
+ PA_INVALID_INDEX,
+ TQString::fromUtf8( info->name ) ) );
}
void PulseModel::sinkInputInfoCb( pa_context *, const pa_sink_input_info *info, int eol, void *userdata )
@@ -168,18 +311,27 @@ void PulseModel::sinkInputInfoCb( pa_context *, const pa_sink_input_info *info,
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 ) );
+ paStreamName( info->proplist, info->name ),
+ paVolumeToPercent( info->volume ), info->mute != 0,
+ info->volume.channels, cvolumeToPan( info->volume ),
+ TQString(), // monitor name filled in by customEvent from parent sink
+ paAppIcon( info->proplist ),
+ info->sink ) ); // parent sink index for peak monitoring
}
void PulseModel::sourceOutputInfoCb( pa_context *, const pa_source_output_info *info, int eol, void *userdata )
{
if ( eol || !info ) return;
+ // Filter out our own peak-monitor streams.
+ if ( info->name && strcmp(info->name, "tmix-peak") == 0 ) 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 ) );
+ paStreamName( info->proplist, info->name ),
+ paVolumeToPercent( info->volume ), info->mute != 0,
+ info->volume.channels, cvolumeToPan( info->volume ),
+ TQString(), paAppIcon( info->proplist ),
+ info->source ) ); // parentIndex = source being recorded from
}
void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata )
@@ -189,6 +341,21 @@ void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uin
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 );
+ if ( facility == PA_SUBSCRIPTION_EVENT_SERVER ) {
+ pa_operation_unref( pa_context_get_server_info( c, serverInfoCb, self ) );
+ return;
+ }
+
+ if ( facility == PA_SUBSCRIPTION_EVENT_CARD ) {
+ if ( evtype == PA_SUBSCRIPTION_EVENT_REMOVE ) {
+ TQApplication::postEvent( self, new PACardEvent( idx ) );
+ } else {
+ pa_operation *op = pa_context_get_card_info_by_index( c, idx, cardInfoCb, self );
+ if ( op ) pa_operation_unref( op );
+ }
+ return;
+ }
+
AudioDevice::Category cat;
switch ( facility ) {
case PA_SUBSCRIPTION_EVENT_SINK: cat = AudioDevice::Output; break;
@@ -223,10 +390,134 @@ void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uin
if ( op ) pa_operation_unref( op );
}
+void PulseModel::cardInfoCb( pa_context *, const pa_card_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+
+ const char *desc = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_DESCRIPTION );
+ const char *vendor = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_VENDOR_NAME );
+ const char *product = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_PRODUCT_NAME );
+ const char *ff = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_FORM_FACTOR );
+ const char *bus = pa_proplist_gets( info->proplist, "device.bus" );
+
+ PACardEvent *ev = new PACardEvent(
+ info->index,
+ TQString::fromUtf8( info->name ),
+ TQString::fromUtf8( desc ? desc : info->name ),
+ info->active_profile2 ? TQString::fromUtf8( info->active_profile2->name ) : TQString() );
+
+ ev->vendor = TQString::fromUtf8( vendor ? vendor : "" );
+ ev->product = TQString::fromUtf8( product ? product : "" );
+ ev->formFactor = TQString::fromUtf8( ff ? ff : "" );
+ ev->busType = TQString::fromUtf8( bus ? bus : "" );
+
+ for ( uint32_t i = 0; info->profiles2 && info->profiles2[i]; ++i ) {
+ PACardEvent::Profile p;
+ p.name = TQString::fromUtf8( info->profiles2[i]->name );
+ p.description = TQString::fromUtf8( info->profiles2[i]->description );
+ p.available = info->profiles2[i]->available != 0;
+ ev->profiles.append( p );
+ }
+
+ for ( uint32_t i = 0; i < info->n_ports; ++i ) {
+ PACardEvent::Port port;
+ port.name = TQString::fromUtf8( info->ports[i]->name );
+ port.description= TQString::fromUtf8( info->ports[i]->description );
+ port.available = info->ports[i]->available;
+ port.direction = info->ports[i]->direction;
+ port.type = info->ports[i]->type;
+ if ( info->ports[i]->availability_group )
+ port.availGroup = TQString::fromUtf8( info->ports[i]->availability_group );
+ ev->ports.append( port );
+ }
+
+ TQApplication::postEvent( self, ev );
+}
+
// ---- main thread event handler ----------------------------------------------
void PulseModel::customEvent( TQCustomEvent *e )
{
+ if ( e->type() == PA_CARD_EVENT ) {
+ PACardEvent *ev = static_cast<PACardEvent *>(e);
+ if ( ev->removed ) {
+ for ( TQValueList<PulseCardInfo>::Iterator it = m_cards.begin();
+ it != m_cards.end(); ++it ) {
+ if ( it->index == ev->index ) {
+ m_cards.remove( it );
+ emit cardRemoved( ev->index );
+ return;
+ }
+ }
+ return;
+ }
+ PulseCardInfo *existing = findCard( ev->index );
+ if ( existing ) {
+ existing->name = ev->name;
+ existing->description = ev->description;
+ existing->activeProfile = ev->activeProfile;
+ existing->vendor = ev->vendor;
+ existing->product = ev->product;
+ existing->formFactor = ev->formFactor;
+ existing->busType = ev->busType;
+ existing->profiles.clear();
+ existing->ports.clear();
+ for ( TQValueList<PACardEvent::Profile>::Iterator it = ev->profiles.begin();
+ it != ev->profiles.end(); ++it ) {
+ PulseCardProfile p; p.name = it->name; p.description = it->description; p.available = it->available;
+ existing->profiles.append( p );
+ }
+ for ( TQValueList<PACardEvent::Port>::Iterator it = ev->ports.begin();
+ it != ev->ports.end(); ++it ) {
+ PulseCardPort port;
+ port.name = it->name; port.description = it->description;
+ port.available = it->available; port.direction = it->direction;
+ port.type = it->type; port.availabilityGroup = it->availGroup;
+ existing->ports.append( port );
+ }
+ emit cardUpdated( ev->index );
+ } else {
+ PulseCardInfo info;
+ info.index = ev->index;
+ info.name = ev->name;
+ info.description = ev->description;
+ info.activeProfile = ev->activeProfile;
+ info.vendor = ev->vendor;
+ info.product = ev->product;
+ info.formFactor = ev->formFactor;
+ info.busType = ev->busType;
+ for ( TQValueList<PACardEvent::Profile>::Iterator it = ev->profiles.begin();
+ it != ev->profiles.end(); ++it ) {
+ PulseCardProfile p; p.name = it->name; p.description = it->description; p.available = it->available;
+ info.profiles.append( p );
+ }
+ for ( TQValueList<PACardEvent::Port>::Iterator it = ev->ports.begin();
+ it != ev->ports.end(); ++it ) {
+ PulseCardPort port;
+ port.name = it->name; port.description = it->description;
+ port.available = it->available; port.direction = it->direction;
+ port.type = it->type; port.availabilityGroup = it->availGroup;
+ info.ports.append( port );
+ }
+ m_cards.append( info );
+ emit cardAdded( ev->index );
+ }
+ return;
+ }
+
+ if ( e->type() == PA_SERVER_EVENT ) {
+ PAServerEvent *ev = static_cast<PAServerEvent *>(e);
+ if ( ev->defaultSinkName != m_defaultSinkName ) {
+ m_defaultSinkName = ev->defaultSinkName;
+ emit defaultOutputChanged( defaultOutput() );
+ }
+ if ( ev->defaultSourceName != m_defaultSourceName ) {
+ m_defaultSourceName = ev->defaultSourceName;
+ emit defaultInputChanged( defaultInput() );
+ }
+ return;
+ }
if ( e->type() != PA_EVENT ) return;
PAEvent *ev = static_cast<PAEvent *>(e);
@@ -241,25 +532,89 @@ void PulseModel::customEvent( TQCustomEvent *e )
if ( ev->kind == PAEvent::DeviceRemoved ) {
PulseDevice *dev = findDevice( *list, ev->paIndex );
if ( dev ) {
+ // Remove from name maps if present.
+ TQMap<TQString,PulseDevice*>::Iterator it;
+ for ( it = m_sinksByName.begin(); it != m_sinksByName.end(); ++it ) {
+ if ( it.data() == dev ) { m_sinksByName.remove(it); break; }
+ }
+ for ( it = m_sourcesByName.begin(); it != m_sourcesByName.end(); ++it ) {
+ if ( it.data() == dev ) { m_sourcesByName.remove(it); break; }
+ }
+ // If a recording stream ended, decrement its parent source's count.
+ if ( ev->cat == AudioDevice::Recording ) {
+ TQMap<uint32_t,uint32_t>::Iterator si = m_sourceOutputToSource.find( ev->paIndex );
+ if ( si != m_sourceOutputToSource.end() ) {
+ PulseDevice *src = findDevice( m_sources, si.data() );
+ if ( src ) src->adjustRecordingCount( -1 );
+ m_sourceOutputToSource.remove( si );
+ }
+ }
emit deviceRemoved( dev );
- list->remove( dev ); // autoDelete=true, so dev is deleted here
+ list->remove( dev );
}
return;
}
// DeviceAdded or DeviceUpdated (both come through the same info callbacks).
+ // For Playback streams, resolve the parent sink's monitor source name so the
+ // level meter can attach a peak-detect stream to just this sink input.
+ TQString monitorName = ev->monitorName;
+ if ( ev->cat == AudioDevice::Playback && monitorName.isEmpty()
+ && ev->parentIndex != PA_INVALID_INDEX ) {
+ PulseDevice *sink = findDevice( m_sinks, ev->parentIndex );
+ if ( sink ) monitorName = sink->monitorName();
+ }
+
PulseDevice *dev = findDevice( *list, ev->paIndex );
if ( dev ) {
- dev->update( ev->name, ev->volume, ev->muted );
+ dev->update( ev->name, ev->volume, ev->muted, ev->channels, ev->pan, monitorName, ev->iconName );
+ if ( ev->cat == AudioDevice::Playback && ev->parentIndex != PA_INVALID_INDEX )
+ dev->setSinkIndex( ev->parentIndex );
} else {
dev = new PulseDevice( ev->cat, ev->paIndex, this );
+ dev->setModel( this );
dev->setPAContext( m_context, m_mainloop );
- dev->update( ev->name, ev->volume, ev->muted );
+ dev->update( ev->name, ev->volume, ev->muted, ev->channels, ev->pan, monitorName, ev->iconName );
+ if ( ev->cat == AudioDevice::Playback && ev->parentIndex != PA_INVALID_INDEX )
+ dev->setSinkIndex( ev->parentIndex );
+ if ( !ev->paName.isEmpty() ) {
+ dev->setPaName( ev->paName );
+ }
list->append( dev );
+ if ( !ev->paName.isEmpty() ) {
+ if ( ev->cat == AudioDevice::Output )
+ m_sinksByName.insert( ev->paName, dev );
+ else if ( ev->cat == AudioDevice::Input )
+ m_sourcesByName.insert( ev->paName, dev );
+ }
emit deviceAdded( dev );
+ if ( ev->cat == AudioDevice::Output && ev->paName == m_defaultSinkName )
+ emit defaultOutputChanged( dev );
+ if ( ev->cat == AudioDevice::Input && ev->paName == m_defaultSourceName )
+ emit defaultInputChanged( dev );
+ // Track which source a new recording stream is attached to.
+ if ( ev->cat == AudioDevice::Recording && ev->parentIndex != PA_INVALID_INDEX ) {
+ m_sourceOutputToSource.insert( ev->paIndex, ev->parentIndex );
+ PulseDevice *src = findDevice( m_sources, ev->parentIndex );
+ if ( src ) src->adjustRecordingCount( +1 );
+ }
}
}
+AudioDevice *PulseModel::defaultOutput() const
+{
+ if ( m_sinksByName.contains(m_defaultSinkName) )
+ return m_sinksByName[m_defaultSinkName];
+ return 0;
+}
+
+AudioDevice *PulseModel::defaultInput() const
+{
+ if ( m_sourcesByName.contains(m_defaultSourceName) )
+ return m_sourcesByName[m_defaultSourceName];
+ return 0;
+}
+
PulseDevice *PulseModel::findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex )
{
for ( TQPtrListIterator<PulseDevice> it(list); *it; ++it )
@@ -268,4 +623,66 @@ PulseDevice *PulseModel::findDevice( TQPtrList<PulseDevice> &list, uint32_t paIn
return 0;
}
+// ---- operations called from UI (main thread) ---------------------------------
+
+void PulseModel::setDefaultOutput( AudioDevice *dev )
+{
+ PulseDevice *pd = dynamic_cast<PulseDevice*>(dev);
+ if ( !pd || pd->paName().isEmpty() || !m_context || !m_mainloop ) return;
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = pa_context_set_default_sink( m_context, pd->paName().utf8().data(), 0, 0 );
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+void PulseModel::setDefaultInput( AudioDevice *dev )
+{
+ PulseDevice *pd = dynamic_cast<PulseDevice*>(dev);
+ if ( !pd || pd->paName().isEmpty() || !m_context || !m_mainloop ) return;
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = pa_context_set_default_source( m_context, pd->paName().utf8().data(), 0, 0 );
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+void PulseModel::moveSinkInputToSink( AudioDevice *playbackDev, AudioDevice *outputDev )
+{
+ PulseDevice *si = dynamic_cast<PulseDevice*>(playbackDev);
+ PulseDevice *sink = dynamic_cast<PulseDevice*>(outputDev);
+ if ( !si || !sink || !m_context || !m_mainloop ) return;
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = pa_context_move_sink_input_by_index(
+ m_context, si->paIndex(), sink->paIndex(), 0, 0 );
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
+const PulseCardInfo *PulseModel::card( uint32_t index ) const
+{
+ for ( TQValueList<PulseCardInfo>::ConstIterator it = m_cards.begin();
+ it != m_cards.end(); ++it )
+ if ( (*it).index == index )
+ return &(*it);
+ return 0;
+}
+
+PulseCardInfo *PulseModel::findCard( uint32_t index )
+{
+ for ( TQValueList<PulseCardInfo>::Iterator it = m_cards.begin();
+ it != m_cards.end(); ++it )
+ if ( it->index == index )
+ return &(*it);
+ return 0;
+}
+
+void PulseModel::setCardProfile( uint32_t cardIndex, const TQString &profileName )
+{
+ if ( !m_context || !m_mainloop || profileName.isEmpty() ) return;
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_operation *op = pa_context_set_card_profile_by_index(
+ m_context, cardIndex, profileName.utf8().data(), 0, 0 );
+ if ( op ) pa_operation_unref( op );
+ pa_threaded_mainloop_unlock( m_mainloop );
+}
+
#include "pulsemodel.moc"
diff --git a/src/model/pulsemodel.h b/src/model/pulsemodel.h
index b96f2b1..52e379f 100644
--- a/src/model/pulsemodel.h
+++ b/src/model/pulsemodel.h
@@ -2,6 +2,8 @@
#include <tqobject.h>
#include <tqptrlist.h>
+#include <tqmap.h>
+#include <tqvaluelist.h>
#include <pulse/pulseaudio.h>
@@ -9,6 +11,38 @@
class PulseDevice;
+// ---- card data structures ---------------------------------------------------
+
+struct PulseCardProfile {
+ TQString name;
+ TQString description;
+ bool available;
+};
+
+struct PulseCardPort {
+ TQString name;
+ TQString description;
+ int available; // pa_port_available_t: 0=unknown, 1=no, 2=yes
+ int direction; // PA_DIRECTION_INPUT=1, PA_DIRECTION_OUTPUT=2
+ uint32_t type; // pa_device_port_type_t
+ TQString availabilityGroup;
+};
+
+struct PulseCardInfo {
+ uint32_t index;
+ TQString name;
+ TQString description;
+ TQString activeProfile;
+ TQString vendor; // device.vendor.name
+ TQString product; // device.product.name
+ TQString formFactor; // device.form_factor
+ TQString busType; // device.bus (pci/usb/etc)
+ TQValueList<PulseCardProfile> profiles;
+ TQValueList<PulseCardPort> ports;
+};
+
+// ---- model ------------------------------------------------------------------
+
class PulseModel : public TQObject
{
TQ_OBJECT
@@ -21,6 +55,16 @@ public:
void close();
TQPtrList<AudioDevice> devices( AudioDevice::Category cat ) const;
+ AudioDevice *defaultOutput() const;
+ AudioDevice *defaultInput() const;
+
+ void setDefaultOutput( AudioDevice *dev );
+ void setDefaultInput( AudioDevice *dev );
+ void moveSinkInputToSink( AudioDevice *playbackDev, AudioDevice *outputDev );
+
+ TQValueList<PulseCardInfo> cards() const { return m_cards; }
+ const PulseCardInfo *card( uint32_t index ) const;
+ void setCardProfile( uint32_t cardIndex, const TQString &profileName );
protected:
void customEvent( TQCustomEvent *e );
@@ -28,14 +72,22 @@ protected:
signals:
void deviceAdded( AudioDevice *dev );
void deviceRemoved( AudioDevice *dev );
- void ready(); // emitted once initial enumeration is complete
+ void defaultOutputChanged( AudioDevice *dev );
+ void defaultInputChanged( AudioDevice *dev );
+ void ready();
+
+ void cardAdded( uint32_t index );
+ void cardRemoved( uint32_t index );
+ void cardUpdated( uint32_t index );
private:
static void contextStateCb( pa_context *c, void *userdata );
+ static void serverInfoCb( pa_context *c, const pa_server_info *info, 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 cardInfoCb( pa_context *c, const pa_card_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();
@@ -48,11 +100,19 @@ private:
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 );
+ TQMap<TQString, PulseDevice*> m_sinksByName;
+ TQMap<TQString, PulseDevice*> m_sourcesByName;
+ TQString m_defaultSinkName;
+ TQString m_defaultSourceName;
+ TQMap<uint32_t, uint32_t> m_sourceOutputToSource; // soIdx → sourceIdx
+
+ TQValueList<PulseCardInfo> m_cards;
+
+ PulseDevice *findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex );
+ PulseCardInfo *findCard( uint32_t index );
};