saysynth.utils

Assorted utilities for use throughout saysynth

  1"""
  2Assorted utilities for use throughout `saysynth`
  3"""
  4
  5import collections
  6import math
  7import os
  8import tempfile
  9from typing import Any, Dict, List, Union
 10
 11from hashids import Hashids
 12
 13from .constants import (DEFAULT_BPM_TIME_BPM, DEFAULT_BPM_TIME_COUNT,
 14                        DEFAULT_BPM_TIME_SIG)
 15
 16hashids = Hashids(salt="saysynth", min_length=3, alphabet="42069iacabplurwtf")
 17
 18
 19def frange(start: float, stop: float, by: float, sig_digits: int = 5):
 20    """
 21    Generate a range of float values.
 22
 23    Args:
 24        start: The starting value of the range.
 25        stop: The ending value of the range.
 26        by: the amount to divide the range by.
 27        sig_digits:  The number of significant digits to use when rounding.
 28
 29    """
 30    div = math.pow(10, sig_digits)
 31    for value in range(int(start * div), int(stop * div), int(by * div)):
 32        yield round(value / div, sig_digits)
 33
 34
 35def here(f, *args):
 36    """
 37    Pass `__file__` to get the current directory and `*args` to generate a filepath relative
 38    to the current directory.
 39
 40    Args:
 41        f: Usually `__file__`
 42    """
 43    return os.path.join(os.path.dirname(os.path.abspath(f)), *args)
 44
 45
 46def make_tempfile(format: str = "txt"):
 47    """
 48    Make a tempfile
 49    Args:
 50        format: The file's suffix.
 51    """
 52    return tempfile.mkstemp(suffix=f".{format}")[-1]
 53
 54
 55def bpm_to_time(
 56    bpm: float = 120.00,
 57    count: Union[str, int, float] = 1,
 58    time_sig: str = DEFAULT_BPM_TIME_SIG,
 59) -> float:
 60    """
 61    Take a bpm, note count, and time_sig and return a length in milliseconds
 62    Args:
 63        bpm: The bpm as a float
 64        count: the count as a string, int, or float (eg: '2/1', 2, 2.0 )
 65        time_sig: The time signature as a string (eg: '4/4')
 66
 67    """
 68    if isinstance(count, str):
 69        if "/" in count:
 70            numerator, denominator = count.split("/")
 71            count = float(numerator) / float(denominator)
 72    time_segments = time_sig.split("/")
 73    return (
 74        (60.00 / float(bpm)) * float(time_segments[0]) * float(count) * 1000.0
 75    )
 76
 77
 78def rescale(
 79    x: Union[int, float],
 80    range_x: List[Union[int, float]],
 81    range_y: List[Union[int, float]],
 82    sig_digits: int = 3,
 83) -> float:
 84    """
 85    Rescale value `x` to scale `y` give the range of `x` and the range of `y`
 86
 87    Args:
 88        x: An value to rescale
 89        range_x: The range ([min, max]) of the origin scale
 90        range_y: The range ([min, max]) of the target scale
 91        sig_digits: The number of significant digits to use when rounding.
 92
 93    """
 94    # Figure out how 'wide' each range is
 95    x_min = min(range_x)
 96    y_min = min(range_y)
 97    x_span = max(range_x) - x_min
 98    y_span = max(range_y) - y_min
 99
