Module wubwub.tracks

Audio track classes for Sequencers in wubwub.

Tracks

Tracks are the primary interface for creating and editing musical arrangements at the level of an individual instrument or sound. Sequencers (i.e. Sequencer) are populated with Tracks corresponding to instruments, and Tracks are populated with musical elements (notes and chords). All Tracks in wubwub are some kind of sampler; they load in an audio file and play it back with various manipulations (in pitch, volume, length, etc.).

The following documentation will cover some of the major ideas behind Tracks, giving examples along the way.

Module structure

There is one major Track class which outlines the major functionalities of Tracks and defines the methods common to all flavors of Tracks in wubwub. Subclasses of this parent define more specific behaviors and constitute the objects which are actually interfaced with to make music. There are three major types of Track available to the user:

  • Sampler is the most basic Track, which takes in a single sample.
  • MultiSampler is similar to the Sampler, but can assign samples to different pitches.
  • Arpeggiator is similar to the Sampler in that it takes in one sample, but has specific methods designed for chord arpeggiation.

Creating Tracks

Expect in rare cases, a Track must be connected with a Sequencer. The recommended approach is to initialize a Sequencer (i.e. a musical project/song) and then add Tracks (i.e. instruments/sounds) to it. There are specific Sequencer methods for creating Tracks:

>>> import wubwub as wb

>>> seq = wb.Sequencer(beats = 8, bpm = 100)
>>> kick = seq.add_sampler(sample='/my_samples/drums/kick.wav', name='kick')
>>> type(kick)
<class 'wubwub.tracks.Sampler'>

In the above example, a Sequencer seq is initialized, and then the Sequencer.add_sampler() method is called to create a new Track. The Track is initialized by providing a path to an audio file (sample) and a string name for identifying the Track (name). Since it was created with the add_sampler() method, the Track is already linked to the seq Sequencer object.

There are similar Sequencer.add_multisampler() and Sequencer.add_arpeggiator() methods for creating the other sorts of Tracks. But this "getting started" section will focus on the simple Sampler for now.

The sample parameter can either be a system path to an audio file, or a pydub AudioSegment object (this is the type of object that the system paths are converted into). The latter option is utilized when working with the samples provided by wubwub.sounds:

>>> import wubwub.sounds as snd
>>> DRUMS = snd.load('drums.808')
>>> x = DRUMS['handclap'] # <class 'pydub.audio_segment.AudioSegment'>
>>> clap = seq.add_sampler(sample=x, name='clap')

How to keep … track

The Sequencer can help locate and manipulate its Tracks after creation. You can use indexing syntax to retrieve Tracks by name:

>>> seq['clap']
Sampler(name="clap")

You can check all the Tracks contained within:

>>> seq.tracks()
(Sampler(name="kick"), Sampler(name="clap"))

>>> seq.tracknames()
['kick', 'clap']

There are several other methods documented under Sequencer which provide basic functionality for managing Tracks. If you need to delete or duplicate a Track, respectively look at Sequencer.delete_track() and Sequencer.duplicate_track().

A Track can also produce information about its parent Sequencer:

# get the Sequencer itself
>>> clap.sequencer
Sequencer(bpm=60, beats=60, tracks=2)

# get the current BPM
>>> clap.get_bpm()
60

# or the number of beats
>>> clap.get_beats()
8

The note dictionary

All tracks have a notedict attribute for keeping track of musical directions. It is initialized as an empty SortedDict:

>>> kick.notedict()
SortedDict({})

Each Track has its own:

>>> kick.notedict is clap.notedict
False

The keys for the notedict are numbers corresponding to sequencer pulses (i.e., musical beats), and the values are objects from the wubwub.notes module (i.e. "Notes" which determine the pitch, length, and volume of sample playback). When playing back or exporting a song, each Track looks at its own notedict to decide when and how to play back samples.

Adding notes

Most of the creation in wubwub involves writing to the note dictionary, and there are several options for doing so. You can simply access the notedict attribute and add to it, but there are other approaches which are more versatile.

Track specific methods

All Tracks have specific helper methods intended to simplify music writing. For most of these, the user provides where to create notes (i.e. what beats); the function then construct the Note and Chord objects and add them to the note dictionary in the correct places:

>>> kick.make_notes(beats = [1, 3, 5, 7])
>>> kick.pprint_notedict()
{1: Note(pitch=0, length=1, volume=0),
 3: Note(pitch=0, length=1, volume=0),
 5: Note(pitch=0, length=1, volume=0),
 7: Note(pitch=0, length=1, volume=0)}

The notes were added to the kick Track note dictionary on the desired beats, and initialized with default pitch, length, and volume values which can be set by SamplerLikeTrack.make_notes(). The values themselves are all some sort of relative measurement (pitch is semitones relative to the original sample, length is beats relative to the Sequencer BPM, and volume is decibels relative to the original sample).

Expand source code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Audio track classes for Sequencers in wubwub.

.. include:: ../docs/tracks.md

