Module musx.tools
An assortmant of tools for working with envlopes, randomness, rescaling, arithmetic, etc.
Expand source code
###############################################################################
"""
An assortmant of tools for working with envlopes, randomness, rescaling, 
arithmetic, etc.
"""
import types
import math
import random
import subprocess
from functools import reduce
__pdoc__ = {
    'parse_string_sequence': False
}
def isgen(x):
    """Returns True if x is a generator."""
    return isinstance(x, types.GeneratorType)
def isfunc(x):
    """Returns True if x is a function."""
    return isinstance(x, types.FunctionType)
def isseq(x):
    """Returns True if x is a list or tuple."""
    return isinstance(x, (list, tuple))
def isnum(x, others=None):
    """
    Returns True if x is an int or float of one of the
    other number types passed in (e.g. Fraction, complex).
    """
    if isinstance(x, (int, float)):
        return True
    if others:
        return isinstance(x, others)
    return False
def rescale(x, x1, x2, y1, y2, mode='lin'):
    """
    Maps the value x ranging between x1 and x2 into a proportional value
    between y1 and y2.
    
    Parameters
    ----------
    x : int | float
        The value to rescale.
    x1 : int | float
        The lower limit of input range.
    x2 : int | float
        The upper limit of input range.
    y1 : int | float
        The lowest limit of output range.
    y2 : int | float
        The upper limit of output range.
    mode : string
        If mode is 'lin' then linear scaling occurs, 'cos' produces cosine
        scaling, 'exp' produces exponential scaling, and '-exp' produces 
        inverted exponential.
    Returns
    -------
    The rescaled value.
    """
    if x > x2: return y2
    if x <= x1: return y1
    # as x moves x1 to x2 mu moves 0 to 1
    mu = (x - x1) / (x2 - x1)
    if mode == 'lin':
        #return (((y2 - y1) / (x2 - x1)) * (x - x1)) + y1
        return (y1 * (1 - mu) + (y2 * mu))
    elif mode == 'cos':
        mu2 = (1 - math.cos(mu * math.pi)) / 2
        return (y1 * (1 - mu2) + y2 * mu2)
    elif mode in ['exp','-exp']:
        # # http://www.pmean.com/10/ExponentialInterpolation.html
        # if y1==0: y1=0.00001
        # return y1 * ((y2/y1) ** (mu))
        # #https://docs.fincad.com/support/developerfunc/mathref/Interpolation.htm
        # if y1==0: y1=0.00001
        # m = math.log(y2 / y1) / (x2 - x1)
        # k = y1 * math.pow(math.e, -m * x1)
        # return k * math.pow(math.e, m * x)
        # a base that yields a slope that is not too steep or shallow...
        b = 512
        if mode == 'exp':
            return y1 + ( ((y2 - y1) / b) * math.pow(b, mu) )
        b = 1/b
        return y1 + ( ((y2 - y1) / (b - 1)) * (math.pow(b, mu) - 1) )
    raise ValueError(f"mode {mode} is not 'lin', 'cos', 'exp', or '-exp'.")
def interp(x, *xys, mode='lin', mul=None, add=None):
    """
    A function that interpolates a y value for a given x in a
    series of x,y coordinate pairs. If x is not within bounds
    then the first or last y value is returned.
    Parameters
    ----------
    x : int | float
        The x value to interpolate in the sequence of x y values. 
    xys : series of int or float | list
        Either a series of in-line x, y values representing the envelope
        or a single list of x y coordinate pairs.
    mode : 'lin' | 'cos' | 'exp' | '-exp'
        A string that specifies the type of interpolation performed;
        'lin' is linear, 'cos' is cosine, 'exp' is exponential and 
        '-exp' is inverted exponential. The default is 'lin'. Note
        that if specified the value must provided as an explicit
        keyword arg, e.g. mode='cos'.
    mul : None | number
        A value to multiply the result by.
    add : None | number
        A value to add to the result after any multiplication.
    Returns
    -------
    The interpolated value of x.
    """
    if len(xys) == 1 and isinstance(xys[0], (tuple,list)):
        xys = xys[0]
    if not xys or len(xys) & 1:
        raise ValueError(f"coordinates not x y pairs: {xys}.")
    # find the segment in the coordinates that contains the given x
    xr,yr = xys[0:2]
    xl,yl = xr,yr
    # iterate remaining pairs of x y values stepping by 2
    for wx,wy in zip(xys[2::2], xys[3::2]):
        if xr > x: break
        xl,yl = xr,yr
        xr,yr = wx,wy
    # print(x, xl, xr, yl, yr, mode)
    val = rescale(x, xl, xr, yl, yr, mode)
    if mul: val *= mul
    if add: val += add
    return val
