summaryrefslogtreecommitdiff
path: root/src/model/pulsedevice.cpp
diff options
context:
space:
mode:
authorCalvin Morrison <calvin@pobox.com>2026-05-15 10:10:04 -0400
committerCalvin Morrison <calvin@pobox.com>2026-05-15 10:10:04 -0400
commite776bc768cf9afca1867200e25d64d315cd72a3e (patch)
tree6745527b939c9d37147d7dc98e8664437ee433f6 /src/model/pulsedevice.cpp
parent4e602e78cdfc210ab7781668df2a88afb923258b (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.cpp227
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"