Source code for librosa.display.signal

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Time-domain signal visualization
================================

This module contains tools for visualizing time-domain audio signals,
waveforms, and their overlays. It includes functions like `waveshow`,
`wavebars`, and `wavef0` which handle adaptive plotting based on
zoom levels and sequence lengths.
"""
# mypy: disable-error-code="attr-defined"

# Standard library imports for type checking and future compatibility
from __future__ import annotations

import weakref
from typing import TYPE_CHECKING, cast

# Third-party imports for plotting and numerical operations
import matplotlib.axes as mplaxes
import matplotlib.collections as mcollections
import matplotlib.lines as mlines
import matplotlib.patches as mpatches
import matplotlib.transforms as mtransforms
import numpy as np
from matplotlib.legend import Legend
from matplotlib.legend_handler import HandlerBase, HandlerLine2D, HandlerPatch

# Core and utility imports for audio processing
from .. import core, util
from ..util.exceptions import ParameterError

# Signal visualization utilities for time-domain waveforms
if TYPE_CHECKING:
    from typing import Any, Literal, Sequence

    import matplotlib
    from matplotlib.artist import Artist
    from matplotlib.collections import PolyCollection
    from matplotlib.lines import Line2D
    from matplotlib.markers import MarkerStyle
    from matplotlib.path import Path as MplPath

    from .._typing import ArrayLike

# Module imports for waveform formatting and adaptive plotting
from .formatting import (
    Transformf0,
    _check_axes,
    _decorate_axis,
)


[docs] class AdaptiveWaveplot: """A helper class for managing adaptive wave visualizations. This object is used to dynamically switch between sample-based and envelope-based visualizations of waveforms. When the display is zoomed in such that no more than `max_samples` would be visible, the sample-based display is used. When displaying the raw samples would require more than `max_samples`, an envelope-based plot is used instead. You should never need to instantiate this object directly, as it is constructed automatically by `waveshow`. Parameters ---------- times : np.ndarray An array containing the time index (in seconds) for each sample. y : np.ndarray An array containing the (monophonic) wave samples. steps : matplotlib.lines.Line2D The matplotlib artist used for the sample-based visualization. This is constructed by `matplotlib.pyplot.step`. envelope : matplotlib.collections.PolyCollection The matplotlib artist used for the envelope-based visualization. This is constructed by `matplotlib.pyplot.fill_between`. sr : number > 0 The sampling rate of the audio max_samples : int > 0 The maximum number of samples to use for sample-based display. transpose : bool If `True`, display the wave vertically instead of horizontally. label : str or None An optional label for the waveplot, used in legend entries. See Also -------- waveshow """ times: np.ndarray samples: np.ndarray sr: float max_samples: int transpose: bool cid: int | None label_proxy_: _WaveplotDecoy
[docs] def __init__( self, times: np.ndarray, y: np.ndarray, steps: Line2D, envelope: PolyCollection, sr: float = 22050, max_samples: int = 11025, transpose: bool = False, label: str | None = None, ): self.times = times self.samples = y self._steps_ref = weakref.ref(steps) self._envelope_ref = weakref.ref(envelope) self.sr = sr self.max_samples = max_samples self.transpose = transpose self.cid = None self._ax_ref: weakref.ref[mplaxes.Axes] | None = None # This creates an invisible proxy artist to contain the label self.label_proxy_ = _WaveplotDecoy(self) self.label_proxy_.set_in_layout(False) if label is not None: self.label_proxy_.set_label(label)
# Preserve the old attribute API by exposing properties with same names @property def steps(self) -> Line2D | None: """The step plot artist (Line2D), or None if garbage collected. Returns ------- Line2D or None The step plot artist, or ``None`` if it has been garbage collected. """ return self._steps_ref() @property def envelope(self) -> PolyCollection | None: """The envelope artist (PolyCollection), or None if garbage collected. Returns ------- PolyCollection or None The envelope artist, or ``None`` if it has been garbage collected. """ return self._envelope_ref() @property def ax(self) -> mplaxes.Axes | None: """The connected Axes, or None if not connected or garbage collected. Returns ------- matplotlib.axes.Axes or None The connected axes, or ``None`` if not connected or garbage collected. """ return None if self._ax_ref is None else self._ax_ref() def __del__(self) -> None: """Disconnect callback methods on delete""" self.disconnect(strict=True) def connect( self, ax: mplaxes.Axes, *, signal: str = "xlim_changed", ) -> None: """Connect the adaptor to a signal on an axes object. Note that if the adaptor has already been connected to an axes object, that connect is first broken and then replaced by a new callback. Parameters ---------- ax : matplotlib.axes.Axes The axes to connect with this adaptor's `update` signal : string, {"xlim_changed", "ylim_changed"} The signal to connect See Also -------- disconnect """ # Disconnect any existing callback first self.disconnect() # Attach to axes and store the connection id self._ax_ref = weakref.ref(ax) ax.add_artist(self.label_proxy_) self.cid = ax.callbacks.connect(signal, self.update) def disconnect(self, *, strict: bool = False) -> None: """Disconnect the adaptor's update callback. Parameters ---------- strict : bool If `True`, remove references to the connected axes. If `False` (default), only disconnect the callback. This functionality is intended primarily for internal use, and should have no observable effects for users. See Also -------- connect """ ax = self.ax if ax is not None and self.cid is not None: ax.callbacks.disconnect(self.cid) self.cid = None if strict: if self.label_proxy_.axes is not None: self.label_proxy_.remove() self._ax_ref = None def update(self, ax: mplaxes.Axes) -> None: """Update the matplotlib display according to the current viewport limits. This is a callback function, and should not be used directly. Parameters ---------- ax : matplotlib.axes.Axes The axes object to update """ # Deref artists and bail if they've been garbage collected steps = self.steps envelope = self.envelope if steps is None or envelope is None: return lims = ax.viewLim if self.transpose: dim = lims.height * self.sr start, end = lims.y0, lims.y1 xdata, ydata = self.samples, self.times data = steps.get_ydata() else: dim = lims.width * self.sr start, end = lims.x0, lims.x1 xdata, ydata = self.times, self.samples data = steps.get_xdata() # Does our width cover fewer than max_samples? # If so, then use the sample-based plot if dim <= self.max_samples: envelope.set_visible(False) steps.set_visible(True) # Now check our viewport # we have to squash mypy errors on operand compatibility # here because the type annotations from matplotlib are too # loose. if start <= data[0] or end >= data[-1]: # type: ignore[index] # Viewport expands beyond current data in steps; update # we want to cover a window of self.max_samples centered on the current viewport midpoint_time = (start + end) / 2 idx_start = np.searchsorted(self.times, midpoint_time - 0.5 * self.max_samples / self.sr) steps.set_data( xdata[idx_start : idx_start + self.max_samples], ydata[idx_start : idx_start + self.max_samples], ) else: # Otherwise, use the envelope plot envelope.set_visible(True) steps.set_visible(False) ax.figure.canvas.draw_idle()
# Keeps adaptors alive as long as their Axes exists, preventing GC _WAVESHOW_ADAPTORS: weakref.WeakKeyDictionary[mplaxes.Axes, set["AdaptiveWaveplot"]] = weakref.WeakKeyDictionary() class _WaveplotDecoy(mlines.Line2D): def __init__(self, parent_waveplot: AdaptiveWaveplot, *args: Any, **kwargs: Any): # We'll never actually set the color on this decoy at construction time kwargs["color"] = "none" super().__init__([], [], *args, **kwargs) self._waveplot_ref = weakref.ref(parent_waveplot) # Store weak reference to the parent wrapper @property def waveplot(self) -> AdaptiveWaveplot | None: return self._waveplot_ref() class _AdaptiveWaveplotHandler(HandlerBase): def create_artists( self, legend: Legend, orig_handle: Artist, xdescent: float, ydescent: float, width: float, height: float, fontsize: float, trans: mtransforms.Transform, ) -> list[Artist]: """ Matplotlib automatically passes the exact dimensions and coordinate transform (`trans`) needed to paint safely inside the legend key box. """ orig_handle = cast("_WaveplotDecoy", orig_handle) waveplot = orig_handle.waveplot if waveplot is None: return [] ax = waveplot.ax if ax is not None: bgcolor = ax.get_facecolor() else: bgcolor = "none" bg_rect = mpatches.Rectangle((0, 0), 1, 1, facecolor=bgcolor, edgecolor="none") bg_artists = HandlerPatch().create_artists(legend, bg_rect, xdescent, ydescent, width, height, fontsize, trans) proxy_line = mlines.Line2D([], []) if waveplot.steps is not None: proxy_line.update_from(waveplot.steps) proxy_line.set_data([], []) proxy_line.set(visible=True) line_artists = HandlerLine2D().create_artists( legend, proxy_line, xdescent, ydescent, width, height, fontsize, trans ) return [*bg_artists, *line_artists] # Add our custom handler to the default legend handler map if _WaveplotDecoy not in Legend.get_default_handler_map(): Legend.update_default_handler_map({_WaveplotDecoy: _AdaptiveWaveplotHandler()}) def _envelope(x, hop): """Compute the max-envelope of non-overlapping frames of x at length hop x is assumed to be multi-channel, of shape (n_channels, n_samples). """ x_frame = np.abs(util.frame(x, frame_length=hop, hop_length=hop)) return x_frame.max(axis=1)
[docs] def waveshow( y: np.ndarray, *, sr: float = 22050, max_points: int = 11025, axis: str | None = "time", offset: float = 0.0, marker: str | MplPath | MarkerStyle = "", where: Literal["pre", "post", "mid"] = "post", label: str | None = None, transpose: bool = False, mask: ArrayLike | None = None, ax: mplaxes.Axes | None = None, invert: bool = False, invert_color: str | tuple | None = None, **kwargs: Any, ) -> AdaptiveWaveplot: """Visualize a waveform in the time domain. This function constructs a plot which adaptively switches between a raw samples-based view of the signal (`matplotlib.pyplot.step`) and an amplitude-envelope view of the signal (`matplotlib.pyplot.fill_between`) depending on the time extent of the plot's viewport. More specifically, when the plot spans a time interval of less than ``max_points / sr`` (by default, 1/2 second), the samples-based view is used, and otherwise a downsampled amplitude envelope is used. This is done to limit the complexity of the visual elements to guarantee an efficient, visually interpretable plot. When using interactive rendering (e.g., in a Jupyter notebook or IPython console), the plot will automatically update as the view-port is changed, either through widget controls or programmatic updates. .. note:: When visualizing stereo waveforms, the amplitude envelope will be generated so that the upper limits derive from the left channel, and the lower limits derive from the right channel, which can produce a vertically asymmetric plot. When zoomed in to the sample view, only the first channel will be shown. If you want to visualize both channels at the sample level, it is recommended to plot each signal independently. To visualize stereo waveforms as two separate signal displays, see `multiplot`. Parameters ---------- y : np.ndarray [shape=(n,) or (2,n)] audio time series (mono or stereo) If stereo, the left channel's amplitude envelope will be used for the top of the plot,, and the right channel's amplitude envelope (negated) will be used for the bottom of the plot. If mono, the signal's envelope is mirrored across the axis. sr : number > 0 [scalar] sampling rate of ``y`` (samples per second) max_points : int > 0 Maximum number of samples to draw. When the plot covers a time extent smaller than ``max_points / sr`` (default: 1/2 second), samples are drawn. If drawing raw samples would exceed `max_points`, then a downsampled amplitude envelope extracted from non-overlapping windows of `y` is visualized instead. The parameters of the amplitude envelope are defined so that the resulting plot cannot produce more than `max_points` frames. axis : str or None Display style of the axis ticks and tick markers. Accepted values are: - 'time' : markers are shown as milliseconds, seconds, minutes, or hours. Values are plotted in units of seconds. - 'h' : markers are shown as hours, minutes, and seconds. - 'm' : markers are shown as minutes and seconds. - 's' : markers are shown as seconds. - 'ms' : markers are shown as milliseconds. - 'lag' : like time, but past the halfway point counts as negative values. - 'lag_h' : same as lag, but in hours. - 'lag_m' : same as lag, but in minutes. - 'lag_s' : same as lag, but in seconds. - 'lag_ms' : same as lag, but in milliseconds. - `None`, 'none', or 'off': ticks and tick markers are hidden. offset : float Offset (in seconds) to start the waveform plot marker : str Marker symbol to use for sample values. (default: no markers) See Also: `matplotlib.markers`. where : {'pre', 'mid', 'post'} This setting determines how both waveform and envelope plots interpolate between observations. See `matplotlib.pyplot.step` for details. Default: 'post' label : str or None The label string applied to this plot. Note that the label transpose : bool If `True`, display the wave vertically instead of horizontally. mask : np.ndarray [shape=(n,)] or None If provided, this mask will be used to determine which samples to display. The mask should be a 1D boolean array of the same length as `y` (`y.shape[-1]`), where `True` indicates that the sample should be displayed, and `False` indicates that it should be ignored. .. note:: This mask is only used directly by the envelope display, and a raw sample display will not be masked. The `mask` parameter is intended to be used by the `wavef0` function, and it is not recommended to be used directly by the user. ax : matplotlib.axes.Axes or None Axes to plot on instead of the default `plt.gca()`. invert : bool If `True`, invert the foreground and background of the display, so that the axes background is colored. If `False` (default), the waveform display is colored and the background is unchanged. .. note:: This option should only be used if the wave display is the only element in the axes. invert_color : str, tuple, None If `invert` is `True`, this parameter specifies the color to use for the inverted waveform display. If `None` (default), the color is set to the current axes background color. **kwargs Additional keyword arguments to `matplotlib.pyplot.fill_between` and `matplotlib.pyplot.step`. Note that only those arguments which are common to both functions will be supported. Returns ------- librosa.display.AdaptiveWaveplot An object of type `librosa.display.AdaptiveWaveplot` See Also -------- wavebars AdaptiveWaveplot multiplot matplotlib.pyplot.step matplotlib.pyplot.fill_between matplotlib.pyplot.fill_betweenx matplotlib.markers Examples -------- Plot a monophonic waveform with an envelope view >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('choice', duration=10) >>> fig, ax = plt.subplots(nrows=3, sharex=True) >>> librosa.display.waveshow(y, sr=sr, ax=ax[0]) >>> ax[0].set(title='Envelope view, mono') >>> ax[0].label_outer() Or a stereo waveform >>> y, sr = librosa.loadx('choice', mono=False, duration=10) >>> librosa.display.waveshow(y, sr=sr, ax=ax[1]) >>> ax[1].set(title='Envelope view, stereo') >>> ax[1].label_outer() Or harmonic and percussive components with transparency >>> y, sr = librosa.loadx('choice', duration=10) >>> y_harm, y_perc = librosa.effects.hpss(y) >>> librosa.display.waveshow(y_harm, sr=sr, color='C1', alpha=0.75, ax=ax[2], label='Harmonic') >>> librosa.display.waveshow(y_perc, sr=sr, color='C2', alpha=0.75, ax=ax[2], label='Percussive') >>> ax[2].set(title='Multiple waveforms') >>> ax[2].legend() >>> plt.show() Zooming in on a plot to show raw sample values >>> fig, (ax, ax2) = plt.subplots(nrows=2, sharex=True) >>> ax.set(xlim=[6.1, 6.25], title='Sample view') >>> librosa.display.waveshow(y, sr=sr, ax=ax, label='Full signal') >>> librosa.display.waveshow(y_harm, sr=sr, color='C1', alpha=0.75, ax=ax2, label='Harmonic') >>> librosa.display.waveshow(y_perc, sr=sr, color='C2', alpha=0.75, ax=ax2, label='Percussive') >>> ax.label_outer() >>> ax.legend() >>> ax2.legend(ncols=2) >>> plt.show() Plotting a transposed wave along with a self-similarity matrix >>> fig, ax = plt.subplot_mosaic("hSSS;hSSS;hSSS;.vvv", layout='compressed') >>> y, sr = librosa.loadx('trumpet') >>> chroma = librosa.feature.chroma_cqt(y=y, sr=sr) >>> sim = librosa.segment.recurrence_matrix(chroma, mode='affinity') >>> librosa.display.specshow(sim, ax=ax['S'], sr=sr, ... x_axis='time', y_axis='time', ... auto_aspect=False) >>> ax['S'].label_outer() >>> ax['S'].sharex(ax['v']) >>> ax['S'].sharey(ax['h']) >>> ax['S'].set(title='Self-similarity') >>> librosa.display.waveshow(y, ax=ax['v']) >>> ax['v'].label_outer() >>> ax['v'].set(title='transpose=False') >>> librosa.display.waveshow(y, ax=ax['h'], transpose=True) >>> ax['h'].label_outer() >>> ax['h'].set(title='transpose=True') >>> plt.show() """ util.valid_audio(y) # Pad an extra channel dimension, if necessary if y.ndim == 1: y = y[np.newaxis, :] if max_points <= 0: raise ParameterError(f"max_points={max_points} must be strictly positive") # Create the adaptive drawing object axes = _check_axes(ax) # Reduce by envelope calculation # this choice of hop ensures that the envelope has at most max_points values hop_length = max(1, y.shape[-1] // max_points) y_env = _envelope(y, hop_length) # Split the envelope into top and bottom y_bottom, y_top = -y_env[-1], y_env[0] times = offset + core.times_like(y, sr=sr, hop_length=1) # Only plot up to max_points worth of data here xdata, ydata = times[:max_points], y[0, :max_points] dec_axis: matplotlib.axis.Axis if transpose: ydata, xdata = xdata, ydata filler = axes.fill_betweenx signal = "ylim_changed" dec_axis = axes.yaxis else: filler = axes.fill_between signal = "xlim_changed" dec_axis = axes.xaxis if mask is not None: mask = cast("Sequence[bool]", np.asarray(mask, dtype=bool)[: len(y_top) * hop_length : hop_length]) (steps,) = axes.step(xdata, ydata, marker=marker, where=where, **kwargs) # Pull color property from the steps object, if we don't already have it if "color" not in kwargs: kwargs.setdefault("color", steps.get_color()) envelope = filler( times[: len(y_top) * hop_length : hop_length], y_bottom, y_top, step=where, where=mask, **kwargs, ) adaptor = AdaptiveWaveplot( times, y[0], steps, envelope, sr=sr, max_samples=max_points, transpose=transpose, label=label, ) # Register adaptor to keep it alive as long as Axes exists bucket = _WAVESHOW_ADAPTORS.get(axes) if bucket is None: bucket = set() _WAVESHOW_ADAPTORS[axes] = bucket bucket.add(adaptor) adaptor.connect(axes, signal=signal) # Force an initial update to ensure the state is consistent adaptor.update(axes) # Handle color inversion if needed if invert: # If no inverted color is given, just swap it from the axes face if invert_color is None: invert_color = axes.patch.get_facecolor() # Get the fg color from the steps plot color = steps.get_color() # Set the axes facecolor to our wave color axes.patch.set_facecolor(color) steps.set_color(invert_color) envelope.set_color(invert_color) # Construct tickers and locators _decorate_axis(dec_axis, axis) return adaptor
[docs] def wavebars( y: np.ndarray, *, sr: float = 22050, n_bars: int = 100, gap_ratio: float = 0.4, rounding_ratio: float = 0.5, axis: str | None = "time", offset: float = 0.0, invert: bool = False, invert_color: str | tuple | None = None, transpose: bool = False, label: str | None = None, ax: mplaxes.Axes | None = None, **patch_kwargs: Any, ) -> mcollections.PatchCollection: """Visualize a waveform as a series of bars representing the amplitude envelope. This visualization is appropriate for displaying a simplified view of the signal, and is best suited for small figures where simplicity is desired. Parameters ---------- y : np.ndarray [shape=(n,) or (2,n)] audio time series (mono or stereo) If stereo, the left channel's amplitude envelope will be used for the top of the bars, and the right channel's amplitude envelope (negated) will be used for the bottom of the bars. If mono, the signal's envelope is mirrored across the axis. sr : number > 0 [scalar] sampling rate of ``y`` (samples per second) n_bars : int > 0 Number of bars to display in the waveform plot. The total time extent of the plot will be divided into `n_bars` segments, and the amplitude envelope of each segment will be represented as a bar. gap_ratio : float in [0, 1] The fraction of the bar width that will be left as a gap between adjacent bars. rounding_ratio : float in [0, 1] The fraction of the bar width that will be used for rounding the corners of the bars. A value of 0.5 will produce bars with rounded corners, while a value of 0 will produce rectangular bars. axis : str or None Display style of the axis ticks and tick markers. Accepted values are: - 'time' : markers are shown as milliseconds, seconds, minutes, or hours. - 'h' : markers are shown as hours, minutes, and seconds. - 'm' : markers are shown as minutes and seconds. - 's' : markers are shown as seconds. - 'ms' : markers are shown as milliseconds. - 'lag' : like time, but past the halfway point counts as negative values. - 'lag_h' : same as lag, but in hours. - 'lag_m' : same as lag, but in minutes. - 'lag_s' : same as lag, but in seconds. - 'lag_ms' : same as lag, but in milliseconds. - `None`, 'none', or 'off': ticks and tick markers are hidden. offset : float Offset (in seconds) to start the waveform plot. invert : bool If `True`, invert the foreground and background of the display, so that the axes background is colored. If `False` (default), the envelope display is colored and the background is unchanged. invert_color : str, tuple, None If `invert` is `True`, this parameter specifies the color to use for the inverted waveform display. If `None` (default), the color is set to the current axes background color. transpose : bool If `True`, display the wave vertically instead of horizontally. label : str or None The label string applied to this plot. If `None`, no label is applied. ax : matplotlib.axes.Axes or None Axes to plot on instead of the default `plt.gca()`. **patch_kwargs : dict Additional keyword arguments to pass to `matplotlib.patches.FancyBboxPatch` Returns ------- matplotlib.collections.PatchCollection A collection of patches representing the amplitude envelope of the waveform. See Also -------- waveshow Examples -------- Plot a waveform as bars, compared to the `waveshow` version of the plot >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('libri1', duration=10) >>> fig, ax = plt.subplots(nrows=2, sharex=True) >>> librosa.display.waveshow(y=y, sr=sr, ax=ax[0], label='waveshow()') >>> ax[0].legend() >>> ax[0].label_outer() >>> librosa.display.wavebars(y=y, sr=sr, ax=ax[1], label='wavebars()') >>> ax[1].legend() >>> plt.show() Make plots with varying amounts of detail, squared corners, and inverted colors. >>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True) >>> librosa.display.wavebars(y=y, sr=sr, n_bars=100, rounding_ratio=0, ... invert=True, ax=ax[0], label='100 bars') >>> librosa.display.wavebars(y=y, sr=sr, n_bars=200, rounding_ratio=0, ... color='C1', invert=True, ax=ax[1], label='200 bars') >>> librosa.display.wavebars(y=y, sr=sr, n_bars=50, rounding_ratio=0, ... color='C2', invert=True, ax=ax[2], label='50 bars') >>> ax[0].legend() >>> ax[1].legend() >>> ax[2].legend() >>> ax[0].label_outer() >>> ax[1].label_outer() >>> plt.show() """ util.valid_audio(y) if y.ndim == 1: y = y[np.newaxis, :] patch_kwargs.setdefault("linewidth", 0) axes = _check_axes(ax) if n_bars <= 0: raise ParameterError(f"n_bars={n_bars} must be strictly positive") hop = max(1, y.shape[-1] // n_bars) env = _envelope(y, hop) env_bottom, env_top = env[-1], env[0] bar_width = (hop / sr) * (1 - gap_ratio) rounding_size = bar_width * rounding_ratio times = offset + core.times_like(env, sr=sr, hop_length=hop) patches = [] boxstyle = f"round,pad=0,rounding_size={rounding_size}" for t, a0, a1 in zip(times, env_bottom, env_top, strict=True): base = min(-rounding_size, -a0) top = max(rounding_size, a1) if transpose: xy, width, height = (base, t), top - base, bar_width else: xy, width, height = (t, base), bar_width, top - base p = mpatches.FancyBboxPatch( xy, width, height, boxstyle=boxstyle, ) patches.append(p) patch_kwargs.setdefault("transform", axes.transData) coll = mcollections.PatchCollection(patches, **patch_kwargs) axes.add_collection(coll) # Create a proxy artist if we have a label to set # Even if we don't have a label, we'll still need it for handling inversion later on proxy = mpatches.FancyBboxPatch((np.nan, np.nan), 1, 1, boxstyle=boxstyle, label=label, **patch_kwargs) proxy.set_in_layout(False) if label is not None: axes.add_patch(proxy) axes.autoscale_view() if invert: # If no inverted color is given, just swap it from the axes face if invert_color is None: invert_color = axes.patch.get_facecolor() # Get the fg color from the steps plot color = coll.get_facecolor()[0] # PatchCollection.get_facecolor() returns a 2D array of shape (1, 4). # Unpack the first row [0] to supply a 1D (4,) RGBA sequence expected # by single Patch artists (axes.patch and proxy) without relying on # implicit downstream array flattening. # Set the axes facecolor to our wave color axes.patch.set_facecolor(color) # type: ignore[arg-type] proxy.set_facecolor(color) # type: ignore[arg-type] coll.set_facecolor(invert_color) if transpose: _decorate_axis(axes.yaxis, axis) else: _decorate_axis(axes.xaxis, axis) return coll
[docs] def wavef0( y: np.ndarray, *, f0: np.ndarray, sr: float = 22050, hop_length: int = 512, bins_per_octave: int = 12, time_axis: str = "time", freq_axis: str = "cqt_note", offset: float = 0.0, key: str = "C:maj", Sa: float | None = None, mela: str | int | None = None, thaat: str | None = None, unicode: bool = True, ax: mplaxes.Axes | None = None, method: str = "waveshow", transpose: bool = False, **kwargs: Any, ) -> AdaptiveWaveplot | mcollections.PatchCollection: """Visualize a waveform with an f0-displacement. This can be used to simultaneously visualize the fundamental frequency (f0) estimates and the waveform or amplitude envelope of an audio signal in one compact display. Parameters ---------- y : np.ndarray [shape=(n,) or (2,n)] audio time series (mono or stereo) If stereo, the left channel's amplitude envelope will be used for the top of the plot, and the right channel's amplitude envelope (negated) will be used for the bottom of the plot. If mono, the signal's envelope is mirrored across the axis. f0 : np.ndarray [shape=(m,)] Fundamental frequency (f0) estimates in Hz. This should be computed using a pitch estimation algorithm such as `librosa.pyin` or `librosa.yin`. sr : number > 0 [scalar] sampling rate of ``y`` (samples per second) hop_length : int > 0 Hop length (in samples) between successive f0 estimates. This should match the hop length used to compute `f0`. bins_per_octave : int > 0 Number of frequency bins per octave used to scale the waveform's amplitude displacement around f0. Combined with the waveform's peak amplitude (used as the displacement norm), this controls how many bins of vertical displacement correspond to one octave above or below f0. time_axis : str Display style of the time axis ticks and tick markers. Accepted values are: - 'time' : markers are shown as milliseconds, seconds, minutes, or hours. - 'h' : markers are shown as hours, minutes, and seconds. - 'm' : markers are shown as minutes and seconds. - 's' : markers are shown as seconds. - 'ms' : markers are shown as milliseconds. - 'lag' : like time, but past the halfway point counts as negative values. - 'lag_h' : same as lag, but in hours. - 'lag_m' : same as lag, but in minutes. - 'lag_s' : same as lag, but in seconds. - 'lag_ms' : same as lag, but in milliseconds. - `None`, 'none', or 'off': ticks and tick markers are hidden. freq_axis : str Display style of the frequency axis ticks and tick markers. Accepted values are: - 'cqt_note' : markers are shown as note names. - 'cqt_hz' : markers are shown as frequencies in Hz. - 'cqt_oct3' : markers are shown in Hz using 1/3-octave intervals. - 'cqt_svara' : markers are shown as Indian classical music svara names. offset : float Offset (in seconds) to start the waveform plot. key : str Key signature for the frequency axis. This is used to determine the note names for the frequency axis when using `cqt_note` mode. Sa : float or None Sa (tonic) frequency in Hz for the frequency axis. Required for `cqt_svara` mode. mela : str or int or None Mela (scale) name or index for the frequency axis. This is used to determine the svara names for the frequency axis when using `cqt_svara` mode. thaat : str or None Thaat (scale) name for the frequency axis. This is used to determine the svara names for the frequency axis when using `cqt_svara` mode. unicode : bool If `True`, use Unicode characters for frequency axis labels. ax : matplotlib.axes.Axes or None Axes to plot on instead of the default `plt.gca()`. method : str Method to use for visualizing the waveform with f0 displacement. Accepted values are: - 'waveshow' : Use `librosa.display.waveshow` to visualize the waveform with an f0 displacement. - 'wavebars' : Use `librosa.display.wavebars` to visualize the waveform as bars with an f0 displacement. transpose : bool If `True`, display the wave vertically instead of horizontally. **kwargs : dict Additional keyword arguments forwarded to the plotting function selected by `method`. If `method='waveshow'`, these must be keyword arguments supported by `librosa.display.waveshow` (for example, `max_points`). If `method='wavebars'`, these must be keyword arguments supported by `librosa.display.wavebars` (for example, `n_bars`, `gap_ratio`, `rounding_ratio`, `invert`, and `invert_color`). Keyword arguments for one method are not valid when using the other. Returns ------- AdaptiveWaveplot or PatchCollection An object of type `librosa.display.AdaptiveWaveplot` if `method='waveshow'`, or a `matplotlib.collections.PatchCollection` if `method='wavebars'`. See Also -------- waveshow wavebars Examples -------- Visualize a waveform with an f0 displacement using `waveshow` >>> import matplotlib.pyplot as plt >>> y, sr = librosa.loadx('trumpet') >>> f0, _, _ = librosa.pyin(y, fmin=librosa.note_to_hz('C2'), ... fmax=librosa.note_to_hz('C7'), ... sr=sr, hop_length=512) >>> fig, ax = plt.subplots() >>> librosa.display.wavef0(y=y, f0=f0, sr=sr, ax=ax, ... method='waveshow') >>> ax.set(title='Waveform with f0 displacement (waveshow)') >>> plt.show() Visualize a waveform with an f0 displacement using `wavebars`, and Hz labels instead of note names. Using a larger number of bars shows more detail here. >>> fig, ax = plt.subplots() >>> librosa.display.wavef0(y=y, f0=f0, sr=sr, ax=ax, n_bars=256, ... method='wavebars', freq_axis='cqt_hz') >>> ax.set(title='Waveform with f0 displacement (wavebars, cqt_hz)') >>> plt.show() Overlay a displaced waveform on a CQT plot via `specshow`. >>> fig, ax = plt.subplots() >>> C = librosa.cqt(y, sr=sr) >>> librosa.display.specshow(C, ax=ax, sr=sr, x_axis='time', y_axis='cqt_note', ... vscale='dBFS', cmap='gray_r') >>> hl = librosa.display.highlight(ax=ax) >>> librosa.display.wavef0(y=y, f0=f0, sr=sr, ax=ax, path_effects=hl) """ # Create the adaptive drawing object axes = _check_axes(ax) if method not in ("waveshow", "wavebars"): raise ParameterError(f"Invalid display method={method}.") # Force norm to be strictly positive and handle empty arrays norm = float(util.tiny(y)) if y.size > 0: norm += max(y.max(), -y.min()) trans = Transformf0( f0, sr=sr, hop_length=hop_length, bins_per_octave=bins_per_octave, norm=norm, offset=offset, transpose=transpose, ) # and transposed mode here if transpose: _decorate_axis( axes.xaxis, freq_axis, key=key, Sa=Sa, mela=mela, thaat=thaat, unicode=unicode, ) else: _decorate_axis( axes.yaxis, freq_axis, key=key, Sa=Sa, mela=mela, thaat=thaat, unicode=unicode, ) if method == "waveshow": times = offset + np.arange(y.shape[-1]) / sr mask = np.isfinite(trans.f0_interp(times)) adaptor = waveshow( y=y, sr=sr, axis=time_axis, offset=offset, mask=mask, ax=axes, transform=trans + axes.transData, transpose=transpose, **kwargs, ) # Kludge the data limits because the fill_between collection does not automatically # update the data limits assert adaptor.envelope is not None xy = adaptor.envelope.get_datalim(trans + axes.transData).get_points() f0min = np.nanmin(f0) f0max = np.nanmax(f0) if transpose: handle = mlines.Line2D([xy[0, 0] + f0min, xy[1, 0] + f0max], xy[:, 1]) else: handle = mlines.Line2D(xy[:, 0], [xy[0, 1] + f0min, xy[1, 1] + f0max]) axes.add_line(handle) axes.autoscale_view() handle.remove() # end kludge return adaptor else: return wavebars( y=y, sr=sr, axis=time_axis, offset=offset, ax=axes, transform=trans + axes.transData, transpose=transpose, **kwargs, )