def rescale_env(env, newxmin=None, newxmax=None, newymin=None, newymax=None,*, mode=None):
    '''
    Rescales the current x and/or y envelope coordinates of the given envelope to proportional
    values lying between the given new minima and maxima. If newxmin or newxmax are unspecified
    they inherit the current x minimum and maximum values. The same is true for y values.
    If mode is specfied it is applied to the y values only. See
    `rescale()` for information about mode.
    Parameters:
    -----------
    env : list 
        A list of x y coordinate values: [x1,y1,x2,y2,...xn,yn] where
        x values are monotoniclly increasing values from left to right.
    newxmin : int | float | None
        The new minimum x value. If None it defaults to the current minimum x value.
    newxmax : int | float | None
        The new maximum x value. If None it defaults to the current maximum x value.
    newymin : int | float | None
        The new minimum y value. If None it defaults to the current minimum y value.
    newymax : int | float | None
        The new maximum y value. If None it defaults to the current maximum y value.
    mode : string
        See `rescale()` for possible mode values.
    Returns
    -------
    A rescaled x,y envelope.
    '''
    #print(f"env:  {env}")
    if len(env) % 2 != 0:
        raise ValueError(f"env {env} does not have an even number of values.")
    xdata, ydata = env[::2], env[1::2]
    #print(f"xdata: {xdata}, ydata: {ydata}")
    for x1,x2 in zip(xdata,xdata[1:]):
        if x2<x1:
            raise ValueError(f"x value {x2} out of order in xdata: {xdata}.")
    oldxmin, oldxmax = xdata[0], xdata[-1]
    ##print(f"oldxmin: {oldxmin}, oldxmax: {oldxmax}")
    if newxmin is None: newxmin = oldxmin
    if newxmax is None: newxmax = oldxmax
    if not newxmin < newxmax:
        raise ValueError(f"newxmin value {newxmin} is not less than newxmax value {newxmax}.")
    #print(f"oldxmin: {oldxmin}, oldxmax: {oldxmax}, newxmin: {newxmin} newxmax: {newxmax}")
    oldymin = reduce(lambda a, b: a if a < b else b, ydata)
    oldymax = reduce(lambda a, b: a if a > b else b, ydata)
    ##print(f"oldymin: {oldymin}, oldymax: {oldymax}")
    if newymin is None: newymin = oldymin
    if newymax is None: newymax = oldymax
    if mode is None: mode = "lin"
    #print(f"oldymin: {oldymin}, oldymax: {oldymax}, newymin: {newymin}, newymax: {newymax}")
    res = []
    for x,y in zip(xdata,ydata):
       res.extend([rescale(x, oldxmin, oldxmax, newxmin, newxmax),
                   rescale(y, oldymin, oldymax, newymin, newymax, mode)])
    return res
def exp_env(segments = 10, base = 2, flip = False, reverse = False, scaler=1, offset=0):
    """
    Returns a normalized exponential envelope, e.g. both axes range 0 to 1.
    As x goes from 0 to segments, y is assigned 1/base**x
    
    Parameters
    ----------
    segments : int
        The number of segments in the envelope, defaults to 10.
    base : int | float
        The base for the exponential, defaults to 2. 
    flip : boolean
        If flip is true y values are inverted.
    reverse : boolean
        If reverse is true x values are retrograded.
    
    Returns
    -------
    An exponential x,y envelope with both axes ranging 0 -> 1.
    """
    xvalues = [x/segments for x in range(0, segments+1, 1)]
    #print("xvalues:", xvalues)
    yvalues = [1/(base**(x)) for x in range(segments+1)]
    if flip:
        for i in range(len(yvalues)):
            yvalues[i] = 1 - yvalues[i]
    if reverse:
        for i in range(len(xvalues)):
            xvalues[i] = 1 - xvalues[i]
    #yvalues[-1] = 0.0
    #print("yvalues:", yvalues)
    env = []
    for x,y in zip(xvalues, yvalues):
        env.extend([x,y * scaler + offset])
    #print("expenv:", env)
    return env
def segment_env(env, dur, attack, release=[]):
    '''
    Returns a version of env with attack and (optionally) decay segments lasting specific amounts of time.
    Parameters
    ----------
    env : list
        The x,y envelope.
    dur : int | float
        The total duration in seconds of the envelope.
    attack : [ _attack_x, attack_dur_ ]
        The ending x coordinate and duration of the attack segment. 
    release : [ _release_x, release_dur_ ] | []
        The starting x coordinate and duration of the release segment.
    '''    
    # maximum x value in envelope.
    x_max = env[-2]
    # get the index positions of the attack and release segments.
    # divides by 2 because x_values will be half the length of env.
    try: 
        attack_index = int(env.index(attack[0])/2)
        attack_dur = attack[1]
    except ValueError as e: 
        raise Exception(f"Attack coordinate {attack[0]} is not in {env}.") from e
    if release:
        try: 
            release_index = int(env.index(release[0])/2)
            release_dur = release[1]
        except ValueError as e:
            raise Exception(f"Release coordinate {release[0]} is not in {env}.") from e     
    # split x and y coords into separate lists with x_values converted to time.
    x_values, y_values = [(x / x_max) * dur for x in env[::2]], env[1::2]
    # rescale x coords from index 0 to att_index to fit in attack_dur time.
    for i in range(0, attack_index+1):
         x_values[i] = rescale(x_values[i], x_values[0], x_values[attack_index], x_values[0], attack_dur)
    # rescale backwards for release values.
    if release:
        end_time = x_values[-1]
        for i in range(len(x_values)-1, release_index-1, -1):
             x_values[i] = rescale(x_values[i], x_values[release_index], end_time, end_time - release_dur, end_time)
    # return new envelope with durations converted back to user's original coordinates.
    env = []
    for x,y in zip(x_values, y_values):
        env.extend([(x / dur) * x_max, y])
    return env
