Skip to content

Swing

swing

This module implements various functions useful for analyzing swing in jazz and other genres.

Author: Huw Cheston (2025)

References
  • Benadon, F. (2006). Slicing the Beat: Jazz Eighth-Notes as Expressive Microrhythm. Ethnomusicology, 50(1), 73–98. https://doi.org/10.2307/20174424

  • Corcoran, C., & Frieler, K. (2021). Playing It Straight: Analyzing Jazz Soloists’ Swing Eighth-Note Distributions with the Weimar Jazz Database. Music Perception, 38(4), 372–385. https://doi.org/10.1525/mp.2021.38.4.372


beat_upbeat_ratio

beat_upbeat_ratio(
    beats: Iterable[float],
    upbeats: Iterable[float],
    log2: bool = False,
    bounded: bool = False,
    lower_bound: float = LOW_BUR,
    upper_bound: float = HIGH_BUR,
) -> list[float]

Extracts beat-upbeat ratio (BUR) values from an array of beats and upbeats.

The beat-upbeat ratio (BUR) is used to analyze the amount of “swing” in two consecutive eighth-note beat durations. It is calculated by dividing the duration of the first (“long”) eighth-note beat by the duration of the second (“short”) eighth-note beat. A BUR value of 2 represents “perfect” swing (e.g., a triplet quarter note followed by a triplet eighth note), while a BUR of 1 represents “even” eighth-note durations.

The formula for BUR is:

$\text{BUR} = \frac{t_{a,b} - t_{a}}{t_{b} - t_{a,b}}$, where

$t_a$ is the beat at position $a$, $t_b$ is the beat at position $b$, and $t_{a,b}$ is the single upbeat between beats $a$ and $b$.

The function takes two iterables of timestamps: beats and upbeats. Both lists should be unique, and missing values should not be present. The function returns an array of BUR values with a size of len(beats) - 1. If multiple upbeats are found between two consecutive beats or if no upbeat is found, the BUR for those beats will be omitted and the corresponding value will be None.

Additionally, the function can calculate the $log_2$ of the BUR values, where a value of 1.0 corresponds to “triplet” swing. This can be enabled by setting log2=True. The values can also be filtered to remove outliers by setting bounded=True, with the default values for the boundaries coming from Corcoran & Frieler (2021).

Parameters:

  • beats (Iterable[float]) –

    An array of beat timestamps. Should not overlap with upbeats.

  • upbeats (Iterable[float]) –

    An array of upbeat timestamps.

  • log2 (bool, default: False ) –

    If True, computes the log base 2 of BUR values, as used in [2]. Defaults to False.

  • bounded (bool, default: False ) –

    If True, filters out BUR values outside the specified range. Defaults to False.

  • lower_bound (float, default: LOW_BUR ) –

    Lower boundary for filtering BUR values. Defaults to 0.25 ($log_2$ value of -2).

  • upper_bound (float, default: HIGH_BUR ) –

    Upper boundary for filtering BUR values. Defaults to 4.0 ($log_2$ value of 2).

Returns:

  • list[float]

    A list of the calculated BUR values.

Examples:

