#include "pulsedevice.h" #include "pulsemodel.h" #include "../ui/devicewidget.h" #include #include // ---- 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_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( bool paAlreadyDead ) { if ( m_monitorStream ) { if ( paAlreadyDead ) { pa_stream_set_read_callback( m_monitorStream, 0, 0 ); pa_stream_unref( m_monitorStream ); } else if ( 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 ); pa_threaded_mainloop_unlock( m_mainloop ); } m_monitorStream = 0; } m_context = 0; m_mainloop = 0; } // ---- 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; 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 ); } // ---- 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. // Source outputs don't support pa_stream_set_monitor_stream; they connect to the source directly. 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(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(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(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 ) { if ( pct <= 0 ) return 0; if ( pct >= 100 ) return PA_VOLUME_NORM; 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 = 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; } if ( op ) pa_operation_unref( op ); pa_threaded_mainloop_unlock( m_mainloop ); } 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; } if ( op ) pa_operation_unref( op ); pa_threaded_mainloop_unlock( m_mainloop ); } TQWidget *PulseDevice::createWidget( TQWidget *parent ) { return new DeviceWidget( this, m_model, parent ); } #include "pulsedevice.moc"