def frange(start, stop=None, step=None):
    """
    Returns an iterator producing a series of floats from start (inclusive)
    to stop (exclusive) by step.
    Parameters
    ----------
    frange can be called with one, two or three arguments:
    
    * frange(stop)
    * frange(start, stop)
    * frange(start, stop, step)
    start : int | float
        The starting value of the sequence.
    stop : int | float
        The exclusive upper (or lower) bounds of the iteration.
    step : int | float
        The increment (or decrement) to move by. Defaults to 1.
    Returns
    -------
    Values ranging from start to stop (exclusive).
    """
    if stop is None:
        stop = start
        start = 0.0
    else:
        start += 0.0
    if step is None:
        step = 1.0
    else:
        step += 0.0
    i = 1
    init = start # initial start value (offset)
    # no iteration if step is 0 or reversed direction of start -> stop.
    while step != 0:
        if step > 0 and start >= stop:
            break
        if step < 0 and start <= stop:
            break
        yield start
        # start += step   # arrg! cumulative addition yields wonky numbers
        start = (step * i) + init  # scale by step and shift to start
        i += 1
def rand(limit):
    """
    Returns a generator that produces uniform random numbers below limit.
    Parameters
    ----------
    limit : int | float
        Sets the exlusive upper bound for random selection. If limit
        is an integer then integer values are returned otherwise float
        values are returned.
    """
    if isinstance(limit, int):
        def irand():
            while True:
                yield random.randrange(limit)
        return irand()
    elif isinstance(limit, float):
        def frand():
            while True:
                yield random.random() * limit
        return frand()
    raise TypeError("limit not an int or float: {limit}.")
def quantize(number, stepsize):
    """Quantizes number to a given step size."""
    return math.floor( (number/stepsize) + .5) * stepsize
def deltas(numbers):
    """
    Returns the changes between consecutive numbers in a list of numbers.
    Example: deltas([1,5,3])  ->  [4, -2]
    Parameters
    ----------
    numbers : list
        The list of numbers to process.
    Returns
    -------
    A list containing the differences between a series of numbers.
    """
    return [l - r for l,r in zip(numbers[1:], numbers[:])]
def _expl(powr, y0, y1, base):
    if powr < 0:
        powr=0.0
    elif powr > 1: 
        powr = 1.0
    if base == 1.0:
        return y0 + (powr * (y1 - y0))
    return y0 + ( ( (y1 - y0) / (base - 1.0) ) * ((base ** powr) - 1.0) )
def _explseg(i, length, summ, powr):
    if i >= length:
        i += -1
    x1 = (i+1) / length
    x2 = i / length
    f1 = _expl(x1, 0.0, 1.0, powr)
    f2 = 0.0 if (i <= 0) else _expl( x2, 0.0, 1.0, powr)
    return summ * (f1 - f2)
def explsegs(num, summ, base=2):
    segs = []
    for i in range(num):
        segs.append(_explseg(i, num, summ, base))
    return segs
def _geoseg( i, length, summ, base):
    if length == 0:
        return 0.0
    a = summ * ((1.0 - base) / (1.0 - (base ** length)))
    return  a * (base ** i)
def geosegs(num, summ, base=2):
    segs = []
    for i in range(num):
        segs.append(_geoseg(i, num, summ, base))
    return segs
def _map_lists(func, left, right):
    if type(left) is list:
        if type(right) is list:
            assert len(left) == len(right), "lists are different lengths."
            return [func(l,r) for l,r in zip(left,right)]
        else:
            return [func(l,right) for l in left]
    else:
        if type(right) is list:
            return [func(left,r) for r in right]
        else:
            return func(left,right)
    
def multiply(left, right):
    """
    List-aware multiplication.
    
    Left and right can be numbers or lists, if both are lists
    they must be of the same length.
    """
    return _map_lists(lambda l,r: l*r, left, right)
def add(left, right):
    """
    List-aware addition.
    
    Left and right can be numbers or lists, if both are lists
    they must be of the same length.
    """
    return _map_lists(lambda l,r: l+r, left, right)
def subtract(left, right):
    """
    List-aware subtraction.
    
    Left and right can be numbers or lists, if both are lists
    they must be of the same length.
    """
    return _map_lists(lambda l,r: l-r, left, right)
def divide(left, right):
    """
    List-aware division.
    
    Left and right can be numbers or lists, if both are lists
    they must be of the same length.
    """
    return _map_lists(lambda l,r: l/r, left, right)
def _rem(x,y):
    #return math.copysign(x % y, x)
    mod = x % y
    res = math.copysign(mod, x)
    return int(res) if isinstance(mod, int) else res
def fit(num, lb, ub, mode='wrap'):
    """
    Forces a number to lie between a lower and upper bound according to mode. 
    Parameters
    ----------
    num : int | float
        The number to fit.
    lb : int | float
        The lower bound.
    ub : int | float
        The upper bound.
    mode : 'reflect' | 'limit' | 'wrap'
        If mode is 'reflect' then the min and max boundaries reflect the value back into range.\
        If mode is 'wrap' then num will be the remainder of num % boundaries.
    Returns
    -------
    The value of num coerced to lie within the range lb to ub.
    Raises
    ------
    ValueError if mode is not one of the supported modes.
    Examples
    --------
    ```python
    [fit(i, 0, 10) for i in range(-20, 21, 5)]
    ```
    """
    if lb > ub:
        ub, lb = lb, ub
    if lb <= num <= ub:
        return num
    b = ub if num > ub else lb
    if mode == 'limit':
        return b
    rng = ub - lb
    if mode == 'reflect':
        # shift num to 0 to compare with range
        # limit num to rng*2 (rising/reflecting)
        num = _rem(num - b, (rng * 2))
        if abs(num) > rng:   # in range2
            if num >= 0:
                num = num - (rng * 2)
            else:
                num = num + (rng * 2)
        else:
            num = -num
        return num + b
    if mode == 'wrap':
        return (lb if b == ub else ub) + _rem(num - b, rng)
    raise ValueError(f"{mode} not one of ['reflect', 'limit', 'wrap'].")
