Skip to content

MgList

MgList

MgList(*objectlist)

Class for handling lists of MgImage, MgFigure and MgList objects in the Musical Gestures Toolbox.

Attributes:

Name Type Description
- *objectlist objects and/or list(s) of objects

MgObjects and/or MgImages to include in the list.

Initializes the MgList object.

Parameters:

Name Type Description Default
*objectlist MgImage / MgFigure / MgList

All the objects to include in the MgList.

()
Source code in musicalgestures/_mglist.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(self, *objectlist):
    """
    Initializes the MgList object.

    Args:
        *objectlist (MgImage/MgFigure/MgList): All the objects to include in the MgList.
    """

    def crawler(l):
        """
        Helper function to flatten all arguments into a single list.

        Args:
            l (list): The list (or list of lists) of objects.

        Returns:
            list: The flattened list.
        """
        _tmp = []
        for elem in l:
            if type(elem) == list:
                _tmp += crawler(elem)
            else:
                _tmp.append(elem)
        return _tmp

    self.objectlist = crawler(objectlist)

show

show(filename=None, key=None, mode='windowed', window_width=640, window_height=480, window_title=None)

Display the objects in the MgList.

By default every item is shown. The keys 'horizontal' and 'vertical' select a single panel, e.g. mv.motiongrams().show(key='horizontal'). The aliases 'mgh'/'vgh' (horizontal) and 'mgv'/'vgv' (vertical) work too, as do the legacy 'mgx'/'vgx' and 'mgy'/'vgy' (the literal x/y files). (The key identifies which item in the list to show — it is not forwarded to the individual images.)

Source code in musicalgestures/_mglist.py
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
def show(self, filename=None, key=None, mode='windowed', window_width=640, window_height=480, window_title=None):
    """
    Display the objects in the MgList.

    By default every item is shown. The keys ``'horizontal'`` and ``'vertical'`` select a
    single panel, e.g. ``mv.motiongrams().show(key='horizontal')``. The aliases 'mgh'/'vgh'
    (horizontal) and 'mgv'/'vgv' (vertical) work too, as do the legacy 'mgx'/'vgx' and
    'mgy'/'vgy' (the literal x/y files). (The key identifies *which item in the list*
    to show — it is not forwarded to the individual images.)
    """
    # The MgList items are already the concrete images, so a key selects an item
    # rather than being passed down (an MgImage can't resolve these itself).
    # Item order is (x, y); horizontal movement = the y file, vertical = the x file.
    key_to_index = {'horizontal': 1, 'vertical': 0,
                    'mgh': 1, 'vgh': 1, 'mgv': 0, 'vgv': 0,
                    'mgx': 0, 'vgx': 0, 'mgy': 1, 'vgy': 1}
    if key is not None and key.lower() in key_to_index:
        idx = key_to_index[key.lower()]
        if 0 <= idx < len(self.objectlist):
            self.objectlist[idx].show(mode=mode, window_width=window_width,
                                      window_height=window_height, window_title=window_title)
        else:
            print(f"This MgList has no item for key '{key}'.")
        return

    for obj in self.objectlist:
        if type(obj) != MgFigure:
            obj.show(mode=mode, window_width=window_width,
                     window_height=window_height, window_title=window_title)
        else:
            obj.show()

__len__

__len__()

Implements len().

Returns:

Name Type Description
int

The length of the MgList.

Source code in musicalgestures/_mglist.py
78
79
80
81
82
83
84
85
def __len__(self):
    """
    Implements `len()`.

    Returns:
        int: The length of the MgList.
    """
    return len(self.objectlist)

__getitem__

__getitem__(key)

Implements getting elements given an index from the MgList.

Parameters:

Name Type Description Default
key int

The index of the element to retrieve.

required

Returns:

Type Description

MgImage/MgFigure/MgList: The element at key.

Source code in musicalgestures/_mglist.py
87
88
89
90
91
92
93
94
95
96
97
def __getitem__(self, key):
    """
    Implements getting elements given an index from the MgList.

    Args:
        key (int): The index of the element to retrieve.

    Returns:
        MgImage/MgFigure/MgList: The element at `key`.
    """
    return self.objectlist[key]

__setitem__

__setitem__(key, value)

Implements setting elements given an index from the MgList.

Parameters:

Name Type Description Default
key int

The index of the element to change.

required
value MgImage / MgFigure / MgList

The element to place at key.

required
Source code in musicalgestures/_mglist.py
 99
100
101
102
103
104
105
106
107
def __setitem__(self, key, value):
    """
    Implements setting elements given an index from the MgList.

    Args:
        key (int): The index of the element to change.
        value (MgImage/MgFigure/MgList): The element to place at `key`.
    """
    self.objectlist[key] = value

