summaryrefslogtreecommitdiff
path: root/src/model/pulsemodel.cpp
diff options
context:
space:
mode:
authorCalvin Morrison <calvin@pobox.com>2026-05-12 21:32:53 -0400
committerCalvin Morrison <calvin@pobox.com>2026-05-12 21:32:53 -0400
commitf6f7c36909fa161efe53c40e9b4c34856e751536 (patch)
treeeff44527b0be61eb2e19c9f483ed38b72879af11 /src/model/pulsemodel.cpp
Initial tmix skeleton — model layer + basic UI
PulseModel: stable PulseDevice objects keyed by PA index, updated in-place via postEvent reconciliation. No bulk rebuilds. Three signals: deviceAdded, deviceRemoved (device changed handled per-device via volumeChanged/muteChanged/nameChanged). MixerWindow: four-tab layout (Output/Input/Playback/Recording), adds and removes individual DeviceWidgets in response to model signals. Builds and links cleanly against TQt3/TDE + libpulse. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/model/pulsemodel.cpp')
-rw-r--r--src/model/pulsemodel.cpp271
1 files changed, 271 insertions, 0 deletions
diff --git a/src/model/pulsemodel.cpp b/src/model/pulsemodel.cpp
new file mode 100644
index 0000000..de745bc
--- /dev/null
+++ b/src/model/pulsemodel.cpp
@@ -0,0 +1,271 @@
+#include "pulsemodel.h"
+#include "pulsedevice.h"
+
+#include <tqapplication.h>
+
+// Custom event posted from the PA thread to the main thread.
+static const int PA_EVENT = TQEvent::User + 1;
+
+struct PAEvent : public TQCustomEvent {
+ enum Kind { DeviceAdded, DeviceRemoved, DeviceUpdated };
+ Kind kind;
+ AudioDevice::Category cat;
+ uint32_t paIndex;
+ TQString name;
+ int volume;
+ bool muted;
+
+ PAEvent( Kind k, AudioDevice::Category c, uint32_t idx,
+ const TQString &n = TQString(), int vol = 0, bool m = false )
+ : TQCustomEvent(PA_EVENT), kind(k), cat(c), paIndex(idx),
+ name(n), volume(vol), muted(m) {}
+};
+
+// ---- helpers ----------------------------------------------------------------
+
+static int paVolumeToPercent( const pa_cvolume &cv )
+{
+ pa_volume_t avg = pa_cvolume_avg( &cv );
+ return (int)( (double)avg / PA_VOLUME_NORM * 100.0 + 0.5 );
+}
+
+// ---- ctor / dtor ------------------------------------------------------------
+
+PulseModel::PulseModel( TQObject *parent )
+ : TQObject(parent), m_mainloop(0), m_context(0)
+{
+ m_sinks.setAutoDelete( true );
+ m_sources.setAutoDelete( true );
+ m_sinkInputs.setAutoDelete( true );
+ m_sourceOutputs.setAutoDelete( true );
+}
+
+PulseModel::~PulseModel()
+{
+ close();
+}
+
+// ---- connect / disconnect ---------------------------------------------------
+
+bool PulseModel::open()
+{
+ m_mainloop = pa_threaded_mainloop_new();
+ if ( !m_mainloop ) return false;
+
+ pa_mainloop_api *api = pa_threaded_mainloop_get_api( m_mainloop );
+ m_context = pa_context_new( api, "tmix" );
+ if ( !m_context ) {
+ pa_threaded_mainloop_free( m_mainloop );
+ m_mainloop = 0;
+ return false;
+ }
+
+ pa_context_set_state_callback( m_context, contextStateCb, this );
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_threaded_mainloop_start( m_mainloop );
+ pa_context_connect( m_context, 0, PA_CONTEXT_NOFLAGS, 0 );
+ pa_threaded_mainloop_unlock( m_mainloop );
+ return true;
+}
+
+void PulseModel::close()
+{
+ if ( m_context ) {
+ pa_threaded_mainloop_lock( m_mainloop );
+ pa_context_disconnect( m_context );
+ pa_context_unref( m_context );
+ m_context = 0;
+ pa_threaded_mainloop_unlock( m_mainloop );
+ }
+ if ( m_mainloop ) {
+ pa_threaded_mainloop_stop( m_mainloop );
+ pa_threaded_mainloop_free( m_mainloop );
+ m_mainloop = 0;
+ }
+ m_sinks.clear();
+ m_sources.clear();
+ m_sinkInputs.clear();
+ m_sourceOutputs.clear();
+}
+
+// ---- public query -----------------------------------------------------------
+
+TQPtrList<AudioDevice> PulseModel::devices( AudioDevice::Category cat ) const
+{
+ TQPtrList<AudioDevice> result;
+ const TQPtrList<PulseDevice> *src = 0;
+ switch ( cat ) {
+ case AudioDevice::Output: src = &m_sinks; break;
+ case AudioDevice::Input: src = &m_sources; break;
+ case AudioDevice::Playback: src = &m_sinkInputs; break;
+ case AudioDevice::Recording: src = &m_sourceOutputs; break;
+ }
+ if ( src )
+ for ( TQPtrListIterator<PulseDevice> it(*src); *it; ++it )
+ result.append( *it );
+ return result;
+}
+
+// ---- PA thread callbacks (called from PA thread — only post events) ---------
+
+void PulseModel::contextStateCb( pa_context *c, void *userdata )
+{
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ switch ( pa_context_get_state(c) ) {
+ case PA_CONTEXT_READY:
+ pa_context_set_subscribe_callback( c, subscribeCb, self );
+ pa_context_subscribe( c,
+ (pa_subscription_mask_t)(
+ PA_SUBSCRIPTION_MASK_SINK |
+ PA_SUBSCRIPTION_MASK_SOURCE |
+ PA_SUBSCRIPTION_MASK_SINK_INPUT |
+ PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT ),
+ 0, 0 );
+ self->enumerateAll();
+ break;
+ case PA_CONTEXT_FAILED:
+ case PA_CONTEXT_TERMINATED:
+ break;
+ default:
+ break;
+ }
+}
+
+void PulseModel::enumerateAll()
+{
+ // Called from PA thread (inside context state callback at READY).
+ pa_operation_unref( pa_context_get_sink_info_list( m_context, sinkInfoCb, this ) );
+ pa_operation_unref( pa_context_get_source_info_list( m_context, sourceInfoCb, this ) );
+ pa_operation_unref( pa_context_get_sink_input_info_list( m_context, sinkInputInfoCb, this ) );
+ pa_operation_unref( pa_context_get_source_output_info_list( m_context, sourceOutputInfoCb, this ) );
+}
+
+void PulseModel::sinkInfoCb( pa_context *, const pa_sink_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAEvent(
+ PAEvent::DeviceAdded, AudioDevice::Output, info->index,
+ TQString::fromUtf8( info->description ),
+ paVolumeToPercent( info->volume ), info->mute != 0 ) );
+}
+
+void PulseModel::sourceInfoCb( pa_context *, const pa_source_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ // Skip monitor sources — they're PA internals, not real inputs.
+ if ( info->monitor_of_sink != PA_INVALID_INDEX ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAEvent(
+ PAEvent::DeviceAdded, AudioDevice::Input, info->index,
+ TQString::fromUtf8( info->description ),
+ paVolumeToPercent( info->volume ), info->mute != 0 ) );
+}
+
+void PulseModel::sinkInputInfoCb( pa_context *, const pa_sink_input_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAEvent(
+ PAEvent::DeviceAdded, AudioDevice::Playback, info->index,
+ TQString::fromUtf8( info->name ),
+ paVolumeToPercent( info->volume ), info->mute != 0 ) );
+}
+
+void PulseModel::sourceOutputInfoCb( pa_context *, const pa_source_output_info *info, int eol, void *userdata )
+{
+ if ( eol || !info ) return;
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+ TQApplication::postEvent( self, new PAEvent(
+ PAEvent::DeviceAdded, AudioDevice::Recording, info->index,
+ TQString::fromUtf8( info->name ),
+ paVolumeToPercent( info->volume ), info->mute != 0 ) );
+}
+
+void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata )
+{
+ PulseModel *self = static_cast<PulseModel *>(userdata);
+
+ pa_subscription_event_type_t facility = (pa_subscription_event_type_t)( t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK );
+ pa_subscription_event_type_t evtype = (pa_subscription_event_type_t)( t & PA_SUBSCRIPTION_EVENT_TYPE_MASK );
+
+ AudioDevice::Category cat;
+ switch ( facility ) {
+ case PA_SUBSCRIPTION_EVENT_SINK: cat = AudioDevice::Output; break;
+ case PA_SUBSCRIPTION_EVENT_SOURCE: cat = AudioDevice::Input; break;
+ case PA_SUBSCRIPTION_EVENT_SINK_INPUT: cat = AudioDevice::Playback; break;
+ case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: cat = AudioDevice::Recording; break;
+ default: return;
+ }
+
+ if ( evtype == PA_SUBSCRIPTION_EVENT_REMOVE ) {
+ TQApplication::postEvent( self, new PAEvent( PAEvent::DeviceRemoved, cat, idx ) );
+ return;
+ }
+
+ // NEW or CHANGE — re-query to get current state.
+ pa_operation *op = 0;
+ switch ( facility ) {
+ case PA_SUBSCRIPTION_EVENT_SINK:
+ op = pa_context_get_sink_info_by_index( c, idx, sinkInfoCb, self );
+ break;
+ case PA_SUBSCRIPTION_EVENT_SOURCE:
+ op = pa_context_get_source_info_by_index( c, idx, sourceInfoCb, self );
+ break;
+ case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
+ op = pa_context_get_sink_input_info( c, idx, sinkInputInfoCb, self );
+ break;
+ case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
+ op = pa_context_get_source_output_info( c, idx, sourceOutputInfoCb, self );
+ break;
+ default: break;
+ }
+ if ( op ) pa_operation_unref( op );
+}
+
+// ---- main thread event handler ----------------------------------------------
+
+void PulseModel::customEvent( TQCustomEvent *e )
+{
+ if ( e->type() != PA_EVENT ) return;
+ PAEvent *ev = static_cast<PAEvent *>(e);
+
+ TQPtrList<PulseDevice> *list = 0;
+ switch ( ev->cat ) {
+ case AudioDevice::Output: list = &m_sinks; break;
+ case AudioDevice::Input: list = &m_sources; break;
+ case AudioDevice::Playback: list = &m_sinkInputs; break;
+ case AudioDevice::Recording: list = &m_sourceOutputs; break;
+ }
+
+ if ( ev->kind == PAEvent::DeviceRemoved ) {
+ PulseDevice *dev = findDevice( *list, ev->paIndex );
+ if ( dev ) {
+ emit deviceRemoved( dev );
+ list->remove( dev ); // autoDelete=true, so dev is deleted here
+ }
+ return;
+ }
+
+ // DeviceAdded or DeviceUpdated (both come through the same info callbacks).
+ PulseDevice *dev = findDevice( *list, ev->paIndex );
+ if ( dev ) {
+ dev->update( ev->name, ev->volume, ev->muted );
+ } else {
+ dev = new PulseDevice( ev->cat, ev->paIndex, this );
+ dev->setPAContext( m_context, m_mainloop );
+ dev->update( ev->name, ev->volume, ev->muted );
+ list->append( dev );
+ emit deviceAdded( dev );
+ }
+}
+
+PulseDevice *PulseModel::findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex )
+{
+ for ( TQPtrListIterator<PulseDevice> it(list); *it; ++it )
+ if ( (*it)->paIndex() == paIndex )
+ return *it;
+ return 0;
+}
+
+#include "pulsemodel.moc"