saysynth.core.arp

The Arp class enables melodic speech synthesis by mapping input text or phonemes onto a configurable arpeggiator.

  1"""The Arp class enables melodic speech synthesis by mapping input text or phonemes onto a configurable arpeggiator.
  2<center><img src="/assets/img/cell.png"></img></center>
  3"""
  4import random
  5from functools import cached_property
  6from typing import List, Optional, Union
  7
  8from midi_utils import midi_arp
  9
 10from .base import SayObject
 11from .lyrics import Lyrics
 12from .note import Note
 13from .segment import Segment
 14
 15
 16class Arp(SayObject):
 17    def __init__(
 18        self,
 19        text: Optional[str] = None,
 20        notes: List[int] = [],
 21        root: str = "C2",
 22        chord: str = "min69",
 23        inversions: List[int] = [],
 24        stack: int = 0,
 25        styles: List[str] = ["down"],
 26        octaves: List[int] = [0],
 27        velocities: List[int] = [100],
 28        volume_level_per_note: int = 3,
 29        beat_bpm: float = 131.0,
 30        beat_count: Union[float, int, str] = "1/16",
 31        beat_time_sig: str = "4/4",
 32        beat_duration: Optional[float] = None,
 33        note_bpm: float = 131.0,
 34        note_count: Union[float, int, str] = "3/64",
 35        note_time_sig: str = "4/4",
 36        note_duration: Optional[float] = None,
 37        randomize_start: Optional[List[int]] = None,
 38        start_bpm: float = 131.0,
 39        start_count: Union[float, int, str] = 0,
 40        start_time_sig: str = "4/4",
 41        start_duration: Optional[float] = None,
 42        duration_bpm: float = 131.0,
 43        duration_count: Union[float, int, str] = "16",
 44        duration_time_sig: str = "4/4",
 45        duration: Optional[float] = None,
 46        loops: Optional[int] = None,
 47        **note_options,
 48    ):
 49        """
 50        Generate an arpeggiated melody
 51
 52        Args:
 53            text: text to 'sing'
 54            notes: an arbitrary list of notes to arpeggiate
 55            root: root note of chord to arpeggiate
 56            chord: the chord name
 57            inversions: a list of inversions to apply to the chord notes
 58            stack: Stack a chord up (1) or down (-1)
 59            styles: A list of arpeggiated style names.
 60                    See https://gitlab.com/gltd/midi-utils/-/blob/main/midi_utils/arp.py for the full list.
 61            octaves: A list of octaves to add to the notes (eg: [-1, 2])
 62            velocities: A list of velocities for each note of the arpeggiator.
 63                        If this list is shorter than the list of notes, a modulo
 64                        operator is user.
 65            volume_level_per_note: How many notes it takes to re-adjust the volume/velocity.
 66                                  This parameter exists because too many volume changes
 67                                  can cause sporadically audio to dropout.
 68            beat_bpm: The bpm to use when calculating the duration of each beat of the arp.
 69            beat_count: The count of one beat of the arp
 70            beat_time_sig: str = The time signature of the arp.
 71            beat_duration: The duration of the beat. If provided, this overrides
 72                           the other beat_* parameters.
 73            note_bpm: The bpm to use when calculating the duration of each note of the arp.
 74                      By default, this is the
 75            note_count: The count of one beat of the arp
 76            note_time_sig: str = The time signature of the arp.
 77            note_duration: The duration of the beat. If provided, this overrides
 78                           the other note_* parameters.
 79            randomize_start: Optional[List[int]] = None,
 80            start_bpm: bpm to use when determining the start of the arp. The start_* parameters
 81                       adds silence at the beginning of the arpeggiator. This is particularly
 82                       useful when creating sequences.
 83            start_count: The count of one beat of starting silence.
 84            start_time_sig: The time signature to use when calculating the duration
 85                           of one beat of starting silence.
 86            start_duration: The amount of silence to add at the beginning in ms
 87            duration_bpm: bpm to use when determining the duration of the arpeggiator.
 88            duration_count: The duration beat count
 89            duration_time_sig: Time signature to use when determining duration
 90            duration: The total duration of the pattern in ms.
 91            loops: The number of times to loop the pattern.
 92                   This overrides `duration_*` settings.
 93            **note_options: Additional options to pass to `Note`.
 94                          These will affect how each note of the
 95                          arpeggiator sounds.
 96
 97        """
 98        self.styles = styles
 99
