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.