Skip to main content

crabshell_services/audio/
peaks.rs

1//! Peak meters: how loud a sink, a source or an app's stream is now. Each
2//! meter is a record stream the server fills with one peak per channel
3//! [`RATE`] times a second, open only while a config holds the meter.
4
5use std::panic::{self, AssertUnwindSafe, Location};
6use std::rc::Rc;
7use std::sync::Arc;
8
9use calloop::channel::Sender;
10use crabshell_reactive::{ReadSignal, Signal, untrack};
11use crabshell_ui::{IntoProp, Prop};
12use libpulse_binding::channelmap::Map;
13use libpulse_binding::context::{self, Context};
14use libpulse_binding::def::BufferAttr;
15use libpulse_binding::sample::{Format, Spec};
16use libpulse_binding::stream::{FlagSet, PeekResult, State, Stream};
17
18use super::{Audio, AudioService, Event, Locked, Message, Nodes, Pulse, on_pa_thread};
19use crate::dbus::set_changed;
20use crate::lines::panic_message;
21
22/// Peaks a second, as pavucontrol reads them: smooth on a level bar, and
23/// each one wakes the shell, so no more than that.
24const RATE: u32 = 25;
25
26/// What a peak meter listens to, for [`Audio::peaks`].
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum PeakTarget {
29  /// The sink called this, through its monitor: what it plays.
30  Sink(String),
31  /// The source called this, such as a microphone or a sink's monitor.
32  Source(String),
33  /// The stream numbered this: what that app plays.
34  Stream(u32),
35}
36
37/// A target that stays.
38impl IntoProp<Option<PeakTarget>> for PeakTarget {
39  fn into_prop(self) -> Prop<Option<PeakTarget>> {
40    // A closure that reads no signal runs once.
41    (move || Some(self.clone())).into_prop()
42  }
43}
44
45impl Audio {
46  /// A peak meter, as Quickshell's `PwNodePeakMonitor`: each channel's
47  /// loudest sample over the last 1/25 s, from 0 to 1, in the order of
48  /// the target's `channels`. The loudest of them, Quickshell's `peak`, is
49  /// `peaks.with(|p| p.iter().copied().fold(0.0, f32::max))`. Empty
50  /// before the first reading, while there is no target and once it goes;
51  /// all 0 while a sink plays nothing or a stream is paused.
52  ///
53  /// `target` takes a [`PeakTarget`] or a closure returning an `Option`
54  /// of one: the meter moves when a signal the closure read changes, and
55  /// `None` stops it, as Quickshell's `enabled`. A panic in the closure is
56  /// logged and stops the meter until the next change.
57  ///
58  /// This is the one audio reading that wakes the shell while nothing
59  /// changes on screen: while the meter records, libpulse's thread and the
60  /// shell wake 25 times a second. The signal is set only when a peak
61  /// changes, so silence repaints nothing. A sink's or a stream's meter
62  /// records only while something plays, is corked while nothing does,
63  /// and never keeps a device awake; a source's records all the time,
64  /// as any recording of a microphone does. Nothing records until a
65  /// config asks for a meter, and it stops when the context that asked
66  /// goes.
67  ///
68  /// On PipeWire a sink's peaks come before its volume; a source's and a
69  /// stream's come after their own volume, as a recording of them would.
70  #[track_caller]
71  pub fn peaks(&self, target: impl IntoProp<Option<PeakTarget>>) -> ReadSignal<Vec<f32>> {
72    let at = Location::caller();
73    let Some(service) = self.service.clone().filter(|_| !self.owner.is_disposed()) else {
74      return self.no_peaks;
75    };
76    let peaks = self.owner.signal(Vec::new());
77    let target = target.into_prop().shared();
78    // A memo, so a closure that reads a sink's volume moves the meter only
79    // when the target itself changes.
80    let target = self.owner.memo(move || {
81      let chosen = panic::catch_unwind(AssertUnwindSafe(|| target()));
82      chosen.unwrap_or_else(|payload| {
83        let message = panic_message(payload.as_ref());
84        tracing::error!(
85          "cx.audio().peaks (called at {at}) panicked choosing its target: {message}"
86        );
87        None
88      })
89    });
90    // Each run's scope holds the meter it opened, so a new target or the
91    // owner going closes it.
92    self.owner.effect_scoped(move |scope| {
93      if let Some(target) = target.get() {
94        let id = service.meter(target, peaks);
95        scope.signal(Unmeter(Rc::clone(&service), id));
96      }
97    });
98    peaks.read_only()
99  }
100}
101
102/// A meter a config holds: what it listens to, and where its peaks go.
103pub(super) struct Wanted {
104  id: u64,
105  target: PeakTarget,
106  pub(super) peaks: Signal<Vec<f32>>,
107}
108
109/// Closes a meter when the scope that opened it drops this.
110struct Unmeter(Rc<AudioService>, u64);
111
112impl Drop for Unmeter {
113  fn drop(&mut self) {
114    self.0.unmeter(self.1);
115  }
116}
117
118impl AudioService {
119  /// Adds a meter for `target`, recording at once if the server has it.
120  fn meter(&self, target: PeakTarget, peaks: Signal<Vec<f32>>) -> u64 {
121    let id = self.next_meter.get();
122    self.next_meter.set(id + 1);
123    self.meters.borrow_mut().push(Wanted { id, target, peaks });
124    self.sync_meters();
125    id
126  }
127
128  fn unmeter(&self, id: u64) {
129    self.meters.borrow_mut().retain(|meter| meter.id != id);
130    self.sync_meters();
131  }
132
133  /// Opens and closes record streams to match the meters. Untracked: a
134  /// meter's effect calls this, and must not come to depend on the peaks
135  /// it sets.
136  fn sync_meters(&self) {
137    untrack(|| {
138      let wanted = self.meters.borrow();
139      if let Some(pulse) = self.pulse.borrow_mut().as_mut() {
140        pulse.sync_meters(&wanted);
141      }
142    });
143  }
144}
145
146/// Where a meter records from.
147#[derive(Clone, PartialEq)]
148pub(super) struct Source {
149  /// The source's name, a sink's monitor for a sink or a stream.
150  name: String,
151  map: Map,
152  /// The stream to keep to, of those its sink plays.
153  stream: Option<u32>,
154  /// Records only while something else keeps the sink playing, so a
155  /// meter alone never wakes a device. A source's meter isn't: a
156  /// microphone's level shows with nothing else recording.
157  passive: bool,
158}
159
160impl Nodes {
161  /// Where a meter for `target` records from, as last read, and whether
162  /// it is quiet: its sink idle or its stream paused, which sends nothing,
163  /// so the last peaks would stay. `None` while the server doesn't have
164  /// the target.
165  fn source_of(&self, target: &PeakTarget) -> Option<(Source, bool)> {
166    let (name, map, stream, quiet) = match target {
167      PeakTarget::Sink(name) => {
168        let sink = self.sinks.iter().find(|sink| sink.name == *name)?;
169        (sink.monitor_source.clone()?, sink.map, None, !sink.running)
170      }
171      PeakTarget::Source(name) => {
172        let source = self.sources.iter().find(|source| source.name == *name)?;
173        (source.name.clone(), source.map, None, false)
174      }
175      PeakTarget::Stream(index) => {
176        let stream = self.streams.iter().find(|stream| stream.index == *index)?;
177        let sink = self.sinks.iter().find(|sink| sink.index == stream.sink)?;
178        let quiet = stream.corked || !sink.running;
179        let name = sink.monitor_source.clone()?;
180        (name, stream.map, Some(*index), quiet)
181      }
182    };
183    let passive = !matches!(target, PeakTarget::Source(_));
184    let source = Source {
185      name,
186      map,
187      stream,
188      passive,
189    };
190    Some((source, quiet))
191  }
192}
193
194/// A meter's record stream.
195pub(super) struct Meter {
196  id: u64,
197  peaks: Signal<Vec<f32>>,
198  source: Source,
199  /// `None` once the server ended it, or if it didn't open. Either way it
200  /// isn't tried again until its source changes.
201  stream: Option<Recording>,
202  /// While quiet, its peaks are 0, its stream is corked and what it
203  /// still reads is dropped.
204  quiet: bool,
205  /// Whether its stream is corked, as last asked of the server.
206  corked: bool,
207}
208
209impl Meter {
210  /// Corks the stream while quiet, so even a server that sends an idle
211  /// sink's silence sends nothing, and uncorks it after. A stream the
212  /// server is still making is corked once it is made.
213  fn cork(&mut self) {
214    let Some(Recording(stream)) = self.stream.as_mut() else {
215      return;
216    };
217    // libpulse makes no operation for a stream that isn't ready, and the
218    // binding panics on that.
219    if self.corked != self.quiet && stream.get_state() == State::Ready {
220      drop(stream.set_corked_state(self.quiet, None));
221      self.corked = self.quiet;
222    }
223  }
224}
225
226/// A record stream whose callbacks are cleared before it drops: libpulse
227/// calls them again as a stream ends, and from its context while the
228/// server is still making one. Dropped under the mainloop's lock, or with
229/// libpulse's thread stopped.
230pub(super) struct Recording(Stream);
231
232impl Drop for Recording {
233  fn drop(&mut self) {
234    self.0.set_read_callback(None);
235    self.0.set_state_callback(None);
236  }
237}
238
239/// Ends `recording`, under the mainloop's lock. Dropping one disconnects
240/// it, but libpulse refuses that while the server is still making it, and
241/// the server would then record with nothing reading. So that one waits
242/// in `closing` until the server answers.
243fn end(
244  mut recording: Recording,
245  closing: &mut Vec<Recording>,
246  sender: &Arc<Sender<Message>>,
247  generation: u32,
248) {
249  if recording.0.get_state() != State::Creating {
250    return;
251  }
252  recording.0.set_read_callback(None);
253  let sender = Arc::clone(sender);
254  let on_state = on_pa_thread(move || {
255    let _ = sender.send((generation, Event::Closing));
256  });
257  recording.0.set_state_callback(Some(Box::new(on_state)));
258  closing.push(recording);
259}
260
261impl Pulse {
262  /// Closes each meter no longer wanted or whose source changed, and
263  /// opens each wanted one the server has.
264  pub(super) fn sync_meters(&mut self, wanted: &[Wanted]) {
265    let next: Vec<_> = wanted
266      .iter()
267      .map(|meter| (meter.id, meter.peaks, self.nodes.source_of(&meter.target)))
268      .collect();
269    let _lock = Locked::new(&mut self.mainloop);
270    let (closing, sender, generation) = (&mut self.closing, &self.sender, self.generation);
271    self.meters.retain_mut(|meter| {
272      let same = |(id, _, found): &(u64, _, Option<(Source, bool)>)| {
273        *id == meter.id
274          && found
275            .as_ref()
276            .is_some_and(|(source, _)| *source == meter.source)
277      };
278      let kept = next.iter().any(same);
279      if !kept {
280        if let Some(recording) = meter.stream.take() {
281          end(recording, closing, sender, generation);
282        }
283        set_changed(meter.peaks, Vec::new());
284      }
285      kept
286    });
287    let mut context = self.context.as_mut();
288    let ready = context
289      .as_ref()
290      .is_some_and(|c| c.get_state() == context::State::Ready);
291    for (id, peaks, found) in next {
292      let Some((source, quiet)) = found else {
293        continue;
294      };
295      let meter = match self.meters.iter_mut().find(|meter| meter.id == id) {
296        Some(meter) => meter,
297        None => match context.as_mut().filter(|_| ready) {
298          Some(context) => {
299            let stream = open(context, &source, id, &self.sender, self.generation, quiet);
300            self.meters.push(Meter {
301              id,
302              peaks,
303              source,
304              stream,
305              quiet,
306              corked: quiet,
307            });
308            self.meters.last_mut().expect("the meter just pushed")
309          }
310          None => continue,
311        },
312      };
313      meter.quiet = quiet;
314      meter.cork();
315      if quiet && meter.stream.is_some() {
316        let channels = usize::from(meter.source.map.len());
317        set_changed(meter.peaks, vec![0.0; channels]);
318      }
319    }
320  }
321
322  /// Shows the peaks meter `id`'s stream has read since the last time:
323  /// for each channel, the loudest.
324  pub(super) fn read_peaks(&mut self, id: u64) {
325    let _lock = Locked::new(&mut self.mainloop);
326    let Some(meter) = self.meters.iter_mut().find(|meter| meter.id == id) else {
327      return;
328    };
329    let channels = usize::from(meter.source.map.len());
330    let Some(Recording(stream)) = meter.stream.as_mut() else {
331      return;
332    };
333    let mut peaks = None;
334    loop {
335      match stream.peek() {
336        Ok(PeekResult::Data(bytes)) => louder(peaks.get_or_insert(vec![0.0; channels]), bytes),
337        Ok(PeekResult::Hole(_)) => {}
338        Ok(PeekResult::Empty) | Err(_) => break,
339      }
340      if stream.discard().is_err() {
341        break;
342      }
343    }
344    // Quiet, a late reading from before would stay.
345    if let Some(peaks) = peaks.filter(|_| !meter.quiet) {
346      set_changed(meter.peaks, peaks);
347    }
348  }
349
350  /// Closes meter `id` if the server ended its stream, as when its source
351  /// goes, and corks or uncorks it once the server made it.
352  pub(super) fn on_meter_state(&mut self, id: u64) {
353    let _lock = Locked::new(&mut self.mainloop);
354    let Some(meter) = self.meters.iter_mut().find(|meter| meter.id == id) else {
355      return;
356    };
357    let state = meter
358      .stream
359      .as_ref()
360      .map(|recording| recording.0.get_state());
361    if matches!(state, Some(State::Failed | State::Terminated)) {
362      let source = &meter.source.name;
363      tracing::info!(source, "the audio server ended a peak meter");
364      meter.stream = None;
365      set_changed(meter.peaks, Vec::new());
366    }
367    meter.cork();
368  }
369
370  /// Ends each closing stream the server has now made or refused.
371  pub(super) fn on_closing(&mut self) {
372    let _lock = Locked::new(&mut self.mainloop);
373    let made = |recording: &Recording| recording.0.get_state() != State::Creating;
374    self.closing.retain(|recording| !made(recording));
375  }
376}
377
378/// Closes every meter as the connection goes, under the mainloop's lock;
379/// the context's streams go with it. Each meter opens again once the next
380/// connection's lists have its source.
381pub(super) fn close_all(meters: &mut Vec<Meter>, closing: &mut Vec<Recording>) {
382  closing.clear();
383  for meter in meters.drain(..) {
384    set_changed(meter.peaks, Vec::new());
385  }
386}
387
388/// Starts recording peaks from `source`, `corked` or not, under the
389/// mainloop's lock. `None`, logged, if the server or libpulse refused.
390fn open(
391  context: &mut Context,
392  source: &Source,
393  id: u64,
394  sender: &Arc<Sender<Message>>,
395  generation: u32,
396  corked: bool,
397) -> Option<Recording> {
398  let spec = Spec {
399    format: Format::FLOAT32NE,
400    rate: RATE,
401    channels: source.map.len(),
402  };
403  let name = &source.name;
404  let Some(mut stream) = Stream::new(context, "Peak meter", &spec, Some(&source.map)) else {
405    tracing::warn!(name, "libpulse made no stream for a peak meter");
406    return None;
407  };
408  if let Some(index) = source.stream
409    && let Err(error) = stream.set_monitor_stream(index)
410  {
411    tracing::warn!(name, index, %error, "a peak meter couldn't keep to its stream");
412    return None;
413  }
414  // One frame a fragment, so each peak arrives as it is measured.
415  let frame = u32::from(spec.channels) * 4;
416  let attr = BufferAttr {
417    maxlength: u32::MAX,
418    tlength: u32::MAX,
419    prebuf: u32::MAX,
420    minreq: u32::MAX,
421    fragsize: frame,
422  };
423  let mut flags = FlagSet::PEAK_DETECT | FlagSet::ADJUST_LATENCY | FlagSet::DONT_MOVE;
424  if corked {
425    flags |= FlagSet::START_CORKED;
426  }
427  if source.passive {
428    flags |= FlagSet::DONT_INHIBIT_AUTO_SUSPEND;
429  }
430  if let Err(error) = stream.connect_record(Some(name), Some(&attr), flags) {
431    tracing::warn!(name, %error, "the audio server refused a peak meter");
432    return None;
433  }
434  // Set under the lock, so neither runs before the stream is kept.
435  let read = Arc::clone(sender);
436  let on_read = on_pa_thread(move |_| {
437    let _ = read.send((generation, Event::Peaks(id)));
438  });
439  stream.set_read_callback(Some(Box::new(on_read)));
440  let state = Arc::clone(sender);
441  let on_state = on_pa_thread(move || {
442    let _ = state.send((generation, Event::MeterState(id)));
443  });
444  stream.set_state_callback(Some(Box::new(on_state)));
445  Some(Recording(stream))
446}
447
448/// Raises each channel's peak to the loudest sample of the frames of
449/// 32-bit floats in `bytes`, at most 1. A sample that isn't a number
450/// counts as silence.
451pub(super) fn louder(peaks: &mut [f32], bytes: &[u8]) {
452  if peaks.is_empty() {
453    return;
454  }
455  for frame in bytes.chunks_exact(4 * peaks.len()) {
456    for (peak, sample) in peaks.iter_mut().zip(frame.as_chunks::<4>().0) {
457      // `max` passes over NaN.
458      *peak = peak.max(f32::from_ne_bytes(*sample).abs()).min(1.0);
459    }
460  }
461}