100        if randomize_start:
101            start_duration = random.choice(
102                range(randomize_start[0], randomize_start[1] + 1)
103            )
104
105        self.sequence = midi_arp(
106            notes=notes,
107            root=root,
108            chord=chord,
109            inversions=inversions,
110            stack=stack,
111            octaves=octaves,
112            styles=styles,
113            velocities=velocities,
114            beat_bpm=beat_bpm,
115            beat_count=beat_count,
116            beat_time_sig=beat_time_sig,
117            beat_duration=beat_duration,
118            note_bpm=note_bpm,
119            note_count=note_count,
120            note_time_sig=note_time_sig,
121            note_duration=note_duration,
122            start_bpm=start_bpm,
123            start_count=start_count,
124            start_time_sig=start_time_sig,
125            start_duration=start_duration,
126            duration_bpm=duration_bpm,
127            duration_count=duration_count,
128            duration_time_sig=duration_time_sig,
129            duration=duration,
130            loops=loops,
131        )
132        self.volume_level_per_note = volume_level_per_note
133        self._note_options = note_options
134        self.lyrics = None
135        if text:
136            # HACK: add padding to prevent skipping phonemes
137            # TODO: figure why this is happening.
138            self.lyrics = Lyrics(" . " + text + " . ")
139
140    def _get_kwargs(self, index, **kwargs):
141        """
142        get kwargs + update with new ones
143        used for mapping similar kwargs over different notes
144        """
145        d = dict(self._note_options.items())
146        d.update(kwargs)
147        d["include_volume_level"] = index % self.volume_level_per_note == 0
148        return d
149
150    @cached_property
151    def notes(self) -> List[Note]:
152        """
153        The generated list of `Note` in the Arp.
154        """
155        start_at_phoneme = 0
156        _notes = []
157        for i, note in enumerate(self.sequence):
158            note_kwargs = self._get_kwargs(i, **note)
159            if self.lyrics:
160                # handle text / phoneme:
161                phonemes = self.lyrics.get_phonemes(start_at=start_at_phoneme)
162                if len(phonemes) == 0:
163                    # TODO: Fix this hack.
164                    start_at_phoneme = 0
165                    phonemes = self.lyrics.get_phonemes(
166                        start_at=start_at_phoneme
167                    )
168                note_kwargs["phoneme"] = phonemes
169            note = Note(**note_kwargs)
170            last_note_length = note.n_segments
171            start_at_phoneme += last_note_length
172            _notes.append(note)
173        return _notes
174
175    @property
176    def segments(self) -> List[Segment]:
177        """
178        The generated list of `Segment` in the Arp.
179        """
180        return [segment for note in self.notes for segment in note.segments]
181
182    @property
183    def n_notes(self) -> int:
184        """
185        The number of Notes in the Arp.
186        """
187        return len(self.notes)
188
189    @property
190    def n_segments(self) -> int:
191        """
192        The number of Segments in the Arp.
193        """
194        return sum([note.n_segments for note in self.notes])
195
196    def to_text(self):
197        """
198        Render this Arp as Apple SpeechSynthesis DSL text.
199        """
200        return "\n".join([n.to_text() for n in self.notes])
201
202    def __repr__(self):
203        return f"<Arp {','.join(self.styles)} {','.join(str(n) for n in self.notes)}>"
class Arp(saysynth.core.base.SayObject):
 17class Arp(SayObject):
 18    def __init__(
 19        self,
 20        text: Optional[str] = None,
 21        notes: List[int] = [],
 22        root: str = "C2",
 23        chord: str = "min69",
 24        inversions: List[int] = [],
 25        stack: int = 0,
 26        styles: List[str] = ["down"],
 27        octaves: List[int] = [0],
 28        velocities: List[int] = [100],
 29        volume_level_per_note: int = 3,
 30        beat_bpm: float = 131.0,
 31        beat_count: Union[float, int, str] = "1/16",
 32        beat_time_sig: str = "4/4",
 33        beat_duration: Optional[float] = None,
 34        note_bpm: float = 131.0,
 35        note_count: Union[float, int, str] = "3/64",
 36        note_time_sig: str = "4/4",
 37        note_duration: Optional[float] = None,
 38        randomize_start: Optional[List[int]] = None,
 39        start_bpm: float = 131.0,
 40        start_count: Union[float, int, str] = 0,
 41        start_time_sig: str = "4/4",
 42        start_duration: Optional[float] = None,
 43        duration_bpm: float = 131.0,
 44        duration_count: Union[float, int, str] = "16",
 45        duration_time_sig: str = "4/4",
 46        duration: Optional[float] = None,
 47        loops: Optional[int] = None,
 48        **note_options,
 49    ):
 50        """
 51        Generate an arpeggiated melody
 52
 53        Args:
 54            text: text to 'sing'
 55            notes: an arbitrary list of notes to arpeggiate
 56            root: root note of chord to arpeggiate
 57            chord: the chord name
 58            inversions: a list of inversions to apply to the chord notes
 59            stack: Stack a chord up (1) or down (-1)
 60            styles: A list of arpeggiated style names.
 61                    See https://gitlab.com/gltd/midi-utils/-/blob/main/midi_utils/arp.py for the full list.
 62            octaves: A list of octaves to add to the notes (eg: [-1, 2])
 63            velocities: A list of velocities for each note of the arpeggiator.
 64                        If this list is shorter than the list of notes, a modulo
 65                        operator is user.
 66            volume_level_per_note: How many notes it takes to re-adjust the volume/velocity.
 67                                  This parameter exists because too many volume changes
 68                                  can cause sporadically audio to dropout.
 69            beat_bpm: The bpm to use when calculating the duration of each beat of the arp.
 70            beat_count: The count of one beat of the arp
 71            beat_time_sig: str = The time signature of the arp.
 72            beat_duration: The duration of the beat. If provided, this overrides
 73                           the other beat_* parameters.
 74            note_bpm: The bpm to use when calculating the duration of each note of the arp.
 75                      By default, this is the
 76            note_count: The count of one beat of the arp
 77            note_time_sig: str = The time signature of the arp.
 78            note_duration: The duration of the beat. If provided, this overrides
 79                           the other note_* parameters.
 80            randomize_start: Optional[List[int]] = None,
 81            start_bpm: bpm to use when determining the start of the arp. The start_* parameters
 82                       adds silence at the beginning of the arpeggiator. This is particularly
 83                       useful when creating sequences.
 84            start_count: The count of one beat of starting silence.
 85            start_time_sig: The time signature to use when calculating the duration
 86                           of one beat of starting silence.
 87            start_duration: The amount of silence to add at the beginning in ms
 88            duration_bpm: bpm to use when determining the duration of the arpeggiator.
 89            duration_count: The duration beat count
 90            duration_time_sig: Time signature to use when determining duration
 91            duration: The total duration of the pattern in ms.
 92            loops: The number of times to loop the pattern.
 93                   This overrides `duration_*` settings.
 94            **note_options: Additional options to pass to `Note`.
 95                          These will affect how each note of the
 96                          arpeggiator sounds.
 97
 98        """
 99        self.styles = styles
