Module musx.spectral
Functions and classes for working with microtonality, the harmonic series, and spectra. For examples of generating microtonal music using midi see the demos and tutorials directories.
Expand source code
################################################################################
"""
Functions and classes for working with microtonality, the harmonic series, and
spectra. For examples of generating microtonal music using midi see the demos
and tutorials directories.
"""
import math
import fractions
from .pitch import keynum
from .tools import quantize
# try:
# from scipy.special import jn as bes_jn
# except ModuleNotFoundError:
# def bes_jn(a,b):
# raise ModuleNotFoundError("\nfmspectrum(): module scipy.special not found.")
def harmonics(h1, h2, fund=1, reverse=False):
"""
Returns the harmonic series ratios between harmonic number
h1 to h2 inclusive.
If 0 < h1 < h2 then the ratios will produce the overtone series,
otherwise (0 > h1 > h2) will produce undertones. If reverse is
False then the overtone ratios are ascending and undertones are
decending.
Parameters
----------
h1 : int
The starting harmonic. If positive, overtones are produced
otherwise negative undertones are produced.
h2 : int
The ending harmonic (inclusive). If positive it must be
equal to or greater than h1. If negativ it must be equal
to or less than h1.
fund : int | float
The fundamental of the series, defaults to 1. If fund is an int
then exact Faction harmonics are produced. If fund is a float
then the series will be floats as well.
reverse : bool
If true then overtones are returned reversed order.
Returns
-------
The list of harmonic ratios between h1 and h2 inclusive.
"""
if not isinstance(h1, int):
TypeError(f"not an integer harmonic number: {h1}.")
if not isinstance(h2, int):
TypeError(f"not an integer harmonic number: {h2}.")
utones = False
harms = []
if (0 < h1 < h2):
pass
elif (0 > h1 > h2):
utones = True
else:
raise ValueError(f"harmonic numbers out of order: {h1} {h2}.")
h1 = fractions.Fraction(abs(h1),1)
h2 = fractions.Fraction(abs(h2),1)
h = h1
f = None
while h <= h2:
# utones collect the reciprocal
f = fund/(h/h1) if utones else fund*(h/h1)
harms.append(f)
h += 1
if reverse:
harms.reverse()
return harms
def temper(ratio, div=12):
"""
Converts a frequency ratio into a tempered interval.
Parameters
----------
ratio : int | float | Fraction | list
The ratio to convert. The value can be an integer, float,
Fraction, or a list of the same.
div : int
The divisions per octave. 12 will convert to
semitones and 1200 will convert to cents.
Returns
-------
The tempered interval.
"""
func = lambda r,s: math.log(r)/math.log(2.0) * s
if type(ratio) is list:
return [func(r, div) for r in ratio]
return func(ratio, div)
def untemper(interval, div=12):
"""
Converts a tempered interval into a frequency ratio.
Parameters
----------
interval : int | float
The interval to convert. The value can be an integer, float,
or a list of the same.
div : int
The divisions per octave. 12 will convert from
semitones and 1200 will convert from cents.
Returns
-------
The untempered frequency ratio
"""
func = lambda i,d: math.pow(2.0, i/d)
if type(interval) is list:
return [func(i, div) for i in interval]
return func(interval, div)
class Spectrum (list):
"""
A structured list of frequency and amplitude pairs with methods for compositional purposes.
Spectrums are produced by the fmspectum() and rmspectum() functions, or by loading datafiles created by
the <a href="https://www.klingbeil.com/spear/">SPEAR</a> application.
Parameters
----------
pairs : list
A sorted list of [frequency, amplitude] pairs with lower
frequency pairs to the left.
Returns:
A Spectrum.
"""
def __new__(cls, pairs):
z = 0
for t in pairs:
if not isinstance(t, list) or len(t) != 2:
raise TypeError(f"not a tuple (frequency, amplitude): {t}.")
if not isinstance(t[0], (int, float)):
raise TypeError(f"invalid frequency: {t[0]}.")
if not isinstance(t[1], (int, float)):
raise TypeError(f"invalid amplitude: {t[0]}.")
if not t[0] > z:
raise ValueError(f"invalid frequency: {t[0]} not greater than {z}.")
# if not t[1] > 0:
# raise ValueError(f"invalid amplitude {t[1]} not greater than 0.")
return super(Spectrum, cls).__new__(cls, pairs)
def size(self):
return len(self)
def freqs(self):
"""Returns a list of the frequency components in the spectrum."""
return [t[0] for t in self]
def amps(self):
"""Returns a list of the amplitude components in the spectrum."""
return [t[1] for t in self]
def pairs(self):
"""Returns a list of frequency and amplitude pairs."""
return list(self)
def maxfreq(self):
"""Returns the maximum frequency in the spectrum."""
return self[-1][0]
def minfreq(self):
"""Returns the minimum frequency in the spectrum."""
return self[0][0]
def minamp(self):
"""Returns the minimum amplitude in the spectrum."""
mina = None
for t in self:
if not mina or t[1] < mina:
mina = t[1]
return mina
def maxamp(self):
"""Returns the minimum amplitude in the spectrum."""
maxa = None
for t in self:
if not maxa or t[1] > maxa:
maxa = t[1]
return maxa
def __str__(self):
return f'<Spectrum: {len(self)} {hex(id(self))}>'
__repr__ = __str__
def keynums(self, quant=None, unique=None, minkey=0, maxkey=127, thresh=0):
"""
Returns a list of the frequency components of spectrum converted to key
numbers.
Parameters
----------
quant : int | float | function | None
If quant is a number then the key numbers returned are quantized to
that semitonal value, e.g. quant .5 returns key numbers quantized
to the nearest quarter-tone and quant 1 returns key numbers rounded
to the nearest semitone. Quant can also be a function, e.g. round,
ceil, or floor.
unique : bool
If unique is True then no duplicate key numbers will be returned.
minkey : int | None
If a minkey number is specified then spectral values lower than that will be octave
shifted upward until they equal or exceed it.
maxkey : int | None
If a maxkey number is specified then spectral values higher than that will be octave
shifted downward until they equal or lower than it.
thresh : float | None
The minimum amplitude that a frequency must possess in order to be
returned as keys.
Returns
-------
A list of key number from the spectrum.
"""
if not (minkey < maxkey):
raise TypeError("minkey {minkey} not less than {maxkey}.")
if callable(quant) or quant is None:
func = quant
elif isinstance(quant, (int, float)):
func = lambda x: quantize(x, quant)
else:
raise TypeError("quant value not a callable, number, or None: {quant}.")
# drop freqs less than C00 or greater than G9
inbounds = lambda x: 8.175798915643707 <= x <= 12543.853951415975
keys = [keynum(x[0], func) for x in self if x[1] >= thresh and inbounds(x[0])]
for i in range(len(keys)):
while keys[i] < minkey: keys[i] += 12
while keys[i] > maxkey: keys[i] -= 12
if not minkey <= keys[i] <= maxkey:
raise ValueError(f"key {keys[i]} outside bounds {minkey} to {maxkey}.")
if unique:
keys = list(dict.fromkeys(keys))
return sorted(keys)
def add(self, freq, amp):
"""
Updates the amplitude of an existing [freq,amp] pair or inserts a new
pair if freq is not yet in the spectrum.
Warning: add alters the existing spectrum by adding or updating
components.
Parameters
----------
freq : int | float
The frequency to add or update.
amp : int | float
The amplitude to add or update.
"""
index = 0
while index < len(self):
if self[index][0] == freq:
self[index][1] += amp # update existing entry
return
elif self[index][0] > freq: # insert before this one
break
index += 1
#print("insert freq", freq, "at index", index, "in", spec)
self.insert(index, [freq, amp]) # add new entry
def fmspectrum(carrier, mratio, index):
"""
Returns a spectrum generated by Frequency Modulation.
Parameters
----------
carrier : int | float
The FM carrier frequency, in hertz.
mratio : int | float
The carrier-to-modulator ratio. 1 means the carrier and modulator
are the same frequency, 2 means the modulator frequency is twice
the carrier.
index : int | float
The index of modulation. The number of FM sidebands (spectral
components) will be 2*index+1.
Returns
-------
A Spectrum created by frequency modulation.
"""
mfreq = carrier * mratio
sides = round(index) + 1
spectrum = {}
frq = 0
amp = 0
for s in range(-sides, sides+1):
frq = carrier + (mfreq * s)
amp = bes_jn(s, index)
if not (amp == 0.0 or frq == 0.0):
if frq < 0:
frq = abs(frq)
amp = -amp
if spectrum.get(frq):
spectrum[frq] += amp
else:
spectrum[frq] = amp
return Spectrum( sorted( [f, round(abs(a), 3)] for f,a in spectrum.items()) )
def bes_j0(x):
'''Translated from Common Music by https://www.codeconvert.ai/'''
if abs(x) < 8.0:
y = x * x
ans1 = 5.756849E+10 + y * (-1.3362591E+10 + y * (6.5161965E+8 + y * (-1.1214424E+7 + y * (77392.33 + y * -184.90524))))
ans2 = 5.756849E+10 + y * (1.029533E+9 + y * (9494681.0 + y * (59272.65 + y * (267.85327 + y * y))))
return ans1 / ans2
else:
ax = abs(x)
z = 8.0 / ax
y = z * z
xx = ax - 0.7853982
ans1 = 1.0 + y * (-0.0010986286 + y * (2.7345104E-5 + y * (-2.0733708E-6 + y * 2.0938872E-7)))
ans2 = -0.015625 + y * (1.4304888E-4 + y * (-6.9111475E-6 + y * (7.621095E-7 + y * -9.349451E-8)))
return math.sqrt(0.63661975 / ax) * (math.cos(xx) * ans1 - z * math.sin(xx) * ans2)
def bes_j1(x):
'''Translated from Common Music by https://www.codeconvert.ai/'''
if abs(x) < 8.0:
y = x * x
ans1 = x * (7.2362615E+10 + y * (-7.8950595E+9 + y * (2.4239685E+8 + y * (-2972611.5 + y * (15704.482 + y * -30.160366)))))
ans2 = 1.4472523E+11 + y * (2.3005353E+9 + y * (1.8583304E+7 + y * (99447.44 + y * (376.99915 + y * y))))
return ans1 / ans2
else:
ax = abs(x)
z = 8.0 / ax
y = z * z
xx = ax - 2.3561945
ans1 = 1.0 + y * (0.00183105 + y * (-3.5163965E-5 + y * (2.4575202E-6 + y * -2.4033702E-7)))
ans2 = 0.046875 + y * (-2.0026909E-4 + y * (8.449199E-6 + y * (-8.822899E-7 + y * 1.05787414E-7)))
return math.copysign(math.sqrt(0.63661975 / ax) * (math.cos(xx) * ans1 - z * math.sin(xx) * ans2), x)
def bes_jn(unn, ux):
'''Translated from Common Music by https://www.codeconvert.ai/'''
nn = unn
x = ux
n = abs(nn)
besn = 0.0
if n == 0:
besn = bes_j0(x)
elif n == 1:
besn = bes_j1(x)
elif x == 0:
besn = 0.0
else:
iacc = 40
ans = 0.0
bigno = 1.0E+10
bigni = 1.0E-10
if abs(x) > n:
tox = 2.0 / abs(x)
bjm = bes_j0(abs(x))
bj = bes_j1(abs(x))
j = 1
bjp = 0.0
while j != n:
bjp = j * tox * bj - bjm
bjm = bj
bj = bjp
j += 1
ans = bj
else:
tox = 2.0 / abs(x)
m = 2 * (n + int((n * iacc) ** 0.5) // 2)
jsum = 0.0
bjm = 0.0
sum = 0.0
bjp = 0.0
bj = 1.0
j = m
while j != 0:
bjm = j * tox * bj - bjp
bjp = bj
bj = bjm
if abs(bj) > bigno:
bj *= bigni
bjp *= bigni
ans *= bigni
sum *= bigni
if jsum != 0:
sum += bj
jsum = -1 * jsum
if j == n:
ans = bjp
j -= 1
sum = 2.0 * sum - bj
ans = ans / sum
if x < 0 and n % 2 != 0:
besn = -ans
else:
besn = ans
if nn < 0 and nn % 2 != 0:
return -besn
else:
return besn
def rmspectrum(freqs1, freqs2, asfreqs=False):
"""
Returns a spectrum generated by ring modulation, where freq1 and freq2
can be frequencies, lists of frequencies, or Spectrum objects.
Ring moduluation produces a spectrum consiting of the sum and difference
tones between all pairs of frequencies in freqs1 and freqs2.
Parameters
----------
freqs1 : int | float | list | Spectrum
A hertz value, list of the same, or Spectrum.
freqs2 : int | float | list | Spectrum
A hertz value, list of the same, or Spectrum.
asfreqs : bool
If true the spectrum's frequency values are returned as a
list, otherwise the specturm is returned.
Returns
-------
A Spectrum or list containing the pairwise sum and difference tones
of freqs1 and freqs2.
"""
if isinstance(freqs1, Spectrum) or isinstance(freqs2, Spectrum):
raise NotImplementedError("Spectrum input not yet implemented. :(")
if not isinstance(freqs1, list):
freqs1 = [freqs1]
if not isinstance(freqs2, list):
freqs2 = [freqs2]
spec = Spectrum([]) # create empty spectrum
for f1 in freqs1:
for f2 in freqs2:
if not f1 == f2:
spec.add(abs(f1+f2), 0.0)
spec.add(abs(f1-f2), 0.0)
return [p[0] for p in spec] if asfreqs else spec
"""
from musx.spectral import fmspectrum
fmspectrum(100, 1.4, 3)
"""
def _read_spear_frame(fstr):
data = fstr.split(" ")
time = float(data.pop(0))
size = int(data.pop(0))
# omit null frames
if not data:
return None
spec = []
for i in range(size):
data.pop(0) # flush partial num
f = float(data.pop(0)) # read freq
a = float(data.pop(0)) # read amp
spec.append([f, a])
spec.sort(key=lambda a: a[0]) # sort by freq
return Spectrum(spec)
def import_spear_frames(filename):
"""Imports the contents of a Spear frame data file as a list of Spectrum objects."""
def rhdr(f):
l = f.readline()
if l == '':
raise ValueError(f"Reached EOF while parsing file header of {filename}")
return l[:-1]
file = open(filename, 'r')
line = rhdr(file)
if not line == "par-text-frame-format":
raise ValueError(f"Expected 'par-text-frame-format' but got '{line}'")
line = rhdr(file)
if not line == "point-type index frequency amplitude":
raise ValueError(f"Expected 'point-type index frequency amplitude' but got '{line}'")
# flush remaining header lines
while True:
if line == "frame-data":
break
line = rhdr(file)
# file now at frame-data, read spectra till eof
frames = []
line = file.readline()
while (line):
spec = _read_spear_frame(line[:-1])
if spec:
frames.append(spec)
line = file.readline()
return frames
def import_spear_partials(filename):
"""Imports the contents of a Spear partials data file as a list of Spectrum objects."""
raise NotImplementedError("not implemented yet :(")
if __name__ == '__main__':
from fractions import Fraction
# print("harmonics(1,8) =>", harmonics(1, 8))
# print("harmonics(-1,-8) =>",harmonics(-1, -8))
# print("harmonics(8,16) =>",harmonics(8, 16))
# print("harmonics(-8,-16) =>",harmonics(-8, -16))
# print("harmonics(8,16,100) =>",harmonics(8, 16, fund=100))
# print("harmonics(-8,-16, 100) =>",harmonics(-8, -16, fund=100))
# print("harmonics(8,16,100) =>",harmonics(8, 16, fund=100.0))
# print("harmonics(-8,-16, 100) =>",harmonics(-8, -16, fund=100.0))
import musx
x=musx.fmspectrum(100, 1.4, 3)
x.keynums()
Functions
def bes_j0(x)
-
Translated from Common Music by https://www.codeconvert.ai/
Expand source code
def bes_j0(x): '''Translated from Common Music by https://www.codeconvert.ai/''' if abs(x) < 8.0: y = x * x ans1 = 5.756849E+10 + y * (-1.3362591E+10 + y * (6.5161965E+8 + y * (-1.1214424E+7 + y * (77392.33 + y * -184.90524)))) ans2 = 5.756849E+10 + y * (1.029533E+9 + y * (9494681.0 + y * (59272.65 + y * (267.85327 + y * y)))) return ans1 / ans2 else: ax = abs(x) z = 8.0 / ax y = z * z xx = ax - 0.7853982 ans1 = 1.0 + y * (-0.0010986286 + y * (2.7345104E-5 + y * (-2.0733708E-6 + y * 2.0938872E-7))) ans2 = -0.015625 + y * (1.4304888E-4 + y * (-6.9111475E-6 + y * (7.621095E-7 + y * -9.349451E-8))) return math.sqrt(0.63661975 / ax) * (math.cos(xx) * ans1 - z * math.sin(xx) * ans2)
def bes_j1(x)
-
Translated from Common Music by https://www.codeconvert.ai/
Expand source code
def bes_j1(x): '''Translated from Common Music by https://www.codeconvert.ai/''' if abs(x) < 8.0: y = x * x ans1 = x * (7.2362615E+10 + y * (-7.8950595E+9 + y * (2.4239685E+8 + y * (-2972611.5 + y * (15704.482 + y * -30.160366))))) ans2 = 1.4472523E+11 + y * (2.3005353E+9 + y * (1.8583304E+7 + y * (99447.44 + y * (376.99915 + y * y)))) return ans1 / ans2 else: ax = abs(x) z = 8.0 / ax y = z * z xx = ax - 2.3561945 ans1 = 1.0 + y * (0.00183105 + y * (-3.5163965E-5 + y * (2.4575202E-6 + y * -2.4033702E-7))) ans2 = 0.046875 + y * (-2.0026909E-4 + y * (8.449199E-6 + y * (-8.822899E-7 + y * 1.05787414E-7))) return math.copysign(math.sqrt(0.63661975 / ax) * (math.cos(xx) * ans1 - z * math.sin(xx) * ans2), x)
def bes_jn(unn, ux)
-
Translated from Common Music by https://www.codeconvert.ai/
Expand source code
def bes_jn(unn, ux): '''Translated from Common Music by https://www.codeconvert.ai/''' nn = unn x = ux n = abs(nn) besn = 0.0 if n == 0: besn = bes_j0(x) elif n == 1: besn = bes_j1(x) elif x == 0: besn = 0.0 else: iacc = 40 ans = 0.0 bigno = 1.0E+10 bigni = 1.0E-10 if abs(x) > n: tox = 2.0 / abs(x) bjm = bes_j0(abs(x)) bj = bes_j1(abs(x)) j = 1 bjp = 0.0 while j != n: bjp = j * tox * bj - bjm bjm = bj bj = bjp j += 1 ans = bj else: tox = 2.0 / abs(x) m = 2 * (n + int((n * iacc) ** 0.5) // 2) jsum = 0.0 bjm = 0.0 sum = 0.0 bjp = 0.0 bj = 1.0 j = m while j != 0: bjm = j * tox * bj - bjp bjp = bj bj = bjm if abs(bj) > bigno: bj *= bigni bjp *= bigni ans *= bigni sum *= bigni if jsum != 0: sum += bj jsum = -1 * jsum if j == n: ans = bjp j -= 1 sum = 2.0 * sum - bj ans = ans / sum if x < 0 and n % 2 != 0: besn = -ans else: besn = ans if nn < 0 and nn % 2 != 0: return -besn else: return besn
def fmspectrum(carrier, mratio, index)
-
Returns a spectrum generated by Frequency Modulation.
Parameters
carrier : int | float The FM carrier frequency, in hertz. mratio : int | float The carrier-to-modulator ratio. 1 means the carrier and modulator are the same frequency, 2 means the modulator frequency is twice the carrier. index : int | float The index of modulation. The number of FM sidebands (spectral components) will be 2*index+1.
Returns
A Spectrum created by frequency modulation.
Expand source code
def fmspectrum(carrier, mratio, index): """ Returns a spectrum generated by Frequency Modulation. Parameters ---------- carrier : int | float The FM carrier frequency, in hertz. mratio : int | float The carrier-to-modulator ratio. 1 means the carrier and modulator are the same frequency, 2 means the modulator frequency is twice the carrier. index : int | float The index of modulation. The number of FM sidebands (spectral components) will be 2*index+1. Returns ------- A Spectrum created by frequency modulation. """ mfreq = carrier * mratio sides = round(index) + 1 spectrum = {} frq = 0 amp = 0 for s in range(-sides, sides+1): frq = carrier + (mfreq * s) amp = bes_jn(s, index) if not (amp == 0.0 or frq == 0.0): if frq < 0: frq = abs(frq) amp = -amp if spectrum.get(frq): spectrum[frq] += amp else: spectrum[frq] = amp return Spectrum( sorted( [f, round(abs(a), 3)] for f,a in spectrum.items()) )
def harmonics(h1, h2, fund=1, reverse=False)
-
Returns the harmonic series ratios between harmonic number h1 to h2 inclusive.
If 0 < h1 < h2 then the ratios will produce the overtone series, otherwise (0 > h1 > h2) will produce undertones. If reverse is False then the overtone ratios are ascending and undertones are decending.
Parameters
h1
:int
- The starting harmonic. If positive, overtones are produced otherwise negative undertones are produced.
h2
:int
- The ending harmonic (inclusive). If positive it must be equal to or greater than h1. If negativ it must be equal to or less than h1.
fund
:int | float
- The fundamental of the series, defaults to 1. If fund is an int then exact Faction harmonics are produced. If fund is a float then the series will be floats as well.
reverse
:bool
- If true then overtones are returned reversed order.
Returns
The list of harmonic ratios between h1 and h2 inclusive.
Expand source code
def harmonics(h1, h2, fund=1, reverse=False): """ Returns the harmonic series ratios between harmonic number h1 to h2 inclusive. If 0 < h1 < h2 then the ratios will produce the overtone series, otherwise (0 > h1 > h2) will produce undertones. If reverse is False then the overtone ratios are ascending and undertones are decending. Parameters ---------- h1 : int The starting harmonic. If positive, overtones are produced otherwise negative undertones are produced. h2 : int The ending harmonic (inclusive). If positive it must be equal to or greater than h1. If negativ it must be equal to or less than h1. fund : int | float The fundamental of the series, defaults to 1. If fund is an int then exact Faction harmonics are produced. If fund is a float then the series will be floats as well. reverse : bool If true then overtones are returned reversed order. Returns ------- The list of harmonic ratios between h1 and h2 inclusive. """ if not isinstance(h1, int): TypeError(f"not an integer harmonic number: {h1}.") if not isinstance(h2, int): TypeError(f"not an integer harmonic number: {h2}.") utones = False harms = [] if (0 < h1 < h2): pass elif (0 > h1 > h2): utones = True else: raise ValueError(f"harmonic numbers out of order: {h1} {h2}.") h1 = fractions.Fraction(abs(h1),1) h2 = fractions.Fraction(abs(h2),1) h = h1 f = None while h <= h2: # utones collect the reciprocal f = fund/(h/h1) if utones else fund*(h/h1) harms.append(f) h += 1 if reverse: harms.reverse() return harms
def import_spear_frames(filename)
-
Imports the contents of a Spear frame data file as a list of Spectrum objects.
Expand source code
def import_spear_frames(filename): """Imports the contents of a Spear frame data file as a list of Spectrum objects.""" def rhdr(f): l = f.readline() if l == '': raise ValueError(f"Reached EOF while parsing file header of {filename}") return l[:-1] file = open(filename, 'r') line = rhdr(file) if not line == "par-text-frame-format": raise ValueError(f"Expected 'par-text-frame-format' but got '{line}'") line = rhdr(file) if not line == "point-type index frequency amplitude": raise ValueError(f"Expected 'point-type index frequency amplitude' but got '{line}'") # flush remaining header lines while True: if line == "frame-data": break line = rhdr(file) # file now at frame-data, read spectra till eof frames = [] line = file.readline() while (line): spec = _read_spear_frame(line[:-1]) if spec: frames.append(spec) line = file.readline() return frames
def import_spear_partials(filename)
-
Imports the contents of a Spear partials data file as a list of Spectrum objects.
Expand source code
def import_spear_partials(filename): """Imports the contents of a Spear partials data file as a list of Spectrum objects.""" raise NotImplementedError("not implemented yet :(")
def rmspectrum(freqs1, freqs2, asfreqs=False)
-
Returns a spectrum generated by ring modulation, where freq1 and freq2 can be frequencies, lists of frequencies, or Spectrum objects.
Ring moduluation produces a spectrum consiting of the sum and difference tones between all pairs of frequencies in freqs1 and freqs2.
Parameters
freqs1
:int | float | list | Spectrum
- A hertz value, list of the same, or Spectrum.
freqs2
:int | float | list | Spectrum
- A hertz value, list of the same, or Spectrum.
asfreqs
:bool
- If true the spectrum's frequency values are returned as a list, otherwise the specturm is returned.
Returns
A Spectrum
orlist containing the pairwise sum and difference tones
of freqs1 and freqs2.
Expand source code
def rmspectrum(freqs1, freqs2, asfreqs=False): """ Returns a spectrum generated by ring modulation, where freq1 and freq2 can be frequencies, lists of frequencies, or Spectrum objects. Ring moduluation produces a spectrum consiting of the sum and difference tones between all pairs of frequencies in freqs1 and freqs2. Parameters ---------- freqs1 : int | float | list | Spectrum A hertz value, list of the same, or Spectrum. freqs2 : int | float | list | Spectrum A hertz value, list of the same, or Spectrum. asfreqs : bool If true the spectrum's frequency values are returned as a list, otherwise the specturm is returned. Returns ------- A Spectrum or list containing the pairwise sum and difference tones of freqs1 and freqs2. """ if isinstance(freqs1, Spectrum) or isinstance(freqs2, Spectrum): raise NotImplementedError("Spectrum input not yet implemented. :(") if not isinstance(freqs1, list): freqs1 = [freqs1] if not isinstance(freqs2, list): freqs2 = [freqs2] spec = Spectrum([]) # create empty spectrum for f1 in freqs1: for f2 in freqs2: if not f1 == f2: spec.add(abs(f1+f2), 0.0) spec.add(abs(f1-f2), 0.0) return [p[0] for p in spec] if asfreqs else spec
def temper(ratio, div=12)
-
Converts a frequency ratio into a tempered interval.
Parameters
ratio
:int | float | Fraction | list
- The ratio to convert. The value can be an integer, float, Fraction, or a list of the same.
div
:int
- The divisions per octave. 12 will convert to semitones and 1200 will convert to cents.
Returns
The tempered interval.
Expand source code
def temper(ratio, div=12): """ Converts a frequency ratio into a tempered interval. Parameters ---------- ratio : int | float | Fraction | list The ratio to convert. The value can be an integer, float, Fraction, or a list of the same. div : int The divisions per octave. 12 will convert to semitones and 1200 will convert to cents. Returns ------- The tempered interval. """ func = lambda r,s: math.log(r)/math.log(2.0) * s if type(ratio) is list: return [func(r, div) for r in ratio] return func(ratio, div)
def untemper(interval, div=12)
-
Converts a tempered interval into a frequency ratio.
Parameters
interval
:int | float
- The interval to convert. The value can be an integer, float, or a list of the same.
div
:int
- The divisions per octave. 12 will convert from semitones and 1200 will convert from cents.
Returns
The untempered frequency ratio
Expand source code
def untemper(interval, div=12): """ Converts a tempered interval into a frequency ratio. Parameters ---------- interval : int | float The interval to convert. The value can be an integer, float, or a list of the same. div : int The divisions per octave. 12 will convert from semitones and 1200 will convert from cents. Returns ------- The untempered frequency ratio """ func = lambda i,d: math.pow(2.0, i/d) if type(interval) is list: return [func(i, div) for i in interval] return func(interval, div)
Classes
class Spectrum (*args, **kwargs)
-
A structured list of frequency and amplitude pairs with methods for compositional purposes. Spectrums are produced by the fmspectum() and rmspectum() functions, or by loading datafiles created by the SPEAR application.
Parameters
pairs
:list
- A sorted list of [frequency, amplitude] pairs with lower frequency pairs to the left.
Returns: A Spectrum.
Expand source code
class Spectrum (list): """ A structured list of frequency and amplitude pairs with methods for compositional purposes. Spectrums are produced by the fmspectum() and rmspectum() functions, or by loading datafiles created by the <a href="https://www.klingbeil.com/spear/">SPEAR</a> application. Parameters ---------- pairs : list A sorted list of [frequency, amplitude] pairs with lower frequency pairs to the left. Returns: A Spectrum. """ def __new__(cls, pairs): z = 0 for t in pairs: if not isinstance(t, list) or len(t) != 2: raise TypeError(f"not a tuple (frequency, amplitude): {t}.") if not isinstance(t[0], (int, float)): raise TypeError(f"invalid frequency: {t[0]}.") if not isinstance(t[1], (int, float)): raise TypeError(f"invalid amplitude: {t[0]}.") if not t[0] > z: raise ValueError(f"invalid frequency: {t[0]} not greater than {z}.") # if not t[1] > 0: # raise ValueError(f"invalid amplitude {t[1]} not greater than 0.") return super(Spectrum, cls).__new__(cls, pairs) def size(self): return len(self) def freqs(self): """Returns a list of the frequency components in the spectrum.""" return [t[0] for t in self] def amps(self): """Returns a list of the amplitude components in the spectrum.""" return [t[1] for t in self] def pairs(self): """Returns a list of frequency and amplitude pairs.""" return list(self) def maxfreq(self): """Returns the maximum frequency in the spectrum.""" return self[-1][0] def minfreq(self): """Returns the minimum frequency in the spectrum.""" return self[0][0] def minamp(self): """Returns the minimum amplitude in the spectrum.""" mina = None for t in self: if not mina or t[1] < mina: mina = t[1] return mina def maxamp(self): """Returns the minimum amplitude in the spectrum.""" maxa = None for t in self: if not maxa or t[1] > maxa: maxa = t[1] return maxa def __str__(self): return f'<Spectrum: {len(self)} {hex(id(self))}>' __repr__ = __str__ def keynums(self, quant=None, unique=None, minkey=0, maxkey=127, thresh=0): """ Returns a list of the frequency components of spectrum converted to key numbers. Parameters ---------- quant : int | float | function | None If quant is a number then the key numbers returned are quantized to that semitonal value, e.g. quant .5 returns key numbers quantized to the nearest quarter-tone and quant 1 returns key numbers rounded to the nearest semitone. Quant can also be a function, e.g. round, ceil, or floor. unique : bool If unique is True then no duplicate key numbers will be returned. minkey : int | None If a minkey number is specified then spectral values lower than that will be octave shifted upward until they equal or exceed it. maxkey : int | None If a maxkey number is specified then spectral values higher than that will be octave shifted downward until they equal or lower than it. thresh : float | None The minimum amplitude that a frequency must possess in order to be returned as keys. Returns ------- A list of key number from the spectrum. """ if not (minkey < maxkey): raise TypeError("minkey {minkey} not less than {maxkey}.") if callable(quant) or quant is None: func = quant elif isinstance(quant, (int, float)): func = lambda x: quantize(x, quant) else: raise TypeError("quant value not a callable, number, or None: {quant}.") # drop freqs less than C00 or greater than G9 inbounds = lambda x: 8.175798915643707 <= x <= 12543.853951415975 keys = [keynum(x[0], func) for x in self if x[1] >= thresh and inbounds(x[0])] for i in range(len(keys)): while keys[i] < minkey: keys[i] += 12 while keys[i] > maxkey: keys[i] -= 12 if not minkey <= keys[i] <= maxkey: raise ValueError(f"key {keys[i]} outside bounds {minkey} to {maxkey}.") if unique: keys = list(dict.fromkeys(keys)) return sorted(keys) def add(self, freq, amp): """ Updates the amplitude of an existing [freq,amp] pair or inserts a new pair if freq is not yet in the spectrum. Warning: add alters the existing spectrum by adding or updating components. Parameters ---------- freq : int | float The frequency to add or update. amp : int | float The amplitude to add or update. """ index = 0 while index < len(self): if self[index][0] == freq: self[index][1] += amp # update existing entry return elif self[index][0] > freq: # insert before this one break index += 1 #print("insert freq", freq, "at index", index, "in", spec) self.insert(index, [freq, amp]) # add new entry
Ancestors
- builtins.list
Methods
def add(self, freq, amp)
-
Updates the amplitude of an existing [freq,amp] pair or inserts a new pair if freq is not yet in the spectrum.
Warning: add alters the existing spectrum by adding or updating components.
Parameters
freq
:int | float
- The frequency to add or update.
amp
:int | float
- The amplitude to add or update.
Expand source code
def add(self, freq, amp): """ Updates the amplitude of an existing [freq,amp] pair or inserts a new pair if freq is not yet in the spectrum. Warning: add alters the existing spectrum by adding or updating components. Parameters ---------- freq : int | float The frequency to add or update. amp : int | float The amplitude to add or update. """ index = 0 while index < len(self): if self[index][0] == freq: self[index][1] += amp # update existing entry return elif self[index][0] > freq: # insert before this one break index += 1 #print("insert freq", freq, "at index", index, "in", spec) self.insert(index, [freq, amp]) # add new entry
def amps(self)
-
Returns a list of the amplitude components in the spectrum.
Expand source code
def amps(self): """Returns a list of the amplitude components in the spectrum.""" return [t[1] for t in self]
def freqs(self)
-
Returns a list of the frequency components in the spectrum.
Expand source code
def freqs(self): """Returns a list of the frequency components in the spectrum.""" return [t[0] for t in self]
def keynums(self, quant=None, unique=None, minkey=0, maxkey=127, thresh=0)
-
Returns a list of the frequency components of spectrum converted to key numbers.
Parameters
quant
:int | float | function | None
- If quant is a number then the key numbers returned are quantized to that semitonal value, e.g. quant .5 returns key numbers quantized to the nearest quarter-tone and quant 1 returns key numbers rounded to the nearest semitone. Quant can also be a function, e.g. round, ceil, or floor.
unique
:bool
- If unique is True then no duplicate key numbers will be returned.
minkey
:int | None
- If a minkey number is specified then spectral values lower than that will be octave shifted upward until they equal or exceed it.
maxkey
:int | None
- If a maxkey number is specified then spectral values higher than that will be octave shifted downward until they equal or lower than it.
thresh
:float | None
- The minimum amplitude that a frequency must possess in order to be returned as keys.
Returns
A list of key number from the spectrum.
Expand source code
def keynums(self, quant=None, unique=None, minkey=0, maxkey=127, thresh=0): """ Returns a list of the frequency components of spectrum converted to key numbers. Parameters ---------- quant : int | float | function | None If quant is a number then the key numbers returned are quantized to that semitonal value, e.g. quant .5 returns key numbers quantized to the nearest quarter-tone and quant 1 returns key numbers rounded to the nearest semitone. Quant can also be a function, e.g. round, ceil, or floor. unique : bool If unique is True then no duplicate key numbers will be returned. minkey : int | None If a minkey number is specified then spectral values lower than that will be octave shifted upward until they equal or exceed it. maxkey : int | None If a maxkey number is specified then spectral values higher than that will be octave shifted downward until they equal or lower than it. thresh : float | None The minimum amplitude that a frequency must possess in order to be returned as keys. Returns ------- A list of key number from the spectrum. """ if not (minkey < maxkey): raise TypeError("minkey {minkey} not less than {maxkey}.") if callable(quant) or quant is None: func = quant elif isinstance(quant, (int, float)): func = lambda x: quantize(x, quant) else: raise TypeError("quant value not a callable, number, or None: {quant}.") # drop freqs less than C00 or greater than G9 inbounds = lambda x: 8.175798915643707 <= x <= 12543.853951415975 keys = [keynum(x[0], func) for x in self if x[1] >= thresh and inbounds(x[0])] for i in range(len(keys)): while keys[i] < minkey: keys[i] += 12 while keys[i] > maxkey: keys[i] -= 12 if not minkey <= keys[i] <= maxkey: raise ValueError(f"key {keys[i]} outside bounds {minkey} to {maxkey}.") if unique: keys = list(dict.fromkeys(keys)) return sorted(keys)
def maxamp(self)
-
Returns the minimum amplitude in the spectrum.
Expand source code
def maxamp(self): """Returns the minimum amplitude in the spectrum.""" maxa = None for t in self: if not maxa or t[1] > maxa: maxa = t[1] return maxa
def maxfreq(self)
-
Returns the maximum frequency in the spectrum.
Expand source code
def maxfreq(self): """Returns the maximum frequency in the spectrum.""" return self[-1][0]
def minamp(self)
-
Returns the minimum amplitude in the spectrum.
Expand source code
def minamp(self): """Returns the minimum amplitude in the spectrum.""" mina = None for t in self: if not mina or t[1] < mina: mina = t[1] return mina
def minfreq(self)
-
Returns the minimum frequency in the spectrum.
Expand source code
def minfreq(self): """Returns the minimum frequency in the spectrum.""" return self[0][0]
def pairs(self)
-
Returns a list of frequency and amplitude pairs.
Expand source code
def pairs(self): """Returns a list of frequency and amplitude pairs.""" return list(self)
def size(self)
-
Expand source code
def size(self): return len(self)