diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-05-15 10:10:04 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-05-15 10:10:04 -0400 |
| commit | e776bc768cf9afca1867200e25d64d315cd72a3e (patch) | |
| tree | 6745527b939c9d37147d7dc98e8664437ee433f6 /src/model | |
| parent | 4e602e78cdfc210ab7781668df2a88afb923258b (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.h | 7 | ||||
| -rw-r--r-- | src/model/pulsedevice.cpp | 227 | ||||
| -rw-r--r-- | src/model/pulsedevice.h | 42 | ||||
| -rw-r--r-- | src/model/pulsemodel.cpp | 451 | ||||
| -rw-r--r-- | src/model/pulsemodel.h | 66 |
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 ); }; |
