#include "pulsemodel.h" #include "pulsedevice.h" #include // 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 PulseModel::devices( AudioDevice::Category cat ) const { TQPtrList result; const TQPtrList *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 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(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(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(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(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(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(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(e); TQPtrList *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 &list, uint32_t paIndex ) { for ( TQPtrListIterator it(list); *it; ++it ) if ( (*it)->paIndex() == paIndex ) return *it; return 0; } #include "pulsemodel.moc"