'''
(defun fit (number lb ub &optional (mode :reflect))
  (when (> lb ub) (rotatef lb ub))
  (if (<= lb number ub)
      number
      (let ((b (if (> number ub) ub lb)) (r (- ub lb)))
        (case mode
          ((:limit) b)
          ((:reflect)
           (let* ((2r (* 2 r)) 
                  (v (rem (- number b) 2r)))
             (+ (if (> (abs v) r)
                    (funcall (if (>= v 0) #'- #'+) v 2r)
                    (- v))
                b)))
          ((:wrap) (+ (if (= b ub) lb ub) (rem (- number b) r)))
          (t (error "~s is not :limit, :reflect or :wrap" mode))))))
'''
midiextensions = ('.mid', '.midi')
"""
A tuple of allowable midi file extensions. Defaults to ('.mid', '.midi').
"""
midiplayer = ''
def setmidiplayer(command):
    """
    Assign a shell command (string) that will play a midi file.
    Parameter
    ---------
    command : string
        The shell command that will play a midi file.
    Example
    -------
    setmidiplayer('fluidsynth -iq -g1 /Users/taube/SoundFonts/MuseScore_General.sf2')
    """    
    
    global midiplayer
    midiplayer = command
audioextensions = ('.aiff', '.wav', '.mp3', '.mp4')
"""
A tuple of allowable audio file extensions. 
Defaults to ('.aiff', '.wav', '.mp3', '.mp4').
"""
audioplayer = ''
"""
A shell command (string) that accepts one argument,
a pathname (file) to play. See: setaudioplayer().
"""
def setaudioplayer(command):
    """
    Assign a shell command (string) that will play an audio file.
    Parameter
    ---------
    command : string
        The shell command that will play an audio file.
    Example
    -------
    setaudioplayer('afplay')
    """    
    
    global audioplayer
    audioplayer = command
def playfile(file, wait=False):
    """
    Plays a midi or audio file using the shell commands you have specified
    for midiplayer and audioplayer. See: setaudioplayer, setmidiplayer.
    Parameters
    ----------
    file : string
        The file to play.
    wait : bool
        If true playfile waits until the file has finished playing
        before returning, otherwise playfile returns immediately.
    """
    args = []
    kind = ''
    if file.endswith(midiextensions):
        kind = 'midi'
        if midiplayer:
            args = midiplayer.split() + [file]
    elif file.endswith(audioextensions):
        kind = 'audio'
        if audioplayer:
            args = audioplayer.split() + [file] 
    if args:
        p = subprocess.Popen(args)
        if wait:
            p.wait()
    else:
        help = f"playfile(): Don't know how to play '{file}':"
        if kind:
            help += f" use musx.set{kind}player() to set a shell command to play {kind} files."
        else:
            help += f" file type not found in musx.midiextensions or musx.audioextensions."
        print(help)
# musx.tools.parse_string_sequence("e*3")
# musx.tools.parse_string_sequence("e fs*3")
#musx.pitch("a4, g3*3 e f")  $ BUG (RECURSION)
def parse_string_sequence(string):
    # split string at blank spaces, by
    # repeated value, check for dangling and undelimited repeats.
    seq = []
    for raw in string.split():
        pos = raw.rfind('*')
        if pos > -1:    # string has * somewhere
            split = raw.split('*')
            if '' in split or len(split) != 2:
                raise SyntaxError(f"Invalid expansion  '{raw}'.")
            sym, rep = split[0], split[1]
            try:
                val = int(rep)
            except Exception as ve:
                ve.args = (f"Invalid expansion factor '{rep}' in '{raw}'.",)
                raise
            if val < 1 or val > 32:
                raise ValueError(f"Expansion factor '{val}' is not between 1 and 32.")
            else:
                for _ in range(val):
                    seq.append(sym)
        else:
            seq.append(raw)
    return seq
def histo(data):
    """
    Returns a dictionary containing a histogram of the input data.
    
    Parameters
    ----------
    data : list
        The list of data to analyze.
    
    Example
    -------
    >>> histo([3,4,1,1,2,1,5,6,5])
    {3: 1, 4: 1, 1: 3, 2: 1, 5: 2, 6: 1}
    """
    if isinstance(data, list):
        hist = {}
        for d in data:
            try: hist[d] += 1
            except: hist[d] = 1
        return hist
    raise TypeError ("Histogram data is not a list.")
Global variables
var audioextensions- 
A tuple of allowable audio file extensions. Defaults to ('.aiff', '.wav', '.mp3', '.mp4').
 var audioplayer- 
A shell command (string) that accepts one argument, a pathname (file) to play. See: setaudioplayer().
 var midiextensions- 
A tuple of allowable midi file extensions. Defaults to ('.mid', '.midi').
 
Functions
def add(left, right)- 
List-aware addition.
Left and right can be numbers or lists, if both are lists they must be of the same length.
Expand source code
def add(left, right): """ List-aware addition. Left and right can be numbers or lists, if both are lists they must be of the same length. """ return _map_lists(lambda l,r: l+r, left, right) def deltas(numbers)- 
Returns the changes between consecutive numbers in a list of numbers.
Example: deltas([1,5,3]) -> [4, -2]
Parameters
numbers:list- The list of numbers to process.
 