>>> my_beats = [0., 1., 2., 3.]
>>> my_upbeats = [0.5, 1.75, 2.2]
>>> beat_upbeat_ratio(my_beats, my_upbeats)
[1., 3., 0.25]
>>> # Consecutive beats without a matching upbeat will be skipped.
>>> my_beats = [0., 1., 2., 3.]
>>> my_upbeats = [0.5, 2.2]
>>> beat_upbeat_ratio(my_beats, my_upbeats)
[1., None, 0.25]
>>> # Consecutive beats with multiple matching upbeats will be skipped.
>>> my_beats = [0., 1., 2., 3.]
>>> my_upbeats = [0.5, 1.5, 1.75, 1.8, 2.2]
>>> beat_upbeat_ratio(my_beats, my_upbeats)
[1., None, 0.25]
>>> # Filter out outlying values by setting `bounded=True`.
>>> my_beats = [0., 1., 2., 3.]
>>> my_upbeats = [0.5, 1.75, 2.99]
>>> beat_upbeat_ratio(my_beats, my_upbeats, bounded=True)
[1., 3., None]
Source code in amads/time/swing.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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 beat_upbeat_ratio(
    beats: Iterable[float],
    upbeats: Iterable[float],
    log2: bool = False,
    bounded: bool = False,
    lower_bound: float = LOW_BUR,
    upper_bound: float = HIGH_BUR,
) -> list[float]:
    r"""
    Extracts beat-upbeat ratio (BUR) values from an array of beats and upbeats.

    The beat-upbeat ratio (BUR) is used to analyze the amount of “swing” in two
    consecutive eighth-note beat durations. It is calculated by dividing the
    duration of the first (“long”) eighth-note beat by the duration of the
    second (“short”) eighth-note beat. A BUR value of 2 represents “perfect”
    swing (e.g., a triplet quarter note followed by a triplet eighth note),
    while a BUR of 1 represents “even” eighth-note durations.

    The formula for BUR is:

    $\text{BUR} = \frac{t_{a,b} - t_{a}}{t_{b} - t_{a,b}}$, where

    $t_a$ is the beat at position $a$, $t_b$ is the beat at position $b$,
    and $t_{a,b}$ is the single upbeat between beats $a$ and $b$.

    The function takes two iterables of timestamps: `beats` and `upbeats`.
    Both lists should be unique, and missing values should not be present.
    The function returns an array of BUR values with a size of
    `len(beats) - 1`. If multiple `upbeats` are found between two
    consecutive `beats` or if no `upbeat` is found, the BUR for those
    beats will be omitted and the corresponding value will be `None`.

    Additionally, the function can calculate the $log_2$ of the
    BUR values, where a value of 1.0 corresponds to “triplet” swing.
    This can be enabled by setting `log2=True`. The values can also
    be filtered to remove outliers by setting `bounded=True`, with
    the default values for the boundaries coming from Corcoran &
    Frieler (2021).

    Parameters
    ----------
    beats : Iterable[float]
        An array of beat timestamps. Should not overlap with `upbeats`.
    upbeats : Iterable[float]
        An array of upbeat timestamps.
    log2 : bool, optional
        If True, computes the log base 2 of BUR values, as used in [2].
        Defaults to False.
    bounded : bool, optional
        If True, filters out BUR values outside the specified range.
        Defaults to False.
    lower_bound : float, optional
        Lower boundary for filtering BUR values. Defaults to 0.25
        ($log_2$ value of -2).
    upper_bound : float, optional
        Upper boundary for filtering BUR values. Defaults to 4.0 ($log_2$
        value of 2).

    Returns
    -------
    list[float]
        A list of the calculated BUR values.

    Examples
    --------
    >>> my_beats = [0., 1., 2., 3.]
    >>> my_upbeats = [0.5, 1.75, 2.2]
    >>> beat_upbeat_ratio(my_beats, my_upbeats)
    [1., 3., 0.25]

    >>> # Consecutive beats without a matching upbeat will be skipped.
    >>> my_beats = [0., 1., 2., 3.]
    >>> my_upbeats = [0.5, 2.2]
    >>> beat_upbeat_ratio(my_beats, my_upbeats)
    [1., None, 0.25]

    >>> # Consecutive beats with multiple matching upbeats will be skipped.
    >>> my_beats = [0., 1., 2., 3.]
    >>> my_upbeats = [0.5, 1.5, 1.75, 1.8, 2.2]
    >>> beat_upbeat_ratio(my_beats, my_upbeats)
    [1., None, 0.25]

    >>> # Filter out outlying values by setting `bounded=True`.
    >>> my_beats = [0., 1., 2., 3.]
    >>> my_upbeats = [0.5, 1.75, 2.99]
    >>> beat_upbeat_ratio(my_beats, my_upbeats, bounded=True)
    [1., 3., None]

    """

    # Parse beats and upbeats to an array, sorting as required
    beats = np.sort(np.array(beats))
    upbeats = np.sort(np.array(upbeats))
    # Validate inputs and raise an errors if required
    _validate_bur_inputs(beats, upbeats)
    # Match beats with upbeats and return a 2d array of shape
    #    [[beat1, upbeat, beat2], [beat2, upbeat, beat3]]
    matched = match_beats_and_upbeats(beats, upbeats)
    # Raise an error if we cannot find any matches between beats and upbeats
    if all([np.isnan(i) for i in matched[:, 1]]):
        raise ValueError(
            "No matches found between beats and upbeats, cannot calculate BUR"
        )
    # Calculate the BUR for upbeats between consecutive beats
    #
    # Use isitem() because Python 3.10 will print np.float64(...) instead of ...
    # when the value is a numpy float:
    burs = [
        _bur(b1, upbeat, b2).item() if not np.isnan(upbeat) else None
        for b1, upbeat, b2 in matched
    ]
    # Apply our filtering if required
    # Filter before log_2 transform to make things simpler
    if bounded:
        burs = [i if lower_bound < i < upper_bound else None for i in burs]
    # Express as base-2 log if required
    if log2:
        burs = [log2_(b) if b is not None else None for b in burs]
    return burs

