saysynth.cli.commands.seq
Play a sequence of chord
, midi
, note
, and/or arp
tracks via yaml configuration.
1""" 2Play a sequence of `chord`, `midi`, `note`, and/or `arp` tracks via yaml configuration. 3""" 4 5import copy 6import os 7from argparse import ArgumentError 8from multiprocessing.pool import ThreadPool as Pool 9from typing import Dict 10 11import click 12import yaml 13 14from saysynth.cli.colors import blue, green, red, yellow 15from saysynth.cli.commands import arp, chord, midi, note 16from saysynth.cli.options import (expand_opt_name, seq_command_arg, seq_opts, 17 set_config_overrides_opt) 18from saysynth.core import controller 19from saysynth.utils import calculate_total_duration 20 21 22def run(**kwargs): 23 controller.ensure_pid_log() 24 25 # process configurations 26 config_path = kwargs.pop("base_config") 27 command = kwargs.pop("command") 28 output_dir = kwargs.pop("output_dir", "./") 29 if config_path: 30 # print config 31 if command == "echo": 32 click.echo(str(config_path.read())) 33 return 34 base_config = yaml.safe_load(config_path) 35 else: 36 base_config = {"name": "*"} 37 38 globals = base_config.pop("globals", {}) 39 seq = base_config.pop("name", None) 40 if seq is None: 41 raise ArgumentError("You must set a `name` in your sequence config") 42 43 tracks = kwargs.get("tracks", None) or [] 44 audio_devices = kwargs.get("audio_devices", None) or [] 45 46 # log sequence name 47 click.echo("-" * 79, err=True) 48 click.echo(f"{red('sequence')}: {yellow(seq)}", err=True) 49 click.echo("-" * 79, err=True) 50 51 # play/start sequences/tracks 52 if command in ["play", "start", "render"]: 53 track_configs = [] 54 for track, base_track_config in base_config.get("tracks", {}).items(): 55 56 # optionally skip tracks 57 if len(tracks) and track not in tracks: 58 continue 59 60 # allow for track-specific overrides 61 config_overrides = kwargs.get("config_overrides", {}) 62 track_overrides = config_overrides.pop(track, {}) 63 config_overrides.update(track_overrides) 64 65 # create track config 66 track_options = copy.copy(globals) # start with globals 67 base_track_options = { 68 expand_opt_name(k): v 69 for k, v in base_track_config.get("options", {}).items() 70 } # expand the names of shortened sequence options 71 track_options.update(base_track_options) 72 track_options.update(config_overrides) 73 74 # optionally filter by audio device 75 ad = track_options.get("audio_device", None) 76 if len(audio_devices) and ad not in audio_devices: 77 continue 78 79 # optionally start tracks immediately 80 if command == "start": 81 track_options["start_count"] = 0 82 83 # optionally render tracks 84 if command == "render": 85 try: 86 os.makedirs(output_dir) 87 except FileExistsError: 88 pass 89 track_options["audio_output_file"] = os.path.join( 90 output_dir, 91 f"{seq}-{track}-{base_track_config['type']}.aiff", 92 ) 93 94 # construct track configs 95 base_track_config["options"] = track_options 96 track_configs.append((seq, track, ad, base_track_config, command)) 97 98 # run everything 99 for _ in Pool(len(track_configs)).imap(run_track_func, track_configs): 100 continue 101 102 if command == "stop": 103 104 if len(audio_devices): 105 for ad in audio_devices: 106 click.echo( 107 f"🛑 {red('stopping')} ➡️ all tracks on {yellow('audio_device')}: {blue(ad)}", 108 err=True, 109 ) 110 controller.stop_child_pids(seq, track=None, ad=ad) 111 112 else: 113 if not len(tracks): 114 tracks = ["*"] 115 for track in tracks: 116 click.echo( 117 f"🛑 {red('stopping')} ➡️ {yellow('track')}: { blue(track)}", 118 err=True, 119 ) 120 controller.stop_child_pids(seq, track) 121 122 123TRACK_FUNCS: Dict[str, callable] = { 124 "chord": chord.run, 125 "midi": midi.run, 126 "note": note.run, 127 "arp": arp.run, 128} 129 130 131def run_track_func(item): 132 seq_name, track, ad, kwargs, command = item 133 pid = os.getpid() 134 parent_pid_file = controller.add_parent_pid(seq_name, track, ad, pid) 135 type = kwargs.get("type", None) 136 options = kwargs.get("options", {}) 137 options["parent_pid"] = pid # pass parent id to child process. 138 options["parent_pid_file"] = parent_pid_file 139 140 if type not in TRACK_FUNCS: 141 raise ValueError( 142 f'Invalid track type: {type}. Choose from: {",".join(TRACK_FUNCS.keys())}' 143 ) 144 colorfx = green 145 if command == "stop": 146 colorfx = red 147 148 # calculate total_duration of track 149 total_duration = calculate_total_duration(**options) 150 151 click.echo( 152 f"▶️ {colorfx(f'{command}ing')} ➡️ {yellow('track')}: {blue(track).ljust(25)} {yellow('audio_device')}: {blue(ad or 'default').ljust(20)} {yellow('parent_pid')}: {blue(pid).ljust(15)} {yellow('total_duration')}: {blue(total_duration)}", 153 err=True, 154 ) 155 return TRACK_FUNCS.get(type)(**options) 156 157 158@click.command( 159 context_settings=dict( 160 ignore_unknown_options=True, 161 allow_extra_args=True, 162 ) 163) 164@seq_command_arg 165@click.argument("base_config", type=click.File(), required=False) 166@seq_opts 167@click.pass_context 168def cli(context, **kwargs): 169 """ 170 Play a sequence of `chord`, `midi`, `note`, and/or `arp` tracks 171 via yaml configuration. 172 173 BASE_CONFIG is a path to a yaml file. 174 175 COMMAND can be: 176 177 play: Plays the sequence as specified in the config file 178 179 start Starts all tracks in the sequence, irregardless of their "start" times. 180 181 stop: Stop tracks in the sequence. 182 183 render: Writes out individual aiff/wav files for each track in the sequence. 184 185 echo: Pretty print the sequence config to the console. 186 187 """ 188 run(**set_config_overrides_opt(context, **kwargs))
def
run(**kwargs):
23def run(**kwargs): 24 controller.ensure_pid_log() 25 26 # process configurations 27 config_path = kwargs.pop("base_config") 28 command = kwargs.pop("command") 29 output_dir = kwargs.pop("output_dir", "./") 30 if config_path: 31 # print config 32 if command == "echo": 33 click.echo(str(config_path.read())) 34 return 35 base_config = yaml.safe_load(config_path) 36 else: 37 base_config = {"name": "*"} 38 39 globals = base_config.pop("globals", {}) 40 seq = base_config.pop("name", None) 41 if seq is None: 42 raise ArgumentError("You must set a `name` in your sequence config") 43 44 tracks = kwargs.get("tracks", None) or [] 45 audio_devices = kwargs.get("audio_devices", None) or [] 46 47 # log sequence name 48 click.echo("-" * 79, err=True) 49 click.echo(f"{red('sequence')}: {yellow(seq)}", err=True) 50 click.echo("-" * 79, err=True) 51 52 # play/start sequences/tracks 53 if command in ["play", "start", "render"]: 54 track_configs = [] 55 for track, base_track_config in base_config.get("tracks", {}).items(): 56 57 # optionally skip tracks 58 if len(tracks) and track not in tracks: 59 continue 60 61 # allow for track-specific overrides 62 config_overrides = kwargs.get("config_overrides", {}) 63 track_overrides = config_overrides.pop(track, {}) 64 config_overrides.update(track_overrides) 65 66 # create track config 67 track_options = copy.copy(globals) # start with globals 68 base_track_options = { 69 expand_opt_name(k): v 70 for k, v in base_track_config.get("options", {}).items() 71 } # expand the names of shortened sequence options 72 track_options.update(base_track_options) 73 track_options.update(config_overrides) 74 75 # optionally filter by audio device 76 ad = track_options.get("audio_device", None) 77 if len(audio_devices) and ad not in audio_devices: 78 continue 79 80 # optionally start tracks immediately 81 if command == "start": 82 track_options["start_count"] = 0 83 84 # optionally render tracks 85 if command == "render": 86 try: 87 os.makedirs(output_dir) 88 except FileExistsError: 89 pass 90 track_options["audio_output_file"] = os.path.join( 91 output_dir, 92 f"{seq}-{track}-{base_track_config['type']}.aiff", 93 ) 94 95 # construct track configs 96 base_track_config["options"] = track_options 97 track_configs.append((seq, track, ad, base_track_config, command)) 98 99 # run everything 100 for _ in Pool(len(track_configs)).imap(run_track_func, track_configs): 101 continue 102 103 if command == "stop": 104 105 if len(audio_devices): 106 for ad in audio_devices: 107 click.echo( 108 f"🛑 {red('stopping')} ➡️ all tracks on {yellow('audio_device')}: {blue(ad)}", 109 err=True, 110 ) 111 controller.stop_child_pids(seq, track=None, ad=ad) 112 113 else: 114 if not len(tracks): 115 tracks = ["*"] 116 for track in tracks: 117 click.echo( 118 f"🛑 {red('stopping')} ➡️ {yellow('track')}: { blue(track)}", 119 err=True, 120 ) 121 controller.stop_child_pids(seq, track)
def
run_track_func(item):
132def run_track_func(item): 133 seq_name, track, ad, kwargs, command = item 134 pid = os.getpid() 135 parent_pid_file = controller.add_parent_pid(seq_name, track, ad, pid) 136 type = kwargs.get("type", None) 137 options = kwargs.get("options", {}) 138 options["parent_pid"] = pid # pass parent id to child process. 139 options["parent_pid_file"] = parent_pid_file 140 141 if type not in TRACK_FUNCS: 142 raise ValueError( 143 f'Invalid track type: {type}. Choose from: {",".join(TRACK_FUNCS.keys())}' 144 ) 145 colorfx = green 146 if command == "stop": 147 colorfx = red 148 149 # calculate total_duration of track 150 total_duration = calculate_total_duration(**options) 151 152 click.echo( 153 f"▶️ {colorfx(f'{command}ing')} ➡️ {yellow('track')}: {blue(track).ljust(25)} {yellow('audio_device')}: {blue(ad or 'default').ljust(20)} {yellow('parent_pid')}: {blue(pid).ljust(15)} {yellow('total_duration')}: {blue(total_duration)}", 154 err=True, 155 ) 156 return TRACK_FUNCS.get(type)(**options)
cli = <Command cli>
Play a sequence of chord
, midi
, note
, and/or arp
tracks
via yaml configuration.
BASE_CONFIG is a path to a yaml file.
COMMAND can be:
play: Plays the sequence as specified in the config file
start Starts all tracks in the sequence, irregardless of their "start" times.
stop: Stop tracks in the sequence.
render: Writes out individual aiff/wav files for each track in the sequence.
echo: Pretty print the sequence config to the console.