Returns
A list containing the differences between a series of numbers.
Expand source code
def deltas(numbers): """ Returns the changes between consecutive numbers in a list of numbers. Example: deltas([1,5,3]) -> [4, -2] Parameters ---------- numbers : list The list of numbers to process. Returns ------- A list containing the differences between a series of numbers. """ return [l - r for l,r in zip(numbers[1:], numbers[:])] def divide(left, right)- 
List-aware division.
Left and right can be numbers or lists, if both are lists they must be of the same length.
Expand source code
def divide(left, right): """ List-aware division. Left and right can be numbers or lists, if both are lists they must be of the same length. """ return _map_lists(lambda l,r: l/r, left, right) def exp_env(segments=10, base=2, flip=False, reverse=False, scaler=1, offset=0)- 
Returns a normalized exponential envelope, e.g. both axes range 0 to 1. As x goes from 0 to segments, y is assigned 1/base**x
Parameters
segments:int- The number of segments in the envelope, defaults to 10.
 base:int | float- The base for the exponential, defaults to 2.
 flip:boolean- If flip is true y values are inverted.
 reverse:boolean- If reverse is true x values are retrograded.
 
Returns
An exponential x,y envelope with both axes ranging 0 -> 1.
Expand source code
def exp_env(segments = 10, base = 2, flip = False, reverse = False, scaler=1, offset=0): """ Returns a normalized exponential envelope, e.g. both axes range 0 to 1. As x goes from 0 to segments, y is assigned 1/base**x Parameters ---------- segments : int The number of segments in the envelope, defaults to 10. base : int | float The base for the exponential, defaults to 2. flip : boolean If flip is true y values are inverted. reverse : boolean If reverse is true x values are retrograded. Returns ------- An exponential x,y envelope with both axes ranging 0 -> 1. """ xvalues = [x/segments for x in range(0, segments+1, 1)] #print("xvalues:", xvalues) yvalues = [1/(base**(x)) for x in range(segments+1)] if flip: for i in range(len(yvalues)): yvalues[i] = 1 - yvalues[i] if reverse: for i in range(len(xvalues)): xvalues[i] = 1 - xvalues[i] #yvalues[-1] = 0.0 #print("yvalues:", yvalues) env = [] for x,y in zip(xvalues, yvalues): env.extend([x,y * scaler + offset]) #print("expenv:", env) return env def explsegs(num, summ, base=2)- 
Expand source code
def explsegs(num, summ, base=2): segs = [] for i in range(num): segs.append(_explseg(i, num, summ, base)) return segs def fit(num, lb, ub, mode='wrap')- 
Forces a number to lie between a lower and upper bound according to mode.
Parameters
num:int | float- The number to fit.
 lb:int | float- The lower bound.
 ub:int | float- The upper bound.
 mode:'reflect' | 'limit' | 'wrap'- If mode is 'reflect' then the min and max boundaries reflect the value back into range. If mode is 'wrap' then num will be the remainder of num % boundaries.
 
Returns
The value of num coerced to lie within the range lb to ub.
Raises
ValueError if mode is not one of the supported modes.
Examples
[fit(i, 0, 10) for i in range(-20, 21, 5)]Expand source code
def fit(num, lb, ub, mode='wrap'): """ Forces a number to lie between a lower and upper bound according to mode. Parameters ---------- num : int | float The number to fit. lb : int | float The lower bound. ub : int | float The upper bound. mode : 'reflect' | 'limit' | 'wrap' If mode is 'reflect' then the min and max boundaries reflect the value back into range.\ If mode is 'wrap' then num will be the remainder of num % boundaries. Returns ------- The value of num coerced to lie within the range lb to ub. Raises ------ ValueError if mode is not one of the supported modes. Examples -------- ```python [fit(i, 0, 10) for i in range(-20, 21, 5)] ``` """ if lb > ub: ub, lb = lb, ub if lb <= num <= ub: return num b = ub if num > ub else lb if mode == 'limit': return b rng = ub - lb if mode == 'reflect': # shift num to 0 to compare with range # limit num to rng*2 (rising/reflecting) num = _rem(num - b, (rng * 2)) if abs(num) > rng: # in range2 if num >= 0: num = num - (rng * 2) else: num = num + (rng * 2) else: num = -num return num + b if mode == 'wrap': return (lb if b == ub else ub) + _rem(num - b, rng) raise ValueError(f"{mode} not one of ['reflect', 'limit', 'wrap'].") def frange(start, stop=None, step=None)- 
Returns an iterator producing a series of floats from start (inclusive) to stop (exclusive) by step.
Parameters
frange can be called with one, two or three arguments:
- frange(stop)
 - frange(start, stop)
 - frange(start, stop, step)
 
start:int | float- The starting value of the sequence.
 stop:int | float- The exclusive upper (or lower) bounds of the iteration.
 step:int | float- The increment (or decrement) to move by. Defaults to 1.
 