100
101        if randomize_start:
102            start_duration = random.choice(
103                range(randomize_start[0], randomize_start[1] + 1)
104            )
105
106        self.sequence = midi_arp(
107            notes=notes,
108            root=root,
109            chord=chord,
110            inversions=inversions,
111            stack=stack,
112            octaves=octaves,
113            styles=styles,
114            velocities=velocities,
115            beat_bpm=beat_bpm,
116            beat_count=beat_count,
117            beat_time_sig=beat_time_sig,
118            beat_duration=beat_duration,
119            note_bpm=note_bpm,
120            note_count=note_count,
121            note_time_sig=note_time_sig,
122            note_duration=note_duration,
123            start_bpm=start_bpm,
124            start_count=start_count,
125            start_time_sig=start_time_sig,
126            start_duration=start_duration,
127            duration_bpm=duration_bpm,
128            duration_count=duration_count,
129            duration_time_sig=duration_time_sig,
130            duration=duration,
131            loops=loops,
132        )
133        self.volume_level_per_note = volume_level_per_note
134        self._note_options = note_options
135        self.lyrics = None
136        if text:
137            # HACK: add padding to prevent skipping phonemes
138            # TODO: figure why this is happening.
139            self.lyrics = Lyrics(" . " + text + " . ")
140
141    def _get_kwargs(self, index, **kwargs):
142        """
143        get kwargs + update with new ones
144        used for mapping similar kwargs over different notes
145        """
146        d = dict(self._note_options.items())
147        d.update(kwargs)
148        d["include_volume_level"] = index % self.volume_level_per_note == 0
149        return d
150
151    @cached_property
152    def notes(self) -> List[Note]:
153        """
154        The generated list of `Note` in the Arp.
155        """
156        start_at_phoneme = 0
157        _notes = []
158        for i, note in enumerate(self.sequence):
159            note_kwargs = self._get_kwargs(i, **note)
160            if self.lyrics:
161                # handle text / phoneme:
162                phonemes = self.lyrics.get_phonemes(start_at=start_at_phoneme)
163                if len(phonemes) == 0:
164                    # TODO: Fix this hack.
165                    start_at_phoneme = 0
166                    phonemes = self.lyrics.get_phonemes(
167                        start_at=start_at_phoneme
168                    )
169                note_kwargs["phoneme"] = phonemes
170            note = Note(**note_kwargs)
171            last_note_length = note.n_segments
172            start_at_phoneme += last_note_length
173            _notes.append(note)
174        return _notes
175
176    @property
177    def segments(self) -> List[Segment]:
178        """
179        The generated list of `Segment` in the Arp.
180        """
181        return [segment for note in self.notes for segment in note.segments]
182
183    @property
184    def n_notes(self) -> int:
185        """
186        The number of Notes in the Arp.
187        """
188        return len(self.notes)
189
190    @property
191    def n_segments(self) -> int:
192        """
193        The number of Segments in the Arp.
194        """
195        return sum([note.n_segments for note in self.notes])
196
197    def to_text(self):
198        """
199        Render this Arp as Apple SpeechSynthesis DSL text.
200        """
201        return "\n".join([n.to_text() for n in self.notes])
202
203    def __repr__(self):
204        return f"<Arp {','.join(self.styles)} {','.join(str(n) for n in self.notes)}>"
Arp( text: Optional[str] = None, notes: List[int] = [], root: str = 'C2', chord: str = 'min69', inversions: List[int] = [], stack: int = 0, styles: List[str] = ['down'], octaves: List[int] = [0], velocities: List[int] = [100], volume_level_per_note: int = 3, beat_bpm: float = 131.0, beat_count: Union[float, int, str] = '1/16', beat_time_sig: str = '4/4', beat_duration: Optional[float] = None, note_bpm: float = 131.0, note_count: Union[float, int, str] = '3/64', note_time_sig: str = '4/4', note_duration: Optional[float] = None, randomize_start: Optional[List[int]] = None, start_bpm: float = 131.0, start_count: Union[float, int, str] = 0, start_time_sig: str = '4/4', start_duration: Optional[float] = None, duration_bpm: float = 131.0, duration_count: Union[float, int, str] = '16', duration_time_sig: str = '4/4', duration: Optional[float] = None, loops: Optional[int] = None, **note_options)
 18    def __init__(
 19        self,
 20        text: Optional[str] = None,
 21        notes: List[int] = [],
 22        root: str = "C2",
 23        chord: str = "min69",
 24        inversions: List[int] = [],
 25        stack: int = 0,
 26        styles: List[str] = ["down"],
 27        octaves: List[int] = [0],
 28        velocities: List[int] = [100],
 29        volume_level_per_note: int = 3,
 30        beat_bpm: float = 131.0,
 31        beat_count: Union[float, int, str] = "1/16",
 32        beat_time_sig: str = "4/4",
 33        beat_duration: Optional[float] = None,
 34        note_bpm: float = 131.0,
 35        note_count: Union[float, int, str] = "3/64",
 36        note_time_sig: str = "4/4",
 37        note_duration: Optional[float] = None,
 38        randomize_start: Optional[List[int]] = None,
 39        start_bpm: float = 131.0,
 40        start_count: Union[float, int, str] = 0,
 41        start_time_sig: str = "4/4",
 42        start_duration: Optional[float] = None,
 43        duration_bpm: float = 131.0,
 44        duration_count: Union[float, int, str] = "16",
 45        duration_time_sig: str = "4/4",
 46        duration: Optional[float] = None,
 47        loops: Optional[int] = None,
 48        **note_options,
 49    ):
 50        """
 51        Generate an arpeggiated melody
 52
 53        Args:
 54            text: text to 'sing'
 55            notes: an arbitrary list of notes to arpeggiate
 56            root: root note of chord to arpeggiate
 57            chord: the chord name
 58            inversions: a list of inversions to apply to the chord notes
 59            stack: Stack a chord up (1) or down (-1)
 60            styles: A list of arpeggiated style names.
 61                    See https://gitlab.com/gltd/midi-utils/-/blob/main/midi_utils/arp.py for the full list.
 62            octaves: A list of octaves to add to the notes (eg: [-1, 2])
 63            velocities: A list of velocities for each note of the arpeggiator.
 64                        If this list is shorter than the list of notes, a modulo
 65                        operator is user.
 66            volume_level_per_note: How many notes it takes to re-adjust the volume/velocity.
 67                                  This parameter exists because too many volume changes
 68                                  can cause sporadically audio to dropout.
 69            beat_bpm: The bpm to use when calculating the duration of each beat of the arp.
 70            beat_count: The count of one beat of the arp
 71            beat_time_sig: str = The time signature of the arp.
 72            beat_duration: The duration of the beat. If provided, this overrides
 73                           the other beat_* parameters.
 74            note_bpm: The bpm to use when calculating the duration of each note of the arp.
 75                      By default, this is the
 76            note_count: The count of one beat of the arp
 77            note_time_sig: str = The time signature of the arp.
 78            note_duration: The duration of the beat. If provided, this overrides
 79                           the other note_* parameters.
 80            randomize_start: Optional[List[int]] = None,
 81            start_bpm: bpm to use when determining the start of the arp. The start_* parameters
 82                       adds silence at the beginning of the arpeggiator. This is particularly
 83                       useful when creating sequences.
 84            start_count: The count of one beat of starting silence.
 85            start_time_sig: The time signature to use when calculating the duration
 86                           of one beat of starting silence.
 87            start_duration: The amount of silence to add at the beginning in ms
 88            duration_bpm: bpm to use when determining the duration of the arpeggiator.
 89            duration_count: The duration beat count
 90            duration_time_sig: Time signature to use when determining duration
 91            duration: The total duration of the pattern in ms.
 92            loops: The number of times to loop the pattern.
 93                   This overrides `duration_*` settings.
 94            **note_options: Additional options to pass to `Note`.
 95                          These will affect how each note of the
 96                          arpeggiator sounds.
 97
 98        """
 99        self.styles = styles
