summaryrefslogtreecommitdiff
path: root/src/model/pulsemodel.cpp
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/pulsemodel.cpp
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/pulsemodel.cpp')
-rw-r--r--src/model/pulsemodel.cpp451
1 files changed, 434 insertions, 17 deletions
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"