Returns
Values ranging from start to stop (exclusive).
Expand source code
def frange(start, stop=None, step=None): """ Returns an iterator producing a series of floats from start (inclusive) to stop (exclusive) by step. Parameters ---------- frange can be called with one, two or three arguments: * frange(stop) * frange(start, stop) * frange(start, stop, step) start : int | float The starting value of the sequence. stop : int | float The exclusive upper (or lower) bounds of the iteration. step : int | float The increment (or decrement) to move by. Defaults to 1. Returns ------- Values ranging from start to stop (exclusive). """ if stop is None: stop = start start = 0.0 else: start += 0.0 if step is None: step = 1.0 else: step += 0.0 i = 1 init = start # initial start value (offset) # no iteration if step is 0 or reversed direction of start -> stop. while step != 0: if step > 0 and start >= stop: break if step < 0 and start <= stop: break yield start # start += step # arrg! cumulative addition yields wonky numbers start = (step * i) + init # scale by step and shift to start i += 1 def geosegs(num, summ, base=2)- 
Expand source code
def geosegs(num, summ, base=2): segs = [] for i in range(num): segs.append(_geoseg(i, num, summ, base)) return segs def histo(data)- 
Returns a dictionary containing a histogram of the input data.
Parameters
data:list- The list of data to analyze.
 
Example
>>> histo([3,4,1,1,2,1,5,6,5]) {3: 1, 4: 1, 1: 3, 2: 1, 5: 2, 6: 1}Expand source code
def histo(data): """ Returns a dictionary containing a histogram of the input data. Parameters ---------- data : list The list of data to analyze. Example ------- >>> histo([3,4,1,1,2,1,5,6,5]) {3: 1, 4: 1, 1: 3, 2: 1, 5: 2, 6: 1} """ if isinstance(data, list): hist = {} for d in data: try: hist[d] += 1 except: hist[d] = 1 return hist raise TypeError ("Histogram data is not a list.") def interp(x, *xys, mode='lin', mul=None, add=None)- 
A function that interpolates a y value for a given x in a series of x,y coordinate pairs. If x is not within bounds then the first or last y value is returned.
Parameters
x:int | float- The x value to interpolate in the sequence of x y values.
 xys:seriesofintorfloat | list- Either a series of in-line x, y values representing the envelope or a single list of x y coordinate pairs.
 mode:'lin' | 'cos' | 'exp' | '-exp'- A string that specifies the type of interpolation performed; 'lin' is linear, 'cos' is cosine, 'exp' is exponential and '-exp' is inverted exponential. The default is 'lin'. Note that if specified the value must provided as an explicit keyword arg, e.g. mode='cos'.
 mul:None | number- A value to multiply the result by.
 add:None | number- A value to add to the result after any multiplication.
 
Returns
The interpolated value of x.
Expand source code
def interp(x, *xys, mode='lin', mul=None, add=None): """ A function that interpolates a y value for a given x in a series of x,y coordinate pairs. If x is not within bounds then the first or last y value is returned. Parameters ---------- x : int | float The x value to interpolate in the sequence of x y values. xys : series of int or float | list Either a series of in-line x, y values representing the envelope or a single list of x y coordinate pairs. mode : 'lin' | 'cos' | 'exp' | '-exp' A string that specifies the type of interpolation performed; 'lin' is linear, 'cos' is cosine, 'exp' is exponential and '-exp' is inverted exponential. The default is 'lin'. Note that if specified the value must provided as an explicit keyword arg, e.g. mode='cos'. mul : None | number A value to multiply the result by. add : None | number A value to add to the result after any multiplication. Returns ------- The interpolated value of x. """ if len(xys) == 1 and isinstance(xys[0], (tuple,list)): xys = xys[0] if not xys or len(xys) & 1: raise ValueError(f"coordinates not x y pairs: {xys}.") # find the segment in the coordinates that contains the given x xr,yr = xys[0:2] xl,yl = xr,yr # iterate remaining pairs of x y values stepping by 2 for wx,wy in zip(xys[2::2], xys[3::2]): if xr > x: break xl,yl = xr,yr xr,yr = wx,wy # print(x, xl, xr, yl, yr, mode) val = rescale(x, xl, xr, yl, yr, mode) if mul: val *= mul if add: val += add return val def isfunc(x)- 
Returns True if x is a function.
Expand source code
def isfunc(x): """Returns True if x is a function.""" return isinstance(x, types.FunctionType) def isgen(x)- 
Returns True if x is a generator.
Expand source code
def isgen(x): """Returns True if x is a generator.""" return isinstance(x, types.GeneratorType) def isnum(x, others=None)- 
Returns True if x is an int or float of one of the other number types passed in (e.g. Fraction, complex).
Expand source code
def isnum(x, others=None): """ Returns True if x is an int or float of one of the other number types passed in (e.g. Fraction, complex). """ if isinstance(x, (int, float)): return True if others: return isinstance(x, others) return False def isseq(x)- 
Returns True if x is a list or tuple.
Expand source code
def isseq(x): """Returns True if x is a list or tuple.""" return isinstance(x, (list, tuple)) def multiply(left, right)- 
List-aware multiplication.
Left and right can be numbers or lists, if both are lists they must be of the same length.
Expand source code
def multiply(left, right): """ List-aware multiplication. Left and right can be numbers or lists, if both are lists they must be of the same length. """ return _map_lists(lambda l,r: l*r, left, right) def playfile(file, wait=False)- 
Plays a midi or audio file using the shell commands you have specified for midiplayer and audioplayer. See: setaudioplayer, setmidiplayer.
Parameters
file:string- The file to play.
 wait:bool- If true playfile waits until the file has finished playing before returning, otherwise playfile returns immediately.
 
