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 or list 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)