100
101        if randomize_start:
102            start_duration = random.choice(
103                range(randomize_start[0], randomize_start[1] + 1)
104            )
105
106        self.sequence = midi_arp(
107            notes=notes,
108            root=root,
109            chord=chord,
110            inversions=inversions,
111            stack=stack,
112            octaves=octaves,
113            styles=styles,
114            velocities=velocities,
115            beat_bpm=beat_bpm,
116            beat_count=beat_count,
117            beat_time_sig=beat_time_sig,
118            beat_duration=beat_duration,
119            note_bpm=note_bpm,
120            note_count=note_count,
121            note_time_sig=note_time_sig,
122            note_duration=note_duration,
123            start_bpm=start_bpm,
124            start_count=start_count,
125            start_time_sig=start_time_sig,
126            start_duration=start_duration,
127            duration_bpm=duration_bpm,
128            duration_count=duration_count,
129            duration_time_sig=duration_time_sig,
130            duration=duration,
131            loops=loops,
132        )
133        self.volume_level_per_note = volume_level_per_note
134        self._note_options = note_options
135        self.lyrics = None
136        if text:
137            # HACK: add padding to prevent skipping phonemes
138            # TODO: figure why this is happening.
139            self.lyrics = Lyrics(" . " + text + " . ")

Generate an arpeggiated melody

