diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-05-15 10:10:04 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-05-15 10:10:04 -0400 |
| commit | e776bc768cf9afca1867200e25d64d315cd72a3e (patch) | |
| tree | 6745527b939c9d37147d7dc98e8664437ee433f6 /src/model/pulsedevice.cpp | |
| parent | 4e602e78cdfc210ab7781668df2a88afb923258b (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/pulsedevice.cpp')
| -rw-r--r-- | src/model/pulsedevice.cpp | 227 |
1 files changed, 190 insertions, 37 deletions
diff --git a/src/model/pulsedevice.cpp b/src/model/pulsedevice.cpp index e0f84ab..de778b9 100644 --- a/src/model/pulsedevice.cpp +++ b/src/model/pulsedevice.cpp @@ -1,37 +1,168 @@ #include "pulsedevice.h" +#include "pulsemodel.h" #include "../ui/devicewidget.h" -#include <tqwidget.h> + +#include <tqapplication.h> +#include <string.h> + +// ---- 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_volume(0), m_muted(false), - m_context(0), m_mainloop(0) + 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() +{ + if ( m_monitorStream && 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 ); + m_monitorStream = 0; + pa_threaded_mainloop_unlock( m_mainloop ); + } + m_context = 0; + m_mainloop = 0; } -void PulseDevice::update( const TQString &name, int volume, bool muted ) +// ---- 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; - m_name = name; - m_volume = volume; - m_muted = muted; + 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 ); } -// ---- write back to PA ------------------------------------------------------- +// ---- 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. + 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<PulseDevice*>(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<const float*>(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<PALevelEvent*>(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 ) { @@ -40,29 +171,60 @@ static pa_volume_t percentToPA( int pct ) 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; - pa_cvolume_set( &cv, 2, percentToPA(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; + 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 ); @@ -71,23 +233,14 @@ void PulseDevice::setVolume( int v ) 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; + 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 ); @@ -95,7 +248,7 @@ void PulseDevice::setMuted( bool m ) TQWidget *PulseDevice::createWidget( TQWidget *parent ) { - return new DeviceWidget( this, parent ); + return new DeviceWidget( this, m_model, parent ); } #include "pulsedevice.moc" |
