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
:series
ofint
orfloat | 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)