diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-05-12 21:32:53 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-05-12 21:32:53 -0400 |
| commit | f6f7c36909fa161efe53c40e9b4c34856e751536 (patch) | |
| tree | eff44527b0be61eb2e19c9f483ed38b72879af11 /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.cpp | 271 |
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" |