mean_bur

mean_bur(
    beats: Iterable[float], upbeats: Iterable[float], **kwargs
) -> float

Calculates mean BUR (or $log_2$ BUR) given a list of beats and upbeats

Source code in amads/time/swing.py
156
157
158
159
160
161
162
163
164
165
def mean_bur(
    beats: Iterable[float], upbeats: Iterable[float], **kwargs
) -> float:
    """Calculates mean BUR (or $log_2$ BUR) given a list of beats and upbeats"""
    # We use nanmean here as we may have null values in cases where
    # multiple upbeats match with a single pair of beats, or where
    # no upbeats match with a beat.
    #     I think this makes sense to avoid the user having to chop a
    # large list into multiple sublists depending on the presence of nans.
    return float(np.nanmean(beat_upbeat_ratio(beats, upbeats, **kwargs)))

std_bur

std_bur(
    beats: Iterable[float], upbeats: Iterable[float], **kwargs
) -> float

Calculates standard deviation BUR (or $log_2$ BUR) given a list of beats and upbeats

Source code in amads/time/swing.py
168
169
170
171
172
def std_bur(
    beats: Iterable[float], upbeats: Iterable[float], **kwargs
) -> float:
    """Calculates standard deviation BUR (or $log_2$ BUR) given a list of beats and upbeats"""
    return np.nanstd(beat_upbeat_ratio(beats, upbeats, **kwargs))

match_beats_and_upbeats

match_beats_and_upbeats(beats: ndarray, upbeats: ndarray) -> ndarray

Iterates over consecutive beats and creates an array of [[beat1, upbeat, beat2], [beat2, upbeat, beat3]]

Source code in amads/time/swing.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def match_beats_and_upbeats(
    beats: np.ndarray, upbeats: np.ndarray
) -> np.ndarray:
    """Iterates over consecutive beats and creates an array of `[[beat1, upbeat, beat2], [beat2, upbeat, beat3]]`"""

    matched = []
    # Iterate over consecutive pairs of beats
    for b1, b2 in zip(beats, beats[1:]):
        # Get the upbeats that are between both pairs of beats
        upbeat_bw = upbeats[(b1 < upbeats) & (upbeats < b2)]
        # Add a missing value in cases where multiple upbeats match with a single pair of beats, or no upbeats match
        #  We do not consider cases where multiple upbeats match with a single beat, as these are not "swing 8ths"
        #  Adding a missing value means that we'll have the shape len(beats) - 1
        if len(upbeat_bw) > 1 or len(upbeat_bw) == 0:
            matched.append([b1, None, b2])
        # This will only catch cases where we have matched a single upbeat
        else:
            matched.append([b1, upbeat_bw[0], b2])
    # Create a 2d array of [[beat1, upbeat, beat2], [beat2, upbeat, beat3]]
    return np.array(matched, dtype=float)