Skip to content

Analysis

General-purpose signal and statistics utilities for analysing rhythmic and periodic structure in motion and audio signals.

These helpers are independent of the MgVideo/MgAudio classes and can be used on any 1-D numpy signal (e.g. quantity-of-motion curves, body-part speeds, audio onset envelopes).

smooth

smooth(x, w=5)

Smooth a 1-D signal with a moving average.

Parameters:

Name Type Description Default
x ndarray

Input signal.

required
w int

Window size in samples. Defaults to 5.

5

Returns:

Type Description

np.ndarray: Smoothed signal of the same length as the input.

Source code in musicalgestures/_analysis.py
13
14
15
16
17
18
19
20
21
22
23
24
25
def smooth(x, w=5):
    """
    Smooth a 1-D signal with a moving average.

    Args:
        x (np.ndarray): Input signal.
        w (int, optional): Window size in samples. Defaults to 5.

    Returns:
        np.ndarray: Smoothed signal of the same length as the input.
    """
    from scipy.ndimage import uniform_filter1d
    return uniform_filter1d(np.asarray(x, dtype=float), size=w)

bandpass

bandpass(signal, lo, hi, fs, order=4)

Apply a zero-phase Butterworth band-pass filter to a signal.

Parameters:

Name Type Description Default
signal ndarray

Input signal.

required
lo float

Lower cutoff frequency (Hz).

required
hi float

Upper cutoff frequency (Hz).

required
fs float

Sampling rate of the signal (Hz).

required
order int

Filter order. Defaults to 4.

4

Returns:

Type Description

np.ndarray: The filtered signal. Returns the input unchanged if the requested band is invalid for the given sampling rate.

Source code in musicalgestures/_analysis.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def bandpass(signal, lo, hi, fs, order=4):
    """
    Apply a zero-phase Butterworth band-pass filter to a signal.

    Args:
        signal (np.ndarray): Input signal.
        lo (float): Lower cutoff frequency (Hz).
        hi (float): Upper cutoff frequency (Hz).
        fs (float): Sampling rate of the signal (Hz).
        order (int, optional): Filter order. Defaults to 4.

    Returns:
        np.ndarray: The filtered signal. Returns the input unchanged if the
            requested band is invalid for the given sampling rate.
    """
    from scipy.signal import butter, filtfilt
    signal = np.asarray(signal, dtype=float)
    nyq = fs / 2
    lo, hi = max(lo, 0.01), min(hi, nyq - 0.01)
    if lo >= hi:
        return signal
    b, a = butter(order, [lo / nyq, hi / nyq], btype="band")
    return filtfilt(b, a, signal)

dominant_frequency

dominant_frequency(signal, fps, fmin=0.5, fmax=8.0)

Find the dominant frequency of a signal within a frequency band using the FFT.

Useful for estimating, e.g., the dominant oscillation rate of a body part's speed signal (steps per second in a dance).

Parameters:

Name Type Description Default
signal ndarray

Input signal.

required
fps float

Sampling rate of the signal (Hz, e.g. frames per second).

required
fmin float

Lowest frequency to consider (Hz). Defaults to 0.5.

0.5
fmax float

Highest frequency to consider (Hz). Defaults to 8.0.

8.0

Returns:

Name Type Description
float

The dominant frequency (Hz) within [fmin, fmax], or 0.0 if the band contains no frequency bins.

Source code in musicalgestures/_analysis.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def dominant_frequency(signal, fps, fmin=0.5, fmax=8.0):
    """
    Find the dominant frequency of a signal within a frequency band using the FFT.

    Useful for estimating, e.g., the dominant oscillation rate of a body part's
    speed signal (steps per second in a dance).

    Args:
        signal (np.ndarray): Input signal.
        fps (float): Sampling rate of the signal (Hz, e.g. frames per second).
        fmin (float, optional): Lowest frequency to consider (Hz). Defaults to 0.5.
        fmax (float, optional): Highest frequency to consider (Hz). Defaults to 8.0.

    Returns:
        float: The dominant frequency (Hz) within [fmin, fmax], or 0.0 if the
            band contains no frequency bins.
    """
    signal = np.asarray(signal, dtype=float)
    freqs = np.fft.rfftfreq(len(signal), d=1.0 / fps)
    fft = np.abs(np.fft.rfft(signal - signal.mean()))
    mask = (freqs >= fmin) & (freqs <= fmax)
    if not mask.any():
        return 0.0
    return float(freqs[mask][np.argmax(fft[mask])])