__delitem__

__delitem__(key)

Implements deleting elements given an index from the MgList.

Parameters:

Name Type Description Default
key int

The index of the element to delete.

required
Source code in musicalgestures/_mglist.py
109
110
111
112
113
114
115
116
def __delitem__(self, key):
    """
    Implements deleting elements given an index from the MgList.

    Args:
        key (int): The index of the element to delete.
    """
    del self.objectlist[key]

__iter__

__iter__()

Implements iter().

Returns:

Name Type Description
iterator

The iterator of self.objectlist.

Source code in musicalgestures/_mglist.py
118
119
120
121
122
123
124
125
def __iter__(self):
    """
    Implements `iter()`.

    Returns:
        iterator: The iterator of `self.objectlist`.
    """
    return iter(self.objectlist)

__iadd__

__iadd__(other)

Implements +=.

Parameters:

Name Type Description Default
other MgImage / MgFigure / MgList

The object(s) to add to the MgList.

required

Returns:

Name Type Description
MgList

The incremented MgList.

Source code in musicalgestures/_mglist.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def __iadd__(self, other):
    """
    Implements `+=`.

    Args:
        other (MgImage/MgFigure/MgList): The object(s) to add to the MgList.

    Returns:
        MgList: The incremented MgList.
    """
    if type(other) == MgList:
        self.objectlist += other.objectlist
    elif type(other) == list:
        for ind, elem in enumerate(other):
            if type(elem) in [MgList, MgImage, MgFigure]:
                self.objectlist.append(elem)
            else:
                raise TypeError(f'Incompatible object type {type(elem)} at index {ind}. Expected MgImage, MgFigure, or MgList.')
    elif type(other) in [MgList, MgImage, MgFigure]:
        self.objectlist.append(other)
    else:
        raise TypeError(f'Incompatible object type {type(other)}. Expected MgImage, MgFigure, or MgList.')
    return MgList(self.objectlist)

__add__

__add__(other)

Implements +.

Parameters:

Name Type Description Default
other MgImage / MgFigure / MgList

The object(s) to add to the MgList.

required

Returns:

Name Type Description
MgList

The incremented MgList.

Source code in musicalgestures/_mglist.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def __add__(self, other):
    """
    Implements `+`.

    Args:
        other (MgImage/MgFigure/MgList): The object(s) to add to the MgList.

    Returns:
        MgList: The incremented MgList.
    """
    if type(other) == MgList:
        return MgList(self.objectlist + other.objectlist)
    elif type(other) == list:
        _tmp_list = []
        _tmp_list += self.objectlist
        for ind, elem in enumerate(other):
            if type(elem) in [MgList, MgImage, MgFigure]:
                _tmp_list.append(elem)
            else:
                raise TypeError(f'Incompatible object type {type(elem)} at index {ind}. Expected MgImage, MgFigure, or MgList.')
        return MgList(_tmp_list)
    elif type(other) in [MgList, MgImage, MgFigure]:
        return MgList(self.objectlist + [other])
    else:
        raise TypeError(f'Incompatible object type {type(other)}. Expected MgImage, MgFigure, or MgList.')

as_figure

as_figure(dpi=300, autoshow=True, title=None, export_png=True)

Creates a time-aligned figure from all the elements in the MgList.

Parameters:

Name Type Description Default
dpi int

Image quality of the rendered figure in DPI. Defaults to 300.

300
autoshow bool

Whether to show the resulting figure automatically. Defaults to True.

True
title str

Optionally add a title to the figure. Defaults to None (no title).

None
export_png bool

Whether to export a png image of the resulting figure automatically. Defaults to True.

True

Returns:

Name Type Description
MgFigure

The MgFigure with all the elements from the MgList as layers.