Arguments:
  • text: text to 'sing'
  • notes: an arbitrary list of notes to arpeggiate
  • root: root note of chord to arpeggiate
  • chord: the chord name
  • inversions: a list of inversions to apply to the chord notes
  • stack: Stack a chord up (1) or down (-1)
  • styles: A list of arpeggiated style names. See https://gitlab.com/gltd/midi-utils/-/blob/main/midi_utils/arp.py for the full list.
  • octaves: A list of octaves to add to the notes (eg: [-1, 2])
  • velocities: A list of velocities for each note of the arpeggiator. If this list is shorter than the list of notes, a modulo operator is user.
  • volume_level_per_note: How many notes it takes to re-adjust the volume/velocity. This parameter exists because too many volume changes can cause sporadically audio to dropout.
  • beat_bpm: The bpm to use when calculating the duration of each beat of the arp.
  • beat_count: The count of one beat of the arp
  • beat_time_sig: str = The time signature of the arp.
  • beat_duration: The duration of the beat. If provided, this overrides the other beat_* parameters.
  • note_bpm: The bpm to use when calculating the duration of each note of the arp. By default, this is the
  • note_count: The count of one beat of the arp
  • note_time_sig: str = The time signature of the arp.
  • note_duration: The duration of the beat. If provided, this overrides the other note_* parameters.
  • randomize_start: Optional[List[int]] = None,
  • start_bpm: bpm to use when determining the start of the arp. The start_* parameters adds silence at the beginning of the arpeggiator. This is particularly useful when creating sequences.
  • start_count: The count of one beat of starting silence.
  • start_time_sig: The time signature to use when calculating the duration of one beat of starting silence.
  • start_duration: The amount of silence to add at the beginning in ms
  • duration_bpm: bpm to use when determining the duration of the arpeggiator.
  • duration_count: The duration beat count
  • duration_time_sig: Time signature to use when determining duration
  • duration: The total duration of the pattern in ms.
  • loops: The number of times to loop the pattern. This overrides duration_* settings.
  • **note_options: Additional options to pass to Note. These will affect how each note of the arpeggiator sounds.

The generated list of Note in the Arp.

The generated list of Segment in the Arp.

n_notes: int

The number of Notes in the Arp.

n_segments: int

The number of Segments in the Arp.

def to_text(self):
197    def to_text(self):
198        """
199        Render this Arp as Apple SpeechSynthesis DSL text.
200        """
201        return "\n".join([n.to_text() for n in self.notes])

Render this Arp as Apple SpeechSynthesis DSL text.