100    # Compute the scale factor between left and right values
101    scale_factor = float(y_span) / float(x_span)
102
103    return round(y_min + (x - x_min) * scale_factor, sig_digits)
104
105
106def calculate_total_duration(**kwargs) -> str:
107    """
108    Calculate the total duration of a track/chord/etc and
109    format it as a string.
110    Args:
111        start_bpm/count/time_sig: the start of the track
112        duration_bpm/count/time_sig: the duration of the passage
113    Returns: float
114    """
115    if not kwargs.get("start_duration", None):
116        start_duration = bpm_to_time(
117            kwargs.get("start_bpm", DEFAULT_BPM_TIME_BPM),
118            kwargs.get("start_count", DEFAULT_BPM_TIME_COUNT),
119            kwargs.get("start_time_sig", DEFAULT_BPM_TIME_SIG),
120        )
121    else:
122        start_duration = kwargs.get("start_duration", 0)
123    if not kwargs.get("duration", None):
124        duration = bpm_to_time(
125            kwargs.get("duration_bpm", DEFAULT_BPM_TIME_BPM),
126            kwargs.get("duration_count", DEFAULT_BPM_TIME_COUNT),
127            kwargs.get("duration_time_sig", DEFAULT_BPM_TIME_SIG),
128        )
129    else:
130        duration = kwargs.get("duration", 0)
131
132    seconds = (start_duration + duration) / 1000.0
133    minutes = math.floor(seconds / 60.0)
134    remainder_seconds = seconds - (minutes * 60.0)
135    return f"{minutes}:{int(math.floor(remainder_seconds)):02}"
136
137
138def update_dict(d: Dict[Any, Any], u: Dict[Any, Any]) -> Dict[Any, Any]:
139    """
140    Recursively update a dictionary.
141    Args:
142        d: the dictionary to be updated
143        u: the dictionary to use in update
144    Returns: dict
145    """
146    for k, v in u.items():
147        if isinstance(v, collections.Mapping):
148            d[k] = update_dict(d.get(k, {}), v)
149        else:
150            d[k] = v
151    return d
152
153
154def random_track_name(track_type: str, **options) -> str:
155    """
156    Generate a unique track name given a type and a dict of options
157    """
158    hash_id = hashids.encode(
159        int.from_bytes((track_type + str(options)).encode(), "little")
160    )[:5]
161    return f"{track_type}-{str(hash_id)}"
def frange(start: float, stop: float, by: float, sig_digits: int = 5):
20def frange(start: float, stop: float, by: float, sig_digits: int = 5):
21    """
22    Generate a range of float values.
23
24    Args:
25        start: The starting value of the range.
26        stop: The ending value of the range.
27        by: the amount to divide the range by.
28        sig_digits:  The number of significant digits to use when rounding.
29
30    """
31    div = math.pow(10, sig_digits)
32    for value in range(int(start * div), int(stop * div), int(by * div)):
33        yield round(value / div, sig_digits)

Generate a range of float values.

Arguments:
  • start: The starting value of the range.
  • stop: The ending value of the range.
  • by: the amount to divide the range by.
  • sig_digits: The number of significant digits to use when rounding.
def here(f, *args):
36def here(f, *args):
37    """
38    Pass `__file__` to get the current directory and `*args` to generate a filepath relative
39    to the current directory.
40
41    Args:
42        f: Usually `__file__`
43    """
44    return os.path.join(os.path.dirname(os.path.abspath(f)), *args)

Pass __file__ to get the current directory and *args to generate a filepath relative to the current directory.

Arguments:
  • f: Usually __file__
def make_tempfile(format: str = 'txt'):
47def make_tempfile(format: str = "txt"):
48    """
49    Make a tempfile
50    Args:
51        format: The file's suffix.
52    """
53    return tempfile.mkstemp(suffix=f".{format}")[-1]

Make a tempfile

Arguments:
  • format: The file's suffix.
def bpm_to_time( bpm: float = 120.0, count: Union[str, int, float] = 1, time_sig: str = '4/4') -> float:
56def bpm_to_time(
57    bpm: float = 120.00,
58    count: Union[str, int, float] = 1,
59    time_sig: str = DEFAULT_BPM_TIME_SIG,
60) -> float:
61    """
62    Take a bpm, note count, and time_sig and return a length in milliseconds
63    Args:
64        bpm: The bpm as a float
65        count: the count as a string, int, or float (eg: '2/1', 2, 2.0 )
66        time_sig: The time signature as a string (eg: '4/4')
67
68    """
69    if isinstance(count, str):
70        if "/" in count:
71            numerator, denominator = count.split("/")
72            count = float(numerator) / float(denominator)
73    time_segments = time_sig.split("/")
74    return (
75        (60.00 / float(bpm)) * float(time_segments[0]) * float(count) * 1000.0
76    )

Take a bpm, note count, and time_sig and return a length in milliseconds

Arguments:
  • bpm: The bpm as a float
  • count: the count as a string, int, or float (eg: '2/1', 2, 2.0 )
  • time_sig: The time signature as a string (eg: '4/4')
