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