Expand source code
def playfile(file, wait=False): """ Plays a midi or audio file using the shell commands you have specified for midiplayer and audioplayer. See: setaudioplayer, setmidiplayer. Parameters ---------- file : string The file to play. wait : bool If true playfile waits until the file has finished playing before returning, otherwise playfile returns immediately. """ args = [] kind = '' if file.endswith(midiextensions): kind = 'midi' if midiplayer: args = midiplayer.split() + [file] elif file.endswith(audioextensions): kind = 'audio' if audioplayer: args = audioplayer.split() + [file] if args: p = subprocess.Popen(args) if wait: p.wait() else: help = f"playfile(): Don't know how to play '{file}':" if kind: help += f" use musx.set{kind}player() to set a shell command to play {kind} files." else: help += f" file type not found in musx.midiextensions or musx.audioextensions." print(help) def quantize(number, stepsize)- 
Quantizes number to a given step size.
Expand source code
def quantize(number, stepsize): """Quantizes number to a given step size.""" return math.floor( (number/stepsize) + .5) * stepsize def rand(limit)- 
Returns a generator that produces uniform random numbers below limit.
Parameters
limit:int | float- Sets the exlusive upper bound for random selection. If limit is an integer then integer values are returned otherwise float values are returned.
 
Expand source code
def rand(limit): """ Returns a generator that produces uniform random numbers below limit. Parameters ---------- limit : int | float Sets the exlusive upper bound for random selection. If limit is an integer then integer values are returned otherwise float values are returned. """ if isinstance(limit, int): def irand(): while True: yield random.randrange(limit) return irand() elif isinstance(limit, float): def frand(): while True: yield random.random() * limit return frand() raise TypeError("limit not an int or float: {limit}.") def rescale(x, x1, x2, y1, y2, mode='lin')- 
Maps the value x ranging between x1 and x2 into a proportional value between y1 and y2.
Parameters
x:int | float- The value to rescale.
 x1:int | float- The lower limit of input range.
 x2:int | float- The upper limit of input range.
 y1:int | float- The lowest limit of output range.
 y2:int | float- The upper limit of output range.
 mode:string- If mode is 'lin' then linear scaling occurs, 'cos' produces cosine scaling, 'exp' produces exponential scaling, and '-exp' produces inverted exponential.
 
Returns
The rescaled value.
Expand source code
def rescale(x, x1, x2, y1, y2, mode='lin'): """ Maps the value x ranging between x1 and x2 into a proportional value between y1 and y2. Parameters ---------- x : int | float The value to rescale. x1 : int | float The lower limit of input range. x2 : int | float The upper limit of input range. y1 : int | float The lowest limit of output range. y2 : int | float The upper limit of output range. mode : string If mode is 'lin' then linear scaling occurs, 'cos' produces cosine scaling, 'exp' produces exponential scaling, and '-exp' produces inverted exponential. Returns ------- The rescaled value. """ if x > x2: return y2 if x <= x1: return y1 # as x moves x1 to x2 mu moves 0 to 1 mu = (x - x1) / (x2 - x1) if mode == 'lin': #return (((y2 - y1) / (x2 - x1)) * (x - x1)) + y1 return (y1 * (1 - mu) + (y2 * mu)) elif mode == 'cos': mu2 = (1 - math.cos(mu * math.pi)) / 2 return (y1 * (1 - mu2) + y2 * mu2) elif mode in ['exp','-exp']: # # http://www.pmean.com/10/ExponentialInterpolation.html # if y1==0: y1=0.00001 # return y1 * ((y2/y1) ** (mu)) # #https://docs.fincad.com/support/developerfunc/mathref/Interpolation.htm # if y1==0: y1=0.00001 # m = math.log(y2 / y1) / (x2 - x1) # k = y1 * math.pow(math.e, -m * x1) # return k * math.pow(math.e, m * x) # a base that yields a slope that is not too steep or shallow... b = 512 if mode == 'exp': return y1 + ( ((y2 - y1) / b) * math.pow(b, mu) ) b = 1/b return y1 + ( ((y2 - y1) / (b - 1)) * (math.pow(b, mu) - 1) ) raise ValueError(f"mode {mode} is not 'lin', 'cos', 'exp', or '-exp'.") def rescale_env(env, newxmin=None, newxmax=None, newymin=None, newymax=None, *, mode=None)- 
Rescales the current x and/or y envelope coordinates of the given envelope to proportional values lying between the given new minima and maxima. If newxmin or newxmax are unspecified they inherit the current x minimum and maximum values. The same is true for y values. If mode is specfied it is applied to the y values only. See
rescale()for information about mode.Parameters:
env : list A list of x y coordinate values: [x1,y1,x2,y2,…xn,yn] where x values are monotoniclly increasing values from left to right. newxmin : int | float | None The new minimum x value. If None it defaults to the current minimum x value. newxmax : int | float | None The new maximum x value. If None it defaults to the current maximum x value. newymin : int | float | None The new minimum y value. If None it defaults to the current minimum y value. newymax : int | float | None The new maximum y value. If None it defaults to the current maximum y value. mode : string See
rescale()for possible mode values.Returns
A rescaled x,y envelope.
Expand source code
def rescale_env(env, newxmin=None, newxmax=None, newymin=None, newymax=None,*, mode=None): ''' Rescales the current x and/or y envelope coordinates of the given envelope to proportional values lying between the given new minima and maxima. If newxmin or newxmax are unspecified they inherit the current x minimum and maximum values. The same is true for y values. If mode is specfied it is applied to the y values only. See `rescale()` for information about mode. Parameters: ----------- env : list A list of x y coordinate values: [x1,y1,x2,y2,...xn,yn] where x values are monotoniclly increasing values from left to right. newxmin : int | float | None The new minimum x value. If None it defaults to the current minimum x value. newxmax : int | float | None The new maximum x value. If None it defaults to the current maximum x value. newymin : int | float | None The new minimum y value. If None it defaults to the current minimum y value. newymax : int | float | None The new maximum y value. If None it defaults to the current maximum y value. mode : string See `rescale()` for possible mode values. Returns ------- A rescaled x,y envelope. ''' #print(f"env: {env}") if len(env) % 2 != 0: raise ValueError(f"env {env} does not have an even number of values.") xdata, ydata = env[::2], env[1::2] #print(f"xdata: {xdata}, ydata: {ydata}") for x1,x2 in zip(xdata,xdata[1:]): if x2<x1: raise ValueError(f"x value {x2} out of order in xdata: {xdata}.") oldxmin, oldxmax = xdata[0], xdata[-1] ##print(f"oldxmin: {oldxmin}, oldxmax: {oldxmax}") if newxmin is None: newxmin = oldxmin if newxmax is None: newxmax = oldxmax if not newxmin < newxmax: raise ValueError(f"newxmin value {newxmin} is not less than newxmax value {newxmax}.") #print(f"oldxmin: {oldxmin}, oldxmax: {oldxmax}, newxmin: {newxmin} newxmax: {newxmax}") oldymin = reduce(lambda a, b: a if a < b else b, ydata) oldymax = reduce(lambda a, b: a if a > b else b, ydata) ##print(f"oldymin: {oldymin}, oldymax: {oldymax}") if newymin is None: newymin = oldymin if newymax is None: newymax = oldymax if mode is None: mode = "lin" #print(f"oldymin: {oldymin}, oldymax: {oldymax}, newymin: {newymin}, newymax: {newymax}") res = [] for x,y in zip(xdata,ydata): res.extend([rescale(x, oldxmin, oldxmax, newxmin, newxmax), rescale(y, oldymin, oldymax, newymin, newymax, mode)]) return res def segment_env(env, dur, attack, release=[])- 
Returns a version of env with attack and (optionally) decay segments lasting specific amounts of time.
Parameters
env:list- The x,y envelope.
 dur:int | float- The total duration in seconds of the envelope.
 attack:[ _attack_x, attack_dur_ ]- The ending x coordinate and duration of the attack segment.
 release:[ _release_x, release_dur_ ] | []- The starting x coordinate and duration of the release segment.
 