def rescale( x: Union[int, float], range_x: List[Union[int, float]], range_y: List[Union[int, float]], sig_digits: int = 3) -> float:
 79def rescale(
 80    x: Union[int, float],
 81    range_x: List[Union[int, float]],
 82    range_y: List[Union[int, float]],
 83    sig_digits: int = 3,
 84) -> float:
 85    """
 86    Rescale value `x` to scale `y` give the range of `x` and the range of `y`
 87
 88    Args:
 89        x: An value to rescale
 90        range_x: The range ([min, max]) of the origin scale
 91        range_y: The range ([min, max]) of the target scale
 92        sig_digits: The number of significant digits to use when rounding.
 93
 94    """
 95    # Figure out how 'wide' each range is
 96    x_min = min(range_x)
 97    y_min = min(range_y)
 98    x_span = max(range_x) - x_min
 99    y_span = max(range_y) - y_min
100
101    # Compute the scale factor between left and right values
102    scale_factor = float(y_span) / float(x_span)
103
104    return round(y_min + (x - x_min) * scale_factor, sig_digits)

Rescale value x to scale y give the range of x and the range of y

Arguments:
  • x: An value to rescale
  • range_x: The range ([min, max]) of the origin scale
  • range_y: The range ([min, max]) of the target scale
  • sig_digits: The number of significant digits to use when rounding.
def calculate_total_duration(**kwargs) -> str:
107def calculate_total_duration(**kwargs) -> str:
108    """
109    Calculate the total duration of a track/chord/etc and
110    format it as a string.
111    Args:
112        start_bpm/count/time_sig: the start of the track
113        duration_bpm/count/time_sig: the duration of the passage
114    Returns: float
115    """
116    if not kwargs.get("start_duration", None):
117        start_duration = bpm_to_time(
118            kwargs.get("start_bpm", DEFAULT_BPM_TIME_BPM),
119            kwargs.get("start_count", DEFAULT_BPM_TIME_COUNT),
120            kwargs.get("start_time_sig", DEFAULT_BPM_TIME_SIG),
121        )
122    else:
123        start_duration = kwargs.get("start_duration", 0)
124    if not kwargs.get("duration", None):
125        duration = bpm_to_time(
126            kwargs.get("duration_bpm", DEFAULT_BPM_TIME_BPM),
127            kwargs.get("duration_count", DEFAULT_BPM_TIME_COUNT),
128            kwargs.get("duration_time_sig", DEFAULT_BPM_TIME_SIG),
129        )
130    else:
131        duration = kwargs.get("duration", 0)
132
133    seconds = (start_duration + duration) / 1000.0
134    minutes = math.floor(seconds / 60.0)
135    remainder_seconds = seconds - (minutes * 60.0)
136    return f"{minutes}:{int(math.floor(remainder_seconds)):02}"

Calculate the total duration of a track/chord/etc and format it as a string.

Arguments:
  • start_bpm/count/time_sig: the start of the track
  • duration_bpm/count/time_sig: the duration of the passage

Returns: float

def update_dict(d: Dict[Any, Any], u: Dict[Any, Any]) -> Dict[Any, Any]:
139def update_dict(d: Dict[Any, Any], u: Dict[Any, Any]) -> Dict[Any, Any]:
140    """
141    Recursively update a dictionary.
142    Args:
143        d: the dictionary to be updated
144        u: the dictionary to use in update
145    Returns: dict
146    """
147    for k, v in u.items():
148        if isinstance(v, collections.Mapping):
149            d[k] = update_dict(d.get(k, {}), v)
150        else:
151            d[k] = v
152    return d

Recursively update a dictionary.

Arguments:
  • d: the dictionary to be updated
  • u: the dictionary to use in update

Returns: dict

def random_track_name(track_type: str, **options) -> str:
155def random_track_name(track_type: str, **options) -> str:
156    """
157    Generate a unique track name given a type and a dict of options
158    """
159    hash_id = hashids.encode(
160        int.from_bytes((track_type + str(options)).encode(), "little")
161    )[:5]
162    return f"{track_type}-{str(hash_id)}"

Generate a unique track name given a type and a dict of options