circular_stats

circular_stats(phases)

Compute circular mean direction and resultant vector length of a set of phases.

Parameters:

Name Type Description Default
phases ndarray

Phase angles in radians.

required

Returns:

Name Type Description
tuple

(R, mean_angle_deg) where R is the mean resultant length in [0, 1] (1 = perfectly concentrated, 0 = uniform) and mean_angle_deg is the mean direction in degrees [0, 360).

Source code in musicalgestures/_analysis.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def circular_stats(phases):
    """
    Compute circular mean direction and resultant vector length of a set of phases.

    Args:
        phases (np.ndarray): Phase angles in radians.

    Returns:
        tuple: ``(R, mean_angle_deg)`` where ``R`` is the mean resultant length
            in [0, 1] (1 = perfectly concentrated, 0 = uniform) and
            ``mean_angle_deg`` is the mean direction in degrees [0, 360).
    """
    phases = np.asarray(phases, dtype=float)
    C, S = np.cos(phases).mean(), np.sin(phases).mean()
    R = float(np.sqrt(C ** 2 + S ** 2))
    mean_angle = float(np.degrees(np.arctan2(S, C)) % 360)
    return R, mean_angle

rayleigh_test

rayleigh_test(phases)

Rayleigh test for non-uniformity of circular data.

Tests the null hypothesis that the phases are uniformly distributed around the circle. A small p-value indicates significant phase concentration (i.e. consistent timing).

Parameters:

Name Type Description Default
phases ndarray

Phase angles in radians.

required

Returns:

Name Type Description
tuple

(Z, p) where Z is the Rayleigh statistic and p is the approximate p-value.

Source code in musicalgestures/_analysis.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def rayleigh_test(phases):
    """
    Rayleigh test for non-uniformity of circular data.

    Tests the null hypothesis that the phases are uniformly distributed around
    the circle. A small p-value indicates significant phase concentration
    (i.e. consistent timing).

    Args:
        phases (np.ndarray): Phase angles in radians.

    Returns:
        tuple: ``(Z, p)`` where ``Z`` is the Rayleigh statistic and ``p`` is the
            approximate p-value.
    """
    phases = np.asarray(phases, dtype=float)
    n = len(phases)
    if n == 0:
        return 0.0, 1.0
    R, _ = circular_stats(phases)
    Z = n * R ** 2
    p = float(np.exp(-Z))
    return float(Z), p

synchrony

synchrony(signal_a, signal_b, times_a=None, times_b=None)

Pearson correlation between two signals after alignment and normalisation.

If time vectors are supplied, signal_b is linearly resampled onto the time base of signal_a before correlating. Both signals are min-max normalised to [0, 1]. Useful for quantifying audio–motion synchrony (e.g. audio onset strength vs. overall motion energy).

Parameters:

Name Type Description Default
signal_a ndarray

First signal (reference time base).

required
signal_b ndarray

Second signal.

required
times_a ndarray

Time stamps for signal_a. Defaults to None.

None
times_b ndarray

Time stamps for signal_b. Defaults to None.

None

Returns:

Name Type Description
float

Pearson correlation coefficient in [-1, 1].

Source code in musicalgestures/_analysis.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def synchrony(signal_a, signal_b, times_a=None, times_b=None):
    """
    Pearson correlation between two signals after alignment and normalisation.

    If time vectors are supplied, ``signal_b`` is linearly resampled onto the
    time base of ``signal_a`` before correlating. Both signals are min-max
    normalised to [0, 1]. Useful for quantifying audio–motion synchrony (e.g.
    audio onset strength vs. overall motion energy).

    Args:
        signal_a (np.ndarray): First signal (reference time base).
        signal_b (np.ndarray): Second signal.
        times_a (np.ndarray, optional): Time stamps for ``signal_a``. Defaults to None.
        times_b (np.ndarray, optional): Time stamps for ``signal_b``. Defaults to None.

    Returns:
        float: Pearson correlation coefficient in [-1, 1].
    """
    a = np.asarray(signal_a, dtype=float)
    b = np.asarray(signal_b, dtype=float)

    if times_a is not None and times_b is not None:
        b = np.interp(times_a, times_b, b)
    elif len(a) != len(b):
        n = min(len(a), len(b))
        a, b = a[:n], b[:n]

    def _norm01(x):
        return (x - x.min()) / (x.max() - x.min() + 1e-9)

    return float(np.corrcoef(_norm01(a), _norm01(b))[0, 1])