Expand source code
def segment_env(env, dur, attack, release=[]): ''' Returns a version of env with attack and (optionally) decay segments lasting specific amounts of time. Parameters ---------- env : list The x,y envelope. dur : int | float The total duration in seconds of the envelope. attack : [ _attack_x, attack_dur_ ] The ending x coordinate and duration of the attack segment. release : [ _release_x, release_dur_ ] | [] The starting x coordinate and duration of the release segment. ''' # maximum x value in envelope. x_max = env[-2] # get the index positions of the attack and release segments. # divides by 2 because x_values will be half the length of env. try: attack_index = int(env.index(attack[0])/2) attack_dur = attack[1] except ValueError as e: raise Exception(f"Attack coordinate {attack[0]} is not in {env}.") from e if release: try: release_index = int(env.index(release[0])/2) release_dur = release[1] except ValueError as e: raise Exception(f"Release coordinate {release[0]} is not in {env}.") from e # split x and y coords into separate lists with x_values converted to time. x_values, y_values = [(x / x_max) * dur for x in env[::2]], env[1::2] # rescale x coords from index 0 to att_index to fit in attack_dur time. for i in range(0, attack_index+1): x_values[i] = rescale(x_values[i], x_values[0], x_values[attack_index], x_values[0], attack_dur) # rescale backwards for release values. if release: end_time = x_values[-1] for i in range(len(x_values)-1, release_index-1, -1): x_values[i] = rescale(x_values[i], x_values[release_index], end_time, end_time - release_dur, end_time) # return new envelope with durations converted back to user's original coordinates. env = [] for x,y in zip(x_values, y_values): env.extend([(x / dur) * x_max, y]) return env def setaudioplayer(command)- 
Assign a shell command (string) that will play an audio file.
Parameter
command : string The shell command that will play an audio file.
Example
setaudioplayer('afplay')
Expand source code
def setaudioplayer(command): """ Assign a shell command (string) that will play an audio file. Parameter --------- command : string The shell command that will play an audio file. Example ------- setaudioplayer('afplay') """ global audioplayer audioplayer = command def setmidiplayer(command)- 
Assign a shell command (string) that will play a midi file.
Parameter
command : string The shell command that will play a midi file.
Example
setmidiplayer('fluidsynth -iq -g1 /Users/taube/SoundFonts/MuseScore_General.sf2')
Expand source code
def setmidiplayer(command): """ Assign a shell command (string) that will play a midi file. Parameter --------- command : string The shell command that will play a midi file. Example ------- setmidiplayer('fluidsynth -iq -g1 /Users/taube/SoundFonts/MuseScore_General.sf2') """ global midiplayer midiplayer = command def subtract(left, right)- 
List-aware subtraction.
Left and right can be numbers or lists, if both are lists they must be of the same length.
Expand source code
def subtract(left, right): """ List-aware subtraction. Left and right can be numbers or lists, if both are lists they must be of the same length. """ return _map_lists(lambda l,r: l-r, left, right)