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)}>"
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.
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.