Source code in musicalgestures/_mglist.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
def as_figure(self, dpi=300, autoshow=True, title=None, export_png=True):
    """
    Creates a time-aligned figure from all the elements in the MgList.

    Args:
        dpi (int, optional): Image quality of the rendered figure in DPI. Defaults to 300.
        autoshow (bool, optional): Whether to show the resulting figure automatically. Defaults to True.
        title (str, optional): Optionally add a title to the figure. Defaults to None (no title).
        export_png (bool, optional): Whether to export a png image of the resulting figure automatically. Defaults to True.

    Returns:
        MgFigure: The MgFigure with all the elements from the MgList as layers.
    """
    import os
    import librosa
    import librosa.display
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
    import matplotlib
    import numpy as np

    there_were_layers, first_slot_was_img, img_to_redo = None, None, None

    def count_elems(elems_list, elem_count):
        """
        Counts the all elements in a list recursively.

        Args:
            elems_list (list): The list to count.
            elem_count (int): The current count. (Pass 0 on the top level.)

        Returns:
            int: The number of elements in `elems_list`.
        """
        _count = elem_count

        for obj in elems_list:
            if type(obj) == MgImage:
                _count += 1

            elif type(obj) == MgFigure:
                if obj.figure_type == 'audio.tempogram':
                    _count += 2
                elif obj.figure_type == 'audio.descriptors':
                    _count += 3
                elif obj.figure_type == 'audio.spectrogram':
                    _count += 1
                elif obj.figure_type == 'audio.waveform':
                    _count += 1
                elif obj.figure_type == 'layers':
                    _count = count_elems(obj.layers, _count)

            elif type(obj) == MgList:
                _count = count_elems(obj.objectlist, _count)

        return _count

    elem_count = count_elems(self.objectlist, 0)

    def build_figure(elems_list, elem_count, fig, ax, index_of_first_plot, plot_counter, of):
        """
        Recursively crawls through the list of objects, and builds a single top-level figure from them.

        Args:
            elems_list (list): List of MgImage, MgFigure or MgList objects.
            elem_count (int): The total number of subplots to make.
            fig (matplotlib.pyplot.figure): The figure to fill.
            ax (list): The list of subplots (or their placeholders).
            index_of_first_plot (int): The index of the first plot.
            plot_counter (int): The running count of subplots (increments while crawling through all levels and building layers).
            of (str): The "running" string for the final output file name (each subplot increments it). 

        Returns:
            str: The final output file name in the current level.
            int: The final count of subplots including the current level.
            bool: Whether there were deeper levels inside the current one.
            bool: Whether the first slot in the figure will come from an MgImage.
            str: The path to the image from the MgImage on the first slot.
        """

        there_were_layers, first_slot_was_img, img_to_redo = None, None, None

        for obj in elems_list:
            if type(obj) == MgImage:
                if plot_counter == 0:
                    first_slot_was_img = True
                    img_to_redo = obj.filename
                ax[plot_counter] = fig.add_subplot(
                    elem_count, 1, plot_counter+1)
                ax[plot_counter].imshow(mpimg.imread(obj.filename))
                ax[plot_counter].set_aspect('auto')
                ax[plot_counter].axes.xaxis.set_visible(False)
                ax[plot_counter].axes.yaxis.set_visible(False)

                # add title based on content
                last_tag = os.path.splitext(obj.filename)[0].split('_')[-1]
                tag_titles = {'mgh': 'Horizontal Motiongram', 'mgv': 'Vertical Motiongram',
                              'vgh': 'Horizontal Videogram', 'vgv': 'Vertical Videogram',
                              'mgx': 'Vertical Motiongram', 'mgy': 'Horizontal Motiongram',
                              'vgx': 'Vertical Videogram', 'vgy': 'Horizontal Videogram'}
                if last_tag in tag_titles:
                    ax[plot_counter].set(title=tag_titles[last_tag])
                else:
                    ax[plot_counter].set(
                        title=os.path.basename(obj.filename))

                # increment output filename
                if plot_counter == 0:
                    of = os.path.splitext(obj.filename)[0]
                else:
                    of += '_'
                    of += os.path.splitext(obj.filename)[0].split('_')[-1]

                plot_counter += 1

            elif type(obj) == MgFigure:
                first_plot = False
                if index_of_first_plot is None:
                    index_of_first_plot = plot_counter  # 0-based!
                    first_plot = True

                if obj.figure_type == 'audio.tempogram':
                    # increment output filename
                    if plot_counter == 0:
                        of = obj.data['of'] + '_tempogram'
                    else:
                        of += '_tempogram'

                    if first_plot:
                        ax[plot_counter] = fig.add_subplot(
                            elem_count, 1, plot_counter+1)
                    else:
                        ax[plot_counter] = fig.add_subplot(
                            elem_count, 1, plot_counter+1, sharex=ax[index_of_first_plot])

                    # make plot for onset strength
                    ax[plot_counter].plot(
                        obj.data['times'], obj.data['onset_env'], label='Onset strength')
                    ax[plot_counter].label_outer()
                    ax[plot_counter].legend(frameon=True)
                    plot_counter += 1

                    # make plot for tempogram
                    ax[plot_counter] = fig.add_subplot(
                        elem_count, 1, plot_counter+1, sharex=ax[index_of_first_plot])
                    librosa.display.specshow(obj.data['tempogram'], sr=obj.data['sr'], hop_length=obj.data['hop_size'],
                                             x_axis='time', y_axis='tempo', cmap='magma', ax=ax[plot_counter])
                    ax[plot_counter].axhline(obj.data['tempo'], color='w', linestyle='--',
                                             alpha=1, label='Estimated tempo={:g}'.format(obj.data['tempo']))
                    ax[plot_counter].legend(loc='upper right')
                    ax[plot_counter].set(title='Tempogram')
                    plot_counter += 1

                elif obj.figure_type == 'audio.descriptors':
                    # increment output filename
                    if plot_counter == 0:
                        of = obj.data['of'] + '_descriptors'
                    else:
                        of += '_descriptors'

                    if first_plot:
                        ax[plot_counter] = fig.add_subplot(
                            elem_count, 1, plot_counter+1)
                    else:
                        ax[plot_counter] = fig.add_subplot(
                            elem_count, 1, plot_counter+1, sharex=ax[index_of_first_plot])

                    # make plot for rms
                    ax[plot_counter].semilogy(
                        obj.data['times'], obj.data['rms'][0], label='RMS Energy')
                    ax[plot_counter].legend(loc='upper right')
                    plot_counter += 1

                    # make plot for flatness
                    ax[plot_counter] = fig.add_subplot(
                        elem_count, 1, plot_counter+1, sharex=ax[index_of_first_plot])
                    ax[plot_counter].plot(
                        obj.data['times'], obj.data['flatness'].T, label='Flatness', color='y')
                    ax[plot_counter].legend(loc='upper right')
                    plot_counter += 1

                    # make plot for spectrogram, centroid, bandwidth and rolloff
                    ax[plot_counter] = fig.add_subplot(
                        elem_count, 1, plot_counter+1, sharex=ax[index_of_first_plot])
                    librosa.display.specshow(librosa.power_to_db(obj.data['S'], ref=np.max, top_db=120), sr=obj.data['sr'],
                                             y_axis='mel', fmax=obj.data['sr']/2, x_axis='time', hop_length=obj.data['hop_size'], ax=ax[plot_counter])
                    # get rid of "default" ticks
                    ax[plot_counter].yaxis.set_minor_locator(
                        matplotlib.ticker.NullLocator())
                    plot_xticks = np.arange(
                        0, obj.data['length']+0.1, obj.data['length']/20)
                    ax[plot_counter].set(xticks=plot_xticks)

                    freq_ticks = [elem*100 for elem in range(10)]
                    freq_ticks = [250]
                    freq = 500
                    while freq < obj.data['sr']/2:
                        freq_ticks.append(freq)
                        freq *= 1.5

                    freq_ticks = [round(elem, -1) for elem in freq_ticks]
                    freq_ticks_labels = [str(round(
                        elem/1000, 1)) + 'k' if elem > 1000 else int(round(elem)) for elem in freq_ticks]

                    ax[plot_counter].set(yticks=(freq_ticks))
                    ax[plot_counter].set(yticklabels=(freq_ticks_labels))

                    ax[plot_counter].fill_between(obj.data['times'], obj.data['cent'][0] - obj.data['spec_bw']
                                                  [0], obj.data['cent'][0] + obj.data['spec_bw'][0], alpha=0.5, label='Centroid +- bandwidth')
                    ax[plot_counter].plot(
                        obj.data['times'], obj.data['cent'].T, label='Centroid', color='y')
                    ax[plot_counter].plot(
                        obj.data['times'], obj.data['rolloff'][0], label='Roll-off frequency (0.99)')
                    ax[plot_counter].plot(
                        obj.data['times'], obj.data['rolloff_min'][0], color='r', label='Roll-off frequency (0.01)')

                    ax[plot_counter].legend(loc='upper right')

                    plot_counter += 1

                elif obj.figure_type == 'audio.spectrogram':
                    # increment output filename
                    if plot_counter == 0:
                        of = obj.data['of'] + '_spectrogram'
                    else:
                        of += '_spectrogram'

                    if first_plot:
                        ax[plot_counter] = fig.add_subplot(
                            elem_count, 1, plot_counter+1)
                    else:
                        ax[plot_counter] = fig.add_subplot(
                            elem_count, 1, plot_counter+1, sharex=ax[index_of_first_plot])

                    librosa.display.specshow(librosa.power_to_db(obj.data['S'], ref=np.max, top_db=120), sr=obj.data['sr'],
                                             y_axis='mel', fmax=obj.data['sr']/2, x_axis='time', hop_length=obj.data['hop_size'], ax=ax[plot_counter])
                    # get rid of "default" ticks
                    ax[plot_counter].yaxis.set_minor_locator(
                        matplotlib.ticker.NullLocator())
                    plot_xticks = np.arange(
                        0, obj.data['length']+0.1, obj.data['length']/20)
                    ax[plot_counter].set(xticks=plot_xticks)

                    freq_ticks = [elem*100 for elem in range(10)]
                    freq_ticks = [250]
                    freq = 500
                    while freq < obj.data['sr']/2:
                        freq_ticks.append(freq)
                        freq *= 1.5

                    freq_ticks = [round(elem, -1) for elem in freq_ticks]
                    freq_ticks_labels = [str(round(
                        elem/1000, 1)) + 'k' if elem > 1000 else int(round(elem)) for elem in freq_ticks]

                    ax[plot_counter].set(yticks=(freq_ticks))
                    ax[plot_counter].set(yticklabels=(freq_ticks_labels))
                    ax[plot_counter].set(title='Spectrogram')

                    plot_counter += 1

                elif obj.figure_type == 'audio.waveform':
                    # increment output filename
                    if plot_counter == 0:
                        of = obj.data['of'] + '_waveform'
                    else:
                        of += '_waveform'

                    if first_plot:
                        ax[plot_counter] = fig.add_subplot(
                            elem_count, 1, plot_counter+1)
                    else:
                        ax[plot_counter] = fig.add_subplot(
                            elem_count, 1, plot_counter+1, sharex=ax[index_of_first_plot])


                    librosa.display.waveshow(obj.data['y'], sr=obj.data['sr'], ax=ax[plot_counter])

                    plot_xticks = np.arange(
                        0, obj.data['length']+0.1, obj.data['length']/20)
                    ax[plot_counter].set(xticks=plot_xticks)

                    ax[plot_counter].set(title='Waveform')

                    plot_counter += 1

                elif obj.figure_type == 'layers':
                    there_were_layers = True
                    if plot_counter == 0:
                        of, plot_counter, _, first_slot_was_img, img_to_redo = build_figure(
                            obj.layers, elem_count, fig, ax, index_of_first_plot, plot_counter, of)
                    else:
                        of, plot_counter, _, _, _ = build_figure(
                            obj.layers, elem_count, fig, ax, index_of_first_plot, plot_counter, of)

            elif type(obj) == MgList:
                of, plot_counter, _, _, _ = build_figure(
                    obj.objectlist, elem_count, fig, ax, index_of_first_plot, plot_counter, of)

        return of, plot_counter, there_were_layers, first_slot_was_img, img_to_redo

    fig = plt.figure(dpi=dpi, figsize=(10, 3*elem_count))

    # make sure background is white
    fig.patch.set_facecolor('white')
    fig.patch.set_alpha(1)

    if title is not None and type(title)==str:
        # add title
        fig.suptitle(title, fontsize=16, y=0.99)

    ax = [None for elem in range(elem_count)]
    index_of_first_plot = None
    plot_counter = 0
    of = None

    of, plot_counter, there_were_layers, first_slot_was_img, img_to_redo = build_figure(
        self.objectlist, elem_count, fig, ax, index_of_first_plot, plot_counter, of)

    # workaround matplotlib bug: if there was a layered figure where the first slot shows an image, delete and redo that slot
    if first_slot_was_img and there_were_layers:
        ax[0].remove()
        ax[0] = fig.add_subplot(elem_count, 1, 1)
        ax[0].imshow(mpimg.imread(img_to_redo))
        ax[0].set_aspect('auto')
        ax[0].axes.xaxis.set_visible(False)
        ax[0].axes.yaxis.set_visible(False)

        # add title based on content
        last_tag = os.path.splitext(img_to_redo)[0].split('_')[-1]
        tag_titles = {'mgh': 'Horizontal Motiongram', 'mgv': 'Vertical Motiongram',
                      'vgh': 'Horizontal Videogram', 'vgv': 'Vertical Videogram',
                      'mgx': 'Vertical Motiongram', 'mgy': 'Horizontal Motiongram',
                      'vgx': 'Vertical Videogram', 'vgy': 'Horizontal Videogram'}
        if last_tag in tag_titles:
            ax[0].set(title=tag_titles[last_tag])
        else:
            ax[0].set(title=os.path.basename(img_to_redo))

    fig.tight_layout()

    # save figure as png
    if export_png:
        plt.savefig(of + '.png', format='png', transparent=False)

    # Always close: the returned MgFigure is displayed via show(), so leaving the
    # figure open would make the inline backend render a duplicate in notebooks.
    plt.close(fig)

    # create MgFigure
    mgf = MgFigure(
        figure=fig,
        figure_type='layers',
        data=None,
        layers=self.objectlist,
        image=of + '.png'
    )

    return mgf