"""

from abc import ABCMeta, abstractmethod
from collections.abc import Iterable
from collections import defaultdict
import copy
from fractions import Fraction
import itertools
from numbers import Number
import os
import pprint
import warnings

import numpy as np
import pydub
from sortedcontainers import SortedDict

from wubwub.audio import add_note_to_audio, add_effects, play, _overhang_to_milli
from wubwub.errors import WubWubError, WubWubWarning
from wubwub.notes import ArpChord, Chord, Note, arpeggiate, _notetypes_
from wubwub.plots import trackplot, pianoroll
from wubwub.resources import random_choice_generator, MINUTE, SECOND

class SliceableDict:
    '''Helper class to implement the "note slice" feature of Tracks.'''
    def __init__(self, d):
        self.d = d

    def __getitem__(self, keys):
        if isinstance(keys, Number):
            return {keys: self.d[keys]}
        elif isinstance(keys, slice):
            start, stop = (keys.start, keys.stop)
            start = 0 if start is None else start
            stop = np.inf if stop is None else stop
            return {k:v for k, v in self.d.items()
                    if start <= k < stop}
        elif isinstance(keys, Iterable):
            if getattr(keys, 'dtype', False) == bool:
                if not len(keys) == len(self.d):
                    raise IndexError(f'Length of boolean index ({len(keys)}) '
                                     f"does not match size of dict ({len(self)}).")
                return {k:v for boolean, (k, v) in
                        zip(keys, self.d.items()) if boolean}

            else:
                return {k: dict.get(self.d, k) for k in keys}
        else:
            raise IndexError('Could not interpret input as int, '
                             'slice, iterable, or boolean index.')

class Track(metaclass=ABCMeta):
    '''Generic Track class.'''

    handle_outside_notes = 'skip'

    def __init__(self, name, sequencer,):
        self.notedict = SortedDict()
        self.samplepath = None

        self.effects = None
        self.volume = 0
        self.pan = 0
        self.postprocess_steps = ['effects', 'volume', 'pan']

        self._name = None
        self._sample = None
        self._sequencer = None
        self.sequencer = sequencer
        self.name = name

        self.plotting = {}

    def __getitem__(self, beat):
        if isinstance(beat, Number):
            return self.notedict[beat]
        elif isinstance(beat, slice):
            start, stop = (beat.start, beat.stop)
            start = 0 if start is None else start
            stop = np.inf if stop is None else stop
            return [self.notedict[k] for k in self.notedict.keys() if start <= k < stop]
        elif isinstance(beat, Iterable):
            if getattr(beat, 'dtype', False) == bool:
                if not len(beat) == len(self.notedict):
                    raise IndexError(f'Length of boolean index ({len(beat)}) '
                                     f"does not match number of notes ({len(self.notedict)}).")
                return [self.notedict[k] for k, b in zip(self.notedict.keys(), beat)
                        if b]

            else:
                return [self.notedict[b] for b in beat]
        else:
            raise WubWubError('Index wubwub.Track with [beat], '
                              '[start:stop], or boolean index, '
                              f'not {type(beat)}')

    def __setitem__(self, beat, value):
        if isinstance(beat, Number):
            self.notedict[beat] = value
        elif isinstance(beat, slice):
            start, stop, step = (beat.start, beat.stop, beat.step)
            if step is None:
                # replace all notes in the range
                start = 0 if start is None else start
                stop = np.inf if stop is None else stop
                for k, v in self.notedict.items():
                    if k < start:
                        continue
                    if k >= stop:
                        break
                    self.notedict[k] = value
            else:
                # fill notes from start to stop every step
                start = 1 if start is None else start
                stop = self.get_beats() + 1 if stop is None else stop
                while start < stop:
                    self.notedict[start] = value
                    start += step
        elif isinstance(beat, Iterable):
            if getattr(beat, 'dtype', False) == bool:
                if not len(beat) == len(self.notedict):
                    raise IndexError(f'Length of boolean index ({len(beat)}) '
                                     f"does not match number of notes ({len(self.notedict)}).")
                if not type(value) in _notetypes_:
                    raise IndexError('Can only set with single note using '
                                     'boolean index.')
                for k, b in zip(self.notedict.keys(), beat):
                    if b:
                        self.notedict[k] = value
            else:
                if type(value) in _notetypes_:
                    value = [value] * len(beat)
                if len(beat) != len(value):
                    raise IndexError(f'Length of new values ({len(value)}) '
                                     'does not equal length of indexer '
                                     f'({len(beat)}).')
                for b, v in zip(beat, value):
                    self.notedict[b] = v

        else:
            raise WubWubError('Index wubwub.Track with [beat], '
                              '[start:stop], or boolean index, '
                              f'not {type(beat)}')

    @property
    def slice(self):
        return SliceableDict(self.notedict)

    @property
    def sequencer(self):
        return self._sequencer

    @sequencer.setter
    def sequencer(self, sequencer):
        if sequencer == None:
            self._sequencer = None
            return

        if self._name in sequencer.tracknames():
            raise WubWubError(f'name "{self._name}" already in use by new sequencer')

        if self._sequencer is not None:
            self._sequencer.delete_track(self)

        self._sequencer = sequencer
        self._sequencer._add_track(self)

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new):
        if self.sequencer and new in self.sequencer.tracknames():
            raise WubWubError(f'track name "{new}" already in use.')
        self._name = new

    def add(self, beat, element, merge=False, outsiders=None):

        if beat >= self.get_beats() + 1:
            method = self.handle_outside_notes if outsiders is None else outsiders
            options = ['skip', 'add', 'warn', 'raise']
            if method not in options:
                w = ('`method` not recognized, '
                     'defaulting to "skip".',)
                warnings.warn(w, WubWubWarning)
                method = 'skip'
            if method == 'skip':
                return
            if method == 'warn':
                s = ("Adding note on beat beyond the "
                     "sequencer's length.  See `handle_outside_notes` "
                     "in class docstring for `wb.Track` to toggle "
                     "this behavior.")
                warnings.warn(s, WubWubWarning)

            elif method == 'raise':
                s = ("Tried to add note on beat beyond the "
                     "sequencer's length.  See `handle_outside_notes` "
                     "in class docstring for `wb.Track` to toggle "
                     "this behavior.")
                raise WubWubError(s)
        existing = self.notedict.get(beat, None)
        if existing and merge:
            element = existing + element
        self.notedict[beat] = element

    def add_fromdict(self, d, offset=0, outsiders=None, merge=False):
        for beat, element in d.items():

            self.add(beat=beat + offset, element=element, merge=merge,
                     outsiders=outsiders)

    def array_of_beats(self):
        return np.array(self.notedict.keys())

    def copy(self, newname=None, newseq=False, with_notes=True,):
        if newname is None:
            newname = self.name
        if newseq is False:
            newseq = self.sequencer
        new = copy.copy(self)
        for k, v in vars(new).items():
            if k == 'notedict':
                setattr(new, k, v.copy())
            elif k == '_name':
                setattr(new, k, newname)
            elif k == '_sequencer':
                setattr(new, k, None)
            else:
                setattr(new, k, copy.deepcopy(v))
        new.sequencer = newseq
        if not with_notes:
            new.delete_all()
        return new

    def copypaste(self, start, stop, newstart, outsiders=None, merge=False,):
        section = self.slice[start:stop]
        if section:
            offset = start - 1
            at_one = {k-offset:v for k, v in section.items()}
            self.add_fromdict(at_one, offset=newstart-1)

    def _handle_beats_dict_boolarray(self, beats):
        if getattr(beats, 'dtype', False) == bool:
            beats = self[beats].keys()
        elif isinstance(beats, dict):
            beats = beats.keys()
        elif isinstance(beats, Number):
            return [beats]
        return beats

    def quantize(self, resolution=1/4, merge=False):
        bts = self.get_beats()
        targets = np.empty(0)
        if isinstance(resolution, Number):
            resolution = [resolution]
        for r in resolution:
            if ((1 / r) % 1) != 0:
                raise WubWubError('`resolution` must evenly divide 1')
            steps = int(bts * (1 / r))
            beats = np.linspace(1, bts + 1, steps, endpoint=False)
            targets = np.append(targets, beats)
        targets = np.unique(targets)
        for b, note in self.notedict.copy().items():
            diffs = np.abs(targets - b)
            argmin = np.argmin(diffs)
            closest = targets[argmin]
            if b != closest:
                del self.notedict[b]
                self.add(closest, note, merge=merge)

    def shift(self, beats, by, merge=False):
        beats = self._handle_beats_dict_boolarray(beats)
        newkeys = [k + by if k in beats else k
                   for k in self.notedict.keys()]
        oldnotes = self.notedict.values()
        self.delete_all_notes()
        for newbeat, note in zip(newkeys, oldnotes):
            self.add(newbeat, note, merge=merge)

    def get_bpm(self):
        return self.sequencer.bpm

    def get_beats(self):
        return self.sequencer.beats

    def count_by_beat(self, res=1):
        out = defaultdict(int)
        res = 1/res
        for beat in self.array_of_beats():
            out[np.floor(beat * res) / res] += 1

        return dict(out)

    def pprint_notedict(self):
        pprint.pprint(self.notedict)

    def clean(self):
        maxi = self.get_beats()
        self.notedict = SortedDict({b:note for b, note in self.notedict.items()
                                    if 1 <= b < maxi +1})

    def delete_all(self):
        self.notedict = SortedDict({})

    def delete(self, beats):
        beats = self._handle_beats_dict_boolarray(beats)
        for beat in beats:
            del self.notedict[beat]

    def delete_fromrange(self, lo, hi):
        self.notedict = SortedDict({b:note for b, note in self.notedict.items()
                                    if not lo <= b < hi})

    def unpack_notes(self, start=0, stop=np.inf,):
        unpacked = []
        for b, element in self.notedict.items():
            if not start <= b < stop:
                continue
            if isinstance(element, Note):
                unpacked.append((b, element))
            elif type(element) in [Chord, ArpChord]:
                for note in element.notes:
                    unpacked.append((b, note))
        return unpacked

    @abstractmethod
    def build(self, overhang=0, overhang_type='beats'):
        pass

    def postprocess(self, build):
        for step in self.postprocess_steps:
            if step == 'effects':
                build = add_effects(build, self.effects)
            if step == 'volume':
                build += self.volume
            if step == 'pan':
                build = build.pan(self.pan)
        return build

    def play(self, start=1, end=None, overhang=0, overhang_type='beats'):
        b = (1/self.get_bpm()) * MINUTE
        start = (start-1) * b
        if end is not None:
            end = (end-1) * b
        build = self.build(overhang, overhang_type)
        play(build[start:end])

    @abstractmethod
    def soundtest(self, duration=None, postprocess=True,):
        pass

    def plot(self, yaxis='semitones', timesig=4, grid=True, ax=None,
             plot_kwds=None, scatter_kwds=None):
        return trackplot(track=self,
                         yaxis=yaxis,
                         timesig=timesig,
                         grid=grid,
                         ax=ax,
                         plot_kwds=plot_kwds,
                         scatter_kwds=scatter_kwds)

    def pianoroll(self, timesig=4, grid=True,):
        return pianoroll(track=self, timesig=timesig, grid=grid)

class SamplerLikeTrack(Track):
    def __init__(self, name, sequencer, **kwargs):
        super().__init__(name=name, sequencer=sequencer)

    def make_notes(self, beats, pitches=0, lengths=1, volumes=0,
                   pitch_select='cycle', length_select='cycle',
                   volume_select='cycle', merge=False):

        if not isinstance(beats, Iterable):
            beats = [beats]

        pitches = self._convert_select_arg(pitches, pitch_select)
        lengths = self._convert_select_arg(lengths, length_select)
        volumes = self._convert_select_arg(volumes, volume_select)

        d = {b : Note(next(pitches), next(lengths), next(volumes))
             for b in beats}

        self.add_fromdict(d, merge=merge)

    def make_notes_every(self, freq, offset=0, pitches=0, lengths=1, volumes=0,
                         start=1, end=None, pitch_select='cycle',
                         length_select='cycle', volume_select='cycle', merge=False):

        freq = Fraction(freq).limit_denominator()

        pitches = self._convert_select_arg(pitches, pitch_select)
        lengths = self._convert_select_arg(lengths, length_select)
        volumes = self._convert_select_arg(volumes, volume_select)

        b = Fraction(start + offset).limit_denominator()
        if end is None:
            end = self.get_beats() + 1
        d = {}
        while b < end:
            pos = b.numerator / b.denominator
            d[pos] = Note(next(pitches), next(lengths), next(volumes))
            b += freq

        self.add_fromdict(d, merge=merge)

    def make_chord(self, beat, pitches, lengths=1, volumes=0, merge=False):
        chord = self._make_chord_assemble(pitches, lengths, volumes)
        self.add(beat, chord, merge=merge)

    def make_chord_every(self, freq, offset=0, pitches=0, lengths=1, volumes=0,
                         start=1, end=None, merge=False):

        freq = Fraction(freq).limit_denominator()

        chord = self._make_chord_assemble(pitches, lengths, volumes)
        b = Fraction(start + offset).limit_denominator()
        if end is None:
            end = self.get_beats() + 1
        d = {}
        while b < end:
            pos = b.numerator / b.denominator
            d[pos] = chord
            b += freq
        self.add_fromdict(d, merge=merge)

    def _make_chord_assemble(self, pitches, lengths, volumes):
        if not isinstance(pitches, Iterable) or isinstance(pitches, str):
            pitches = [pitches]

        if isinstance(lengths, Number):
            lengths = [lengths] * len(pitches)

        if isinstance(volumes, Number):
            volumes = [volumes] * len(pitches)

        notes = [Note(p, l, v) for p, l, v in zip(pitches, lengths, volumes)]
        return Chord(notes)

    def _convert_select_arg(self, arg, option):
        if not isinstance(arg, Iterable) or isinstance(arg, str):
            arg = [arg]

        if option == 'cycle':
            return itertools.cycle(arg)
        elif option == 'random':
            return random_choice_generator(arg)
        else:
            raise WubWubError('pitch, length, and volume select must be ',
                              '"cycle" or "random".')

class SingleSampleTrack(Track):
    def __init__(self, name, sample, sequencer, **kwargs):
        super().__init__(name=name, sequencer=sequencer, **kwargs)
        self._sample = None
        self.sample = sample

    @property
    def sample(self):
        return self._sample

    @sample.setter
    def sample(self, sample):
        if isinstance(sample, str):
            _, ext = os.path.splitext(sample)
            ext = ext.lower().strip('.')
            self._sample = pydub.AudioSegment.from_file(sample,
                                                        format=ext)
            self.samplepath = os.path.abspath(sample)
        elif isinstance(sample, pydub.AudioSegment):
            self._sample = sample
        else:
            raise WubWubError('sample must be a path or pydub.AudioSegment')

class MultiSampleTrack(Track):
    def __init__(self, name, sequencer, **kwargs):
        super().__init__(name=name, sequencer=sequencer, **kwargs)
        self.samples = {}

class Sampler(SingleSampleTrack, SamplerLikeTrack):
    def __init__(self, name, sample, sequencer, basepitch='C4', overlap=True):
        super().__init__(name=name, sample=sample, sequencer=sequencer,
                         basepitch=basepitch, overlap=overlap)
        self.overlap = overlap
        self.basepitch = basepitch

    def __repr__(self):
        return f'Sampler(name="{self.name}")'

    def build(self, overhang=0, overhang_type='beats'):
        b = (1/self.get_bpm()) * MINUTE
        overhang = _overhang_to_milli(overhang, overhang_type, b)
        tracklength = self.get_beats() * b + overhang
        audio = pydub.AudioSegment.silent(duration=tracklength)
        sample = self.sample
        basepitch = self.basepitch
        next_position = np.inf
        for beat, value in sorted(self.notedict.items(), reverse=True):
            position = (beat-1) * b
            if isinstance(value, Note):
                note = value
                duration = note.length * b
                if (position + duration) > next_position and not self.overlap:
                    duration = next_position - position
                next_position = position
                audio = add_note_to_audio(note=note,
                                          audio=audio,
                                          sample=sample,
                                          position=position,
                                          duration=duration,
                                          basepitch=basepitch)
            elif isinstance(value, Chord):
                chord = value
                for note in chord.notes:
                    duration = note.length * b
                    if (position + duration) > next_position and not self.overlap:
                        duration = next_position - position
                    audio = add_note_to_audio(note=note,
                                              audio=audio,
                                              sample=sample,
                                              position=position,
                                              duration=duration,
                                              basepitch=basepitch)
                next_position = position

        return self.postprocess(audio)

    def soundtest(self, duration=None, postprocess=True,):
        test = self.sample
        if postprocess:
            test = self.postprocess(test)
        if duration is None:
            duration = len(test)
        else:
            duration = duration * SECOND
        play(test[:duration])

class MultiSampler(MultiSampleTrack, SamplerLikeTrack):
    def __init__(self, name, sequencer, overlap=True):
        super().__init__(name=name, sequencer=sequencer)
        self.overlap = overlap
        self.default_sample = pydub.AudioSegment.empty()

    def __repr__(self):
        return f'MultiSampler(name="{self.name}")'

    def build(self, overhang=0, overhang_type='beats'):
        b = (1/self.get_bpm()) * MINUTE
        overhang = _overhang_to_milli(overhang, overhang_type, b)
        tracklength = self.get_beats() * b + overhang
        audio = pydub.AudioSegment.silent(duration=tracklength)
        next_position = np.inf
        for beat, value in sorted(self.notedict.items(), reverse=True):
            position = (beat-1) * b
            if isinstance(value, Note):
                note = value
                duration = note.length * b
                if (position + duration) > next_position and not self.overlap:
                    duration = next_position - position
                next_position = position
                audio = add_note_to_audio(note=note,
                                          audio=audio,
                                          sample=self.get_sample(note.pitch),
                                          position=position,
                                          duration=duration,
                                          shift=False)
            elif isinstance(value, Chord):
                chord = value
                for note in chord.notes:
                    duration = note.length * b
                    if (position + duration) > next_position and not self.overlap:
                        duration = next_position - position
                    audio = add_note_to_audio(note=note,
                                              audio=audio,
                                              sample=self.get_sample(note.pitch),
                                              position=position,
                                              duration=duration,
                                              shift=False)
                next_position = position

        return self.postprocess(audio)

    def soundtest(self, duration=None, postprocess=True,):
        for k, v in self.samples.items():
            test = v
            if postprocess:
                test = self.postprocess(test)
            if duration is None:
                duration = len(test)
            else:
                duration = duration * SECOND
            play(test[:duration])

    def add_sample(self, key, sample):
        if isinstance(sample, str):
            _, ext = os.path.splitext(sample)
            ext = ext.lower().strip('.')
            self.samples[key] = pydub.AudioSegment.from_file(sample,
                                                             format=ext)
        elif isinstance(sample, pydub.AudioSegment):
            self.samples[key] = sample
        else:
            raise WubWubError('sample must be a path or pydub.AudioSegment')

    def get_sample(self, key):
        return self.samples.get(key, self.default_sample)

class Arpeggiator(SingleSampleTrack):
    def __init__(self, name, sample, sequencer, basepitch='C4', freq=.5,
                 method='up'):
        super().__init__(name=name, sample=sample, sequencer=sequencer,)
        self.freq = freq
        self.method = method
        self.basepitch = basepitch

    def __repr__(self):
        return (f'Arpeggiator(name="{self.name}", '
                f'freq={self.freq}, method="{self.method}")')

    def make_chord(self, beat, pitches, length=1, merge=False):
        notes = [Note(p) for p in pitches]
        chord = ArpChord(notes, length)
        self.add(beat, chord, merge=merge,)

    def make_chord_every(self, freq, offset=0, pitches=0, length=1,
                         start=1, end=None, merge=False):
        notes = [Note(p) for p in pitches]
        chord = ArpChord(notes, length)
        b = start + offset
        if end is None:
            end = self.get_beats() + 1
        d = {}
        while b < end:
            d[b] = chord
            b += freq
        self.add_fromdict(d, merge=merge)

    def build(self, overhang=0, overhang_type='beats'):
        b = (1/self.get_bpm()) * MINUTE
        overhang = _overhang_to_milli(overhang, overhang_type, b)
        tracklength = self.get_beats() * b + overhang
        audio = pydub.AudioSegment.silent(duration=tracklength)
        sample = self.sample
        basepitch = self.basepitch
        next_beat = np.inf
        for beat, chord in sorted(self.notedict.items(), reverse=True):
            try:
                length = chord.length
            except AttributeError:
                length = max(n.length for n in chord.notes)
            if beat + length >= next_beat:
                length = next_beat - beat
            next_beat = beat
            arpeggiated = arpeggiate(chord, beat=beat, length=length,
                                     freq=self.freq, method=self.method)
            for arpbeat, note in arpeggiated.items():
                position = (arpbeat-1) * b
                duration = note.length * b
                audio = add_note_to_audio(note=note,
                                          audio=audio,
                                          sample=sample,
                                          position=position,
                                          duration=duration,
                                          basepitch=basepitch)

        return self.postprocess(audio)

    def soundtest(self, duration=None, postprocess=True,):
        test = self.sample
        if postprocess:
            test = self.postprocess(test)
        if duration is None:
            duration = len(test)
        else:
            duration = duration * SECOND
        play(test[:duration])

    def unpack_notes(self, start=0, stop=np.inf,):
        unpacked = []
        for b, element in self.notedict.items():
            if not start <= b < stop:
                continue
            if isinstance(element, Note):
                unpacked.append((b, element))
            elif type(element) in [Chord, ArpChord]:
                arpeggiated = arpeggiate(element, beat=b,
                                         freq=self.freq, method=self.method)
                for k, v in arpeggiated.items():
                    unpacked.append((k, v))

        return unpacked

Classes

class Arpeggiator (name, sample, sequencer, basepitch='C4', freq=0.5, method='up')

Generic Track class.

Expand source code
class Arpeggiator(SingleSampleTrack):
    def __init__(self, name, sample, sequencer, basepitch='C4', freq=.5,
                 method='up'):
        super().__init__(name=name, sample=sample, sequencer=sequencer,)
        self.freq = freq
        self.method = method
        self.basepitch = basepitch

    def __repr__(self):
        return (f'Arpeggiator(name="{self.name}", '
                f'freq={self.freq}, method="{self.method}")')

    def make_chord(self, beat, pitches, length=1, merge=False):
        notes = [Note(p) for p in pitches]
        chord = ArpChord(notes, length)
        self.add(beat, chord, merge=merge,)

    def make_chord_every(self, freq, offset=0, pitches=0, length=1,
                         start=1, end=None, merge=False):
        notes = [Note(p) for p in pitches]
        chord = ArpChord(notes, length)
        b = start + offset
        if end is None:
            end = self.get_beats() + 1
        d = {}
        while b < end:
            d[b] = chord
            b += freq
        self.add_fromdict(d, merge=merge)

    def build(self, overhang=0, overhang_type='beats'):
        b = (1/self.get_bpm()) * MINUTE
        overhang = _overhang_to_milli(overhang, overhang_type, b)
        tracklength = self.get_beats() * b + overhang
        audio = pydub.AudioSegment.silent(duration=tracklength)
        sample = self.sample
        basepitch = self.basepitch
        next_beat = np.inf
        for beat, chord in sorted(self.notedict.items(), reverse=True):
            try:
                length = chord.length
            except AttributeError:
                length = max(n.length for n in chord.notes)
            if beat + length >= next_beat:
                length = next_beat - beat
            next_beat = beat
            arpeggiated = arpeggiate(chord, beat=beat, length=length,
                                     freq=self.freq, method=self.method)
            for arpbeat, note in arpeggiated.items():
                position = (arpbeat-1) * b
                duration = note.length * b
                audio = add_note_to_audio(note=note,
                                          audio=audio,
                                          sample=sample,
                                          position=position,
                                          duration=duration,
                                          basepitch=basepitch)

        return self.postprocess(audio)

    def soundtest(self, duration=None, postprocess=True,):
        test = self.sample
        if postprocess:
            test = self.postprocess(test)
        if duration is None:
            duration = len(test)
        else:
            duration = duration * SECOND
        play(test[:duration])

    def unpack_notes(self, start=0, stop=np.inf,):
        unpacked = []
        for b, element in self.notedict.items():
            if not start <= b < stop:
                continue
            if isinstance(element, Note):
                unpacked.append((b, element))
            elif type(element) in [Chord, ArpChord]:
                arpeggiated = arpeggiate(element, beat=b,
                                         freq=self.freq, method=self.method)
                for k, v in arpeggiated.items():
                    unpacked.append((k, v))

        return unpacked

Ancestors

Methods

def build(self, overhang=0, overhang_type='beats')
Expand source code
def build(self, overhang=0, overhang_type='beats'):
    b = (1/self.get_bpm()) * MINUTE
    overhang = _overhang_to_milli(overhang, overhang_type, b)
    tracklength = self.get_beats() * b + overhang
    audio = pydub.AudioSegment.silent(duration=tracklength)
    sample = self.sample
    basepitch = self.basepitch
    next_beat = np.inf
    for beat, chord in sorted(self.notedict.items(), reverse=True):
        try:
            length = chord.length
        except AttributeError:
            length = max(n.length for n in chord.notes)
        if beat + length >= next_beat:
            length = next_beat - beat
        next_beat = beat
        arpeggiated = arpeggiate(chord, beat=beat, length=length,
                                 freq=self.freq, method=self.method)
        for arpbeat, note in arpeggiated.items():
            position = (arpbeat-1) * b
            duration = note.length * b
            audio = add_note_to_audio(note=note,
                                      audio=audio,
                                      sample=sample,
                                      position=position,
                                      duration=duration,
                                      basepitch=basepitch)

    return self.postprocess(audio)
def make_chord(self, beat, pitches, length=1, merge=False)
Expand source code
def make_chord(self, beat, pitches, length=1, merge=False):
    notes = [Note(p) for p in pitches]
    chord = ArpChord(notes, length)
    self.add(beat, chord, merge=merge,)
def make_chord_every(self, freq, offset=0, pitches=0, length=1, start=1, end=None, merge=False)
Expand source code
def make_chord_every(self, freq, offset=0, pitches=0, length=1,
                     start=1, end=None, merge=False):
    notes = [Note(p) for p in pitches]
    chord = ArpChord(notes, length)
    b = start + offset
    if end is None:
        end = self.get_beats() + 1
    d = {}
    while b < end:
        d[b] = chord
        b += freq
    self.add_fromdict(d, merge=merge)
def soundtest(self, duration=None, postprocess=True)
Expand source code
def soundtest(self, duration=None, postprocess=True,):
    test = self.sample
    if postprocess:
        test = self.postprocess(test)
    if duration is None:
        duration = len(test)
    else:
        duration = duration * SECOND
    play(test[:duration])
def unpack_notes(self, start=0, stop=inf)
Expand source code
def unpack_notes(self, start=0, stop=np.inf,):
    unpacked = []
    for b, element in self.notedict.items():
        if not start <= b < stop:
            continue
        if isinstance(element, Note):
            unpacked.append((b, element))
        elif type(element) in [Chord, ArpChord]:
            arpeggiated = arpeggiate(element, beat=b,
                                     freq=self.freq, method=self.method)
            for k, v in arpeggiated.items():
                unpacked.append((k, v))

    return unpacked
class MultiSampleTrack (name, sequencer, **kwargs)

Generic Track class.

Expand source code
class MultiSampleTrack(Track):
    def __init__(self, name, sequencer, **kwargs):
        super().__init__(name=name, sequencer=sequencer, **kwargs)
        self.samples = {}

Ancestors

Subclasses

class MultiSampler (name, sequencer, overlap=True)

Generic Track class.

Expand source code
class MultiSampler(MultiSampleTrack, SamplerLikeTrack):
    def __init__(self, name, sequencer, overlap=True):
        super().__init__(name=name, sequencer=sequencer)
        self.overlap = overlap
        self.default_sample = pydub.AudioSegment.empty()

    def __repr__(self):
        return f'MultiSampler(name="{self.name}")'

    def build(self, overhang=0, overhang_type='beats'):
        b = (1/self.get_bpm()) * MINUTE
        overhang = _overhang_to_milli(overhang, overhang_type, b)
        tracklength = self.get_beats() * b + overhang
        audio = pydub.AudioSegment.silent(duration=tracklength)
        next_position = np.inf
        for beat, value in sorted(self.notedict.items(), reverse=True):
            position = (beat-1) * b
            if isinstance(value, Note):
                note = value
                duration = note.length * b
                if (position + duration) > next_position and not self.overlap:
                    duration = next_position - position
                next_position = position
                audio = add_note_to_audio(note=note,
                                          audio=audio,
                                          sample=self.get_sample(note.pitch),
                                          position=position,
                                          duration=duration,
                                          shift=False)
            elif isinstance(value, Chord):
                chord = value
                for note in chord.notes:
                    duration = note.length * b
                    if (position + duration) > next_position and not self.overlap:
                        duration = next_position - position
                    audio = add_note_to_audio(note=note,
                                              audio=audio,
                                              sample=self.get_sample(note.pitch),
                                              position=position,
                                              duration=duration,
                                              shift=False)
                next_position = position

        return self.postprocess(audio)

    def soundtest(self, duration=None, postprocess=True,):
        for k, v in self.samples.items():
            test = v
            if postprocess:
                test = self.postprocess(test)
            if duration is None:
                duration = len(test)
            else:
                duration = duration * SECOND
            play(test[:duration])

    def add_sample(self, key, sample):
        if isinstance(sample, str):
            _, ext = os.path.splitext(sample)
            ext = ext.lower().strip('.')
            self.samples[key] = pydub.AudioSegment.from_file(sample,
                                                             format=ext)
        elif isinstance(sample, pydub.AudioSegment):
            self.samples[key] = sample
        else:
            raise WubWubError('sample must be a path or pydub.AudioSegment')

    def get_sample(self, key):
        return self.samples.get(key, self.default_sample)

Ancestors

Methods

def add_sample(self, key, sample)
Expand source code
def add_sample(self, key, sample):
    if isinstance(sample, str):
        _, ext = os.path.splitext(sample)
        ext = ext.lower().strip('.')
        self.samples[key] = pydub.AudioSegment.from_file(sample,
                                                         format=ext)
    elif isinstance(sample, pydub.AudioSegment):
        self.samples[key] = sample
    else:
        raise WubWubError('sample must be a path or pydub.AudioSegment')
def build(self, overhang=0, overhang_type='beats')
Expand source code
def build(self, overhang=0, overhang_type='beats'):
    b = (1/self.get_bpm()) * MINUTE
    overhang = _overhang_to_milli(overhang, overhang_type, b)
    tracklength = self.get_beats() * b + overhang
    audio = pydub.AudioSegment.silent(duration=tracklength)
    next_position = np.inf
    for beat, value in sorted(self.notedict.items(), reverse=True):
        position = (beat-1) * b
        if isinstance(value, Note):
            note = value
            duration = note.length * b
            if (position + duration) > next_position and not self.overlap:
                duration = next_position - position
            next_position = position
            audio = add_note_to_audio(note=note,
                                      audio=audio,
                                      sample=self.get_sample(note.pitch),
                                      position=position,
                                      duration=duration,
                                      shift=False)
        elif isinstance(value, Chord):
            chord = value
            for note in chord.notes:
                duration = note.length * b
                if (position + duration) > next_position and not self.overlap:
                    duration = next_position - position
                audio = add_note_to_audio(note=note,
                                          audio=audio,
                                          sample=self.get_sample(note.pitch),
                                          position=position,
                                          duration=duration,
                                          shift=False)
            next_position = position

    return self.postprocess(audio)
def get_sample(self, key)
Expand source code
def get_sample(self, key):
    return self.samples.get(key, self.default_sample)
def soundtest(self, duration=None, postprocess=True)
Expand source code
def soundtest(self, duration=None, postprocess=True,):
    for k, v in self.samples.items():
        test = v
        if postprocess:
            test = self.postprocess(test)
        if duration is None:
            duration = len(test)
        else:
            duration = duration * SECOND
        play(test[:duration])
class Sampler (name, sample, sequencer, basepitch='C4', overlap=True)

Generic Track class.

Expand source code
class Sampler(SingleSampleTrack, SamplerLikeTrack):
    def __init__(self, name, sample, sequencer, basepitch='C4', overlap=True):
        super().__init__(name=name, sample=sample, sequencer=sequencer,
                         basepitch=basepitch, overlap=overlap)
        self.overlap = overlap
        self.basepitch = basepitch

    def __repr__(self):
        return f'Sampler(name="{self.name}")'

    def build(self, overhang=0, overhang_type='beats'):
        b = (1/self.get_bpm()) * MINUTE
        overhang = _overhang_to_milli(overhang, overhang_type, b)
        tracklength = self.get_beats() * b + overhang
        audio = pydub.AudioSegment.silent(duration=tracklength)
        sample = self.sample
        basepitch = self.basepitch
        next_position = np.inf
        for beat, value in sorted(self.notedict.items(), reverse=True):
            position = (beat-1) * b
            if isinstance(value, Note):
                note = value
                duration = note.length * b
                if (position + duration) > next_position and not self.overlap:
                    duration = next_position - position
                next_position = position
                audio = add_note_to_audio(note=note,
                                          audio=audio,
                                          sample=sample,
                                          position=position,
                                          duration=duration,
                                          basepitch=basepitch)
            elif isinstance(value, Chord):
                chord = value
                for note in chord.notes:
                    duration = note.length * b
                    if (position + duration) > next_position and not self.overlap:
                        duration = next_position - position
                    audio = add_note_to_audio(note=note,
                                              audio=audio,
                                              sample=sample,
                                              position=position,
                                              duration=duration,
                                              basepitch=basepitch)
                next_position = position

        return self.postprocess(audio)

    def soundtest(self, duration=None, postprocess=True,):
        test = self.sample
        if postprocess:
            test = self.postprocess(test)
        if duration is None:
            duration = len(test)
        else:
            duration = duration * SECOND
        play(test[:duration])

Ancestors

Methods

def build(self, overhang=0, overhang_type='beats')
Expand source code
def build(self, overhang=0, overhang_type='beats'):
    b = (1/self.get_bpm()) * MINUTE
    overhang = _overhang_to_milli(overhang, overhang_type, b)
    tracklength = self.get_beats() * b + overhang
    audio = pydub.AudioSegment.silent(duration=tracklength)
    sample = self.sample
    basepitch = self.basepitch
    next_position = np.inf
    for beat, value in sorted(self.notedict.items(), reverse=True):
        position = (beat-1) * b
        if isinstance(value, Note):
            note = value
            duration = note.length * b
            if (position + duration) > next_position and not self.overlap:
                duration = next_position - position
            next_position = position
            audio = add_note_to_audio(note=note,
                                      audio=audio,
                                      sample=sample,
                                      position=position,
                                      duration=duration,
                                      basepitch=basepitch)
        elif isinstance(value, Chord):
            chord = value
            for note in chord.notes:
                duration = note.length * b
                if (position + duration) > next_position and not self.overlap:
                    duration = next_position - position
                audio = add_note_to_audio(note=note,
                                          audio=audio,
                                          sample=sample,
                                          position=position,
                                          duration=duration,
                                          basepitch=basepitch)
            next_position = position

    return self.postprocess(audio)
def soundtest(self, duration=None, postprocess=True)
Expand source code
def soundtest(self, duration=None, postprocess=True,):
    test = self.sample
    if postprocess:
        test = self.postprocess(test)
    if duration is None:
        duration = len(test)
    else:
        duration = duration * SECOND
    play(test[:duration])
class SamplerLikeTrack (name, sequencer, **kwargs)

Generic Track class.

Expand source code
class SamplerLikeTrack(Track):
    def __init__(self, name, sequencer, **kwargs):
        super().__init__(name=name, sequencer=sequencer)

    def make_notes(self, beats, pitches=0, lengths=1, volumes=0,
                   pitch_select='cycle', length_select='cycle',
                   volume_select='cycle', merge=False):

        if not isinstance(beats, Iterable):
            beats = [beats]

        pitches = self._convert_select_arg(pitches, pitch_select)
        lengths = self._convert_select_arg(lengths, length_select)
        volumes = self._convert_select_arg(volumes, volume_select)

        d = {b : Note(next(pitches), next(lengths), next(volumes))
             for b in beats}

        self.add_fromdict(d, merge=merge)

    def make_notes_every(self, freq, offset=0, pitches=0, lengths=1, volumes=0,
                         start=1, end=None, pitch_select='cycle',
                         length_select='cycle', volume_select='cycle', merge=False):

        freq = Fraction(freq).limit_denominator()

        pitches = self._convert_select_arg(pitches, pitch_select)
        lengths = self._convert_select_arg(lengths, length_select)
        volumes = self._convert_select_arg(volumes, volume_select)

        b = Fraction(start + offset).limit_denominator()
        if end is None:
            end = self.get_beats() + 1
        d = {}
        while b < end:
            pos = b.numerator / b.denominator
            d[pos] = Note(next(pitches), next(lengths), next(volumes))
            b += freq

        self.add_fromdict(d, merge=merge)

    def make_chord(self, beat, pitches, lengths=1, volumes=0, merge=False):
        chord = self._make_chord_assemble(pitches, lengths, volumes)
        self.add(beat, chord, merge=merge)

    def make_chord_every(self, freq, offset=0, pitches=0, lengths=1, volumes=0,
                         start=1, end=None, merge=False):

        freq = Fraction(freq).limit_denominator()

        chord = self._make_chord_assemble(pitches, lengths, volumes)
        b = Fraction(start + offset).limit_denominator()
        if end is None:
            end = self.get_beats() + 1
        d = {}
        while b < end:
            pos = b.numerator / b.denominator
            d[pos] = chord
            b += freq
        self.add_fromdict(d, merge=merge)

    def _make_chord_assemble(self, pitches, lengths, volumes):
        if not isinstance(pitches, Iterable) or isinstance(pitches, str):
            pitches = [pitches]

        if isinstance(lengths, Number):
            lengths = [lengths] * len(pitches)

        if isinstance(volumes, Number):
            volumes = [volumes] * len(pitches)

        notes = [Note(p, l, v) for p, l, v in zip(pitches, lengths, volumes)]
        return Chord(notes)

    def _convert_select_arg(self, arg, option):
        if not isinstance(arg, Iterable) or isinstance(arg, str):
            arg = [arg]

        if option == 'cycle':
            return itertools.cycle(arg)
        elif option == 'random':
            return random_choice_generator(arg)
        else:
            raise WubWubError('pitch, length, and volume select must be ',
                              '"cycle" or "random".')

Ancestors

Subclasses

Methods

def make_chord(self, beat, pitches, lengths=1, volumes=0, merge=False)
Expand source code
def make_chord(self, beat, pitches, lengths=1, volumes=0, merge=False):
    chord = self._make_chord_assemble(pitches, lengths, volumes)
    self.add(beat, chord, merge=merge)
def make_chord_every(self, freq, offset=0, pitches=0, lengths=1, volumes=0, start=1, end=None, merge=False)
Expand source code
def make_chord_every(self, freq, offset=0, pitches=0, lengths=1, volumes=0,
                     start=1, end=None, merge=False):

    freq = Fraction(freq).limit_denominator()

    chord = self._make_chord_assemble(pitches, lengths, volumes)
    b = Fraction(start + offset).limit_denominator()
    if end is None:
        end = self.get_beats() + 1
    d = {}
    while b < end:
        pos = b.numerator / b.denominator
        d[pos] = chord
        b += freq
    self.add_fromdict(d, merge=merge)
def make_notes(self, beats, pitches=0, lengths=1, volumes=0, pitch_select='cycle', length_select='cycle', volume_select='cycle', merge=False)
Expand source code
def make_notes(self, beats, pitches=0, lengths=1, volumes=0,
               pitch_select='cycle', length_select='cycle',
               volume_select='cycle', merge=False):

    if not isinstance(beats, Iterable):
        beats = [beats]

    pitches = self._convert_select_arg(pitches, pitch_select)
    lengths = self._convert_select_arg(lengths, length_select)
    volumes = self._convert_select_arg(volumes, volume_select)

    d = {b : Note(next(pitches), next(lengths), next(volumes))
         for b in beats}

    self.add_fromdict(d, merge=merge)
def make_notes_every(self, freq, offset=0, pitches=0, lengths=1, volumes=0, start=1, end=None, pitch_select='cycle', length_select='cycle', volume_select='cycle', merge=False)
Expand source code
def make_notes_every(self, freq, offset=0, pitches=0, lengths=1, volumes=0,
                     start=1, end=None, pitch_select='cycle',
                     length_select='cycle', volume_select='cycle', merge=False):

    freq = Fraction(freq).limit_denominator()

    pitches = self._convert_select_arg(pitches, pitch_select)
    lengths = self._convert_select_arg(lengths, length_select)
    volumes = self._convert_select_arg(volumes, volume_select)

    b = Fraction(start + offset).limit_denominator()
    if end is None:
        end = self.get_beats() + 1
    d = {}
    while b < end:
        pos = b.numerator / b.denominator
        d[pos] = Note(next(pitches), next(lengths), next(volumes))
        b += freq

    self.add_fromdict(d, merge=merge)
class SingleSampleTrack (name, sample, sequencer, **kwargs)

Generic Track class.

Expand source code
class SingleSampleTrack(Track):
    def __init__(self, name, sample, sequencer, **kwargs):
        super().__init__(name=name, sequencer=sequencer, **kwargs)
        self._sample = None
        self.sample = sample

    @property
    def sample(self):
        return self._sample

    @sample.setter
    def sample(self, sample):
        if isinstance(sample, str):
            _, ext = os.path.splitext(sample)
            ext = ext.lower().strip('.')
            self._sample = pydub.AudioSegment.from_file(sample,
                                                        format=ext)
            self.samplepath = os.path.abspath(sample)
        elif isinstance(sample, pydub.AudioSegment):
            self._sample = sample
        else:
            raise WubWubError('sample must be a path or pydub.AudioSegment')

Ancestors

Subclasses

Instance variables

var sample
Expand source code
@property
def sample(self):
    return self._sample
class SliceableDict (d)

Helper class to implement the "note slice" feature of Tracks.

Expand source code
class SliceableDict:
    '''Helper class to implement the "note slice" feature of Tracks.'''
    def __init__(self, d):
        self.d = d

    def __getitem__(self, keys):
        if isinstance(keys, Number):
            return {keys: self.d[keys]}
        elif isinstance(keys, slice):
            start, stop = (keys.start, keys.stop)
            start = 0 if start is None else start
            stop = np.inf if stop is None else stop
            return {k:v for k, v in self.d.items()
                    if start <= k < stop}
        elif isinstance(keys, Iterable):
            if getattr(keys, 'dtype', False) == bool:
                if not len(keys) == len(self.d):
                    raise IndexError(f'Length of boolean index ({len(keys)}) '
                                     f"does not match size of dict ({len(self)}).")
                return {k:v for boolean, (k, v) in
                        zip(keys, self.d.items()) if boolean}

            else:
                return {k: dict.get(self.d, k) for k in keys}
        else:
            raise IndexError('Could not interpret input as int, '
                             'slice, iterable, or boolean index.')
class Track (name, sequencer)

Generic Track class.

Expand source code
class Track(metaclass=ABCMeta):
    '''Generic Track class.'''

    handle_outside_notes = 'skip'

    def __init__(self, name, sequencer,):
        self.notedict = SortedDict()
        self.samplepath = None

        self.effects = None
        self.volume = 0
        self.pan = 0
        self.postprocess_steps = ['effects', 'volume', 'pan']

        self._name = None
        self._sample = None
        self._sequencer = None
        self.sequencer = sequencer
        self.name = name

        self.plotting = {}

    def __getitem__(self, beat):
        if isinstance(beat, Number):
            return self.notedict[beat]
        elif isinstance(beat, slice):
            start, stop = (beat.start, beat.stop)
            start = 0 if start is None else start
            stop = np.inf if stop is None else stop
            return [self.notedict[k] for k in self.notedict.keys() if start <= k < stop]
        elif isinstance(beat, Iterable):
            if getattr(beat, 'dtype', False) == bool:
                if not len(beat) == len(self.notedict):
                    raise IndexError(f'Length of boolean index ({len(beat)}) '
                                     f"does not match number of notes ({len(self.notedict)}).")
                return [self.notedict[k] for k, b in zip(self.notedict.keys(), beat)
                        if b]

            else:
                return [self.notedict[b] for b in beat]
        else:
            raise WubWubError('Index wubwub.Track with [beat], '
                              '[start:stop], or boolean index, '
                              f'not {type(beat)}')

    def __setitem__(self, beat, value):
        if isinstance(beat, Number):
            self.notedict[beat] = value
        elif isinstance(beat, slice):
            start, stop, step = (beat.start, beat.stop, beat.step)
            if step is None:
                # replace all notes in the range
                start = 0 if start is None else start
                stop = np.inf if stop is None else stop
                for k, v in self.notedict.items():
                    if k < start:
                        continue
                    if k >= stop:
                        break
                    self.notedict[k] = value
            else:
                # fill notes from start to stop every step
                start = 1 if start is None else start
                stop = self.get_beats() + 1 if stop is None else stop
                while start < stop:
                    self.notedict[start] = value
                    start += step
        elif isinstance(beat, Iterable):
            if getattr(beat, 'dtype', False) == bool:
                if not len(beat) == len(self.notedict):
                    raise IndexError(f'Length of boolean index ({len(beat)}) '
                                     f"does not match number of notes ({len(self.notedict)}).")
                if not type(value) in _notetypes_:
                    raise IndexError('Can only set with single note using '
                                     'boolean index.')
                for k, b in zip(self.notedict.keys(), beat):
                    if b:
                        self.notedict[k] = value
            else:
                if type(value) in _notetypes_:
                    value = [value] * len(beat)
                if len(beat) != len(value):
                    raise IndexError(f'Length of new values ({len(value)}) '
                                     'does not equal length of indexer '
                                     f'({len(beat)}).')
                for b, v in zip(beat, value):
                    self.notedict[b] = v

        else:
            raise WubWubError('Index wubwub.Track with [beat], '
                              '[start:stop], or boolean index, '
                              f'not {type(beat)}')

    @property
    def slice(self):
        return SliceableDict(self.notedict)

    @property
    def sequencer(self):
        return self._sequencer

    @sequencer.setter
    def sequencer(self, sequencer):
        if sequencer == None:
            self._sequencer = None
            return

        if self._name in sequencer.tracknames():
            raise WubWubError(f'name "{self._name}" already in use by new sequencer')

        if self._sequencer is not None:
            self._sequencer.delete_track(self)

        self._sequencer = sequencer
        self._sequencer._add_track(self)

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new):
        if self.sequencer and new in self.sequencer.tracknames():
            raise WubWubError(f'track name "{new}" already in use.')
        self._name = new

    def add(self, beat, element, merge=False, outsiders=None):

        if beat >= self.get_beats() + 1:
            method = self.handle_outside_notes if outsiders is None else outsiders
            options = ['skip', 'add', 'warn', 'raise']
            if method not in options:
                w = ('`method` not recognized, '
                     'defaulting to "skip".',)
                warnings.warn(w, WubWubWarning)
                method = 'skip'
            if method == 'skip':
                return
            if method == 'warn':
                s = ("Adding note on beat beyond the "
                     "sequencer's length.  See `handle_outside_notes` "
                     "in class docstring for `wb.Track` to toggle "
                     "this behavior.")
                warnings.warn(s, WubWubWarning)

            elif method == 'raise':
                s = ("Tried to add note on beat beyond the "
                     "sequencer's length.  See `handle_outside_notes` "
                     "in class docstring for `wb.Track` to toggle "
                     "this behavior.")
                raise WubWubError(s)
        existing = self.notedict.get(beat, None)
        if existing and merge:
            element = existing + element
        self.notedict[beat] = element

    def add_fromdict(self, d, offset=0, outsiders=None, merge=False):
        for beat, element in d.items():

            self.add(beat=beat + offset, element=element, merge=merge,
                     outsiders=outsiders)

    def array_of_beats(self):
        return np.array(self.notedict.keys())

    def copy(self, newname=None, newseq=False, with_notes=True,):
        if newname is None:
            newname = self.name
        if newseq is False:
            newseq = self.sequencer
        new = copy.copy(self)
        for k, v in vars(new).items():
            if k == 'notedict':
                setattr(new, k, v.copy())
            elif k == '_name':
                setattr(new, k, newname)
            elif k == '_sequencer':
                setattr(new, k, None)
            else:
                setattr(new, k, copy.deepcopy(v))
        new.sequencer = newseq
        if not with_notes:
            new.delete_all()
        return new

    def copypaste(self, start, stop, newstart, outsiders=None, merge=False,):
        section = self.slice[start:stop]
        if section:
            offset = start - 1
            at_one = {k-offset:v for k, v in section.items()}
            self.add_fromdict(at_one, offset=newstart-1)

    def _handle_beats_dict_boolarray(self, beats):
        if getattr(beats, 'dtype', False) == bool:
            beats = self[beats].keys()
        elif isinstance(beats, dict):
            beats = beats.keys()
        elif isinstance(beats, Number):
            return [beats]
        return beats

    def quantize(self, resolution=1/4, merge=False):
        bts = self.get_beats()
        targets = np.empty(0)
        if isinstance(resolution, Number):
            resolution = [resolution]
        for r in resolution:
            if ((1 / r) % 1) != 0:
                raise WubWubError('`resolution` must evenly divide 1')
            steps = int(bts * (1 / r))
            beats = np.linspace(1, bts + 1, steps, endpoint=False)
            targets = np.append(targets, beats)
        targets = np.unique(targets)
        for b, note in self.notedict.copy().items():
            diffs = np.abs(targets - b)
            argmin = np.argmin(diffs)
            closest = targets[argmin]
            if b != closest:
                del self.notedict[b]
                self.add(closest, note, merge=merge)

    def shift(self, beats, by, merge=False):
        beats = self._handle_beats_dict_boolarray(beats)
        newkeys = [k + by if k in beats else k
                   for k in self.notedict.keys()]
        oldnotes = self.notedict.values()
        self.delete_all_notes()
        for newbeat, note in zip(newkeys, oldnotes):
            self.add(newbeat, note, merge=merge)

    def get_bpm(self):
        return self.sequencer.bpm

    def get_beats(self):
        return self.sequencer.beats

    def count_by_beat(self, res=1):
        out = defaultdict(int)
        res = 1/res
        for beat in self.array_of_beats():
            out[np.floor(beat * res) / res] += 1

        return dict(out)

    def pprint_notedict(self):
        pprint.pprint(self.notedict)

    def clean(self):
        maxi = self.get_beats()
        self.notedict = SortedDict({b:note for b, note in self.notedict.items()
                                    if 1 <= b < maxi +1})

    def delete_all(self):
        self.notedict = SortedDict({})

    def delete(self, beats):
        beats = self._handle_beats_dict_boolarray(beats)
        for beat in beats:
            del self.notedict[beat]

    def delete_fromrange(self, lo, hi):
        self.notedict = SortedDict({b:note for b, note in self.notedict.items()
                                    if not lo <= b < hi})

    def unpack_notes(self, start=0, stop=np.inf,):
        unpacked = []
        for b, element in self.notedict.items():
            if not start <= b < stop:
                continue
            if isinstance(element, Note):
                unpacked.append((b, element))
            elif type(element) in [Chord, ArpChord]:
                for note in element.notes:
                    unpacked.append((b, note))
        return unpacked

    @abstractmethod
    def build(self, overhang=0, overhang_type='beats'):
        pass

    def postprocess(self, build):
        for step in self.postprocess_steps:
            if step == 'effects':
                build = add_effects(build, self.effects)
            if step == 'volume':
                build += self.volume
            if step == 'pan':
                build = build.pan(self.pan)
        return build

    def play(self, start=1, end=None, overhang=0, overhang_type='beats'):
        b = (1/self.get_bpm()) * MINUTE
        start = (start-1) * b
        if end is not None:
            end = (end-1) * b
        build = self.build(overhang, overhang_type)
        play(build[start:end])

    @abstractmethod
    def soundtest(self, duration=None, postprocess=True,):
        pass

    def plot(self, yaxis='semitones', timesig=4, grid=True, ax=None,
             plot_kwds=None, scatter_kwds=None):
        return trackplot(track=self,
                         yaxis=yaxis,
                         timesig=timesig,
                         grid=grid,
                         ax=ax,
                         plot_kwds=plot_kwds,
                         scatter_kwds=scatter_kwds)

    def pianoroll(self, timesig=4, grid=True,):
        return pianoroll(track=self, timesig=timesig, grid=grid)

Subclasses

Class variables

var handle_outside_notes

Instance variables

var name
Expand source code
@property
def name(self):
    return self._name
var sequencer
Expand source code
@property
def sequencer(self):
    return self._sequencer
var slice
Expand source code
@property
def slice(self):
    return SliceableDict(self.notedict)

Methods

def add(self, beat, element, merge=False, outsiders=None)
Expand source code
def add(self, beat, element, merge=False, outsiders=None):

    if beat >= self.get_beats() + 1:
        method = self.handle_outside_notes if outsiders is None else outsiders
        options = ['skip', 'add', 'warn', 'raise']
        if method not in options:
            w = ('`method` not recognized, '
                 'defaulting to "skip".',)
            warnings.warn(w, WubWubWarning)
            method = 'skip'
        if method == 'skip':
            return
        if method == 'warn':
            s = ("Adding note on beat beyond the "
                 "sequencer's length.  See `handle_outside_notes` "
                 "in class docstring for `wb.Track` to toggle "
                 "this behavior.")
            warnings.warn(s, WubWubWarning)

        elif method == 'raise':
            s = ("Tried to add note on beat beyond the "
                 "sequencer's length.  See `handle_outside_notes` "
                 "in class docstring for `wb.Track` to toggle "
                 "this behavior.")
            raise WubWubError(s)
    existing = self.notedict.get(beat, None)
    if existing and merge:
        element = existing + element
    self.notedict[beat] = element
def add_fromdict(self, d, offset=0, outsiders=None, merge=False)
Expand source code
def add_fromdict(self, d, offset=0, outsiders=None, merge=False):
    for beat, element in d.items():

        self.add(beat=beat + offset, element=element, merge=merge,
                 outsiders=outsiders)
def array_of_beats(self)
Expand source code
def array_of_beats(self):
    return np.array(self.notedict.keys())
def build(self, overhang=0, overhang_type='beats')
Expand source code
@abstractmethod
def build(self, overhang=0, overhang_type='beats'):
    pass
def clean(self)
Expand source code
def clean(self):
    maxi = self.get_beats()
    self.notedict = SortedDict({b:note for b, note in self.notedict.items()
                                if 1 <= b < maxi +1})
def copy(self, newname=None, newseq=False, with_notes=True)
Expand source code
def copy(self, newname=None, newseq=False, with_notes=True,):
    if newname is None:
        newname = self.name
    if newseq is False:
        newseq = self.sequencer
    new = copy.copy(self)
    for k, v in vars(new).items():
        if k == 'notedict':
            setattr(new, k, v.copy())
        elif k == '_name':
            setattr(new, k, newname)
        elif k == '_sequencer':
            setattr(new, k, None)
        else:
            setattr(new, k, copy.deepcopy(v))
    new.sequencer = newseq
    if not with_notes:
        new.delete_all()
    return new
def copypaste(self, start, stop, newstart, outsiders=None, merge=False)
Expand source code
def copypaste(self, start, stop, newstart, outsiders=None, merge=False,):
    section = self.slice[start:stop]
    if section:
        offset = start - 1
        at_one = {k-offset:v for k, v in section.items()}
        self.add_fromdict(at_one, offset=newstart-1)
def count_by_beat(self, res=1)
Expand source code
def count_by_beat(self, res=1):
    out = defaultdict(int)
    res = 1/res
    for beat in self.array_of_beats():
        out[np.floor(beat * res) / res] += 1

    return dict(out)
def delete(self, beats)
Expand source code
def delete(self, beats):
    beats = self._handle_beats_dict_boolarray(beats)
    for beat in beats:
        del self.notedict[beat]
def delete_all(self)
Expand source code
def delete_all(self):
    self.notedict = SortedDict({})
def delete_fromrange(self, lo, hi)
Expand source code
def delete_fromrange(self, lo, hi):
    self.notedict = SortedDict({b:note for b, note in self.notedict.items()
                                if not lo <= b < hi})
def get_beats(self)
Expand source code
def get_beats(self):
    return self.sequencer.beats
def get_bpm(self)
Expand source code
def get_bpm(self):
    return self.sequencer.bpm
def pianoroll(self, timesig=4, grid=True)
Expand source code
def pianoroll(self, timesig=4, grid=True,):
    return pianoroll(track=self, timesig=timesig, grid=grid)
def play(self, start=1, end=None, overhang=0, overhang_type='beats')
Expand source code
def play(self, start=1, end=None, overhang=0, overhang_type='beats'):
    b = (1/self.get_bpm()) * MINUTE
    start = (start-1) * b
    if end is not None:
        end = (end-1) * b
    build = self.build(overhang, overhang_type)
    play(build[start:end])
def plot(self, yaxis='semitones', timesig=4, grid=True, ax=None, plot_kwds=None, scatter_kwds=None)
Expand source code
def plot(self, yaxis='semitones', timesig=4, grid=True, ax=None,
         plot_kwds=None, scatter_kwds=None):
    return trackplot(track=self,
                     yaxis=yaxis,
                     timesig=timesig,
                     grid=grid,
                     ax=ax,
                     plot_kwds=plot_kwds,
                     scatter_kwds=scatter_kwds)
def postprocess(self, build)
Expand source code
def postprocess(self, build):
    for step in self.postprocess_steps:
        if step == 'effects':
            build = add_effects(build, self.effects)
        if step == 'volume':
            build += self.volume
        if step == 'pan':
            build = build.pan(self.pan)
    return build
def pprint_notedict(self)
Expand source code
def pprint_notedict(self):
    pprint.pprint(self.notedict)
def quantize(self, resolution=0.25, merge=False)
Expand source code
def quantize(self, resolution=1/4, merge=False):
    bts = self.get_beats()
    targets = np.empty(0)
    if isinstance(resolution, Number):
        resolution = [resolution]
    for r in resolution:
        if ((1 / r) % 1) != 0:
            raise WubWubError('`resolution` must evenly divide 1')
        steps = int(bts * (1 / r))
        beats = np.linspace(1, bts + 1, steps, endpoint=False)
        targets = np.append(targets, beats)
    targets = np.unique(targets)
    for b, note in self.notedict.copy().items():
        diffs = np.abs(targets - b)
        argmin = np.argmin(diffs)
        closest = targets[argmin]
        if b != closest:
            del self.notedict[b]
            self.add(closest, note, merge=merge)
def shift(self, beats, by, merge=False)
Expand source code
def shift(self, beats, by, merge=False):
    beats = self._handle_beats_dict_boolarray(beats)
    newkeys = [k + by if k in beats else k
               for k in self.notedict.keys()]
    oldnotes = self.notedict.values()
    self.delete_all_notes()
    for newbeat, note in zip(newkeys, oldnotes):
        self.add(newbeat, note, merge=merge)
def soundtest(self, duration=None, postprocess=True)
Expand source code
@abstractmethod
def soundtest(self, duration=None, postprocess=True,):
    pass
def unpack_notes(self, start=0, stop=inf)
Expand source code
def unpack_notes(self, start=0, stop=np.inf,):
    unpacked = []
    for b, element in self.notedict.items():
        if not start <= b < stop:
            continue
        if isinstance(element, Note):
            unpacked.append((b, element))
        elif type(element) in [Chord, ArpChord]:
            for note in element.notes:
                unpacked.append((b, note))
    return unpacked