from __future__ import annotations
from dataclasses import dataclass
from math import sqrt, ceil
from pitchtools import n2m, f2n, n2f, m2n, f2m, m2f, interval2ratio
from emlib.containers import RecordList
from emlib import misc
from emlib.mathlib import frange
import emlib.envir
import emlib.iterlib
from itertools import combinations
import maelzel.core as mc

from maelzel.common import asmidi
from typing import Sequence, Union


# ------------------------------------------------------------
#     COMBINATION TONES
# ------------------------------------------------------------


pitch_t = Union[int, float, str]


def _asnote(n) -> mc.Note:
    return n if isinstance(n, mc.Note) else mc.Note(n)
    

def _ringmod2f(f1: float, f2: float) -> tuple[float, float]:
    """
    Calculate the difference and combination freqs. between f1 and f2
    """
    comb = f1+f2
    diff = max(9., abs(f1-f2))
    return diff, comb


def _parsePitches(*pitches) -> list[float]:
    midis = []
    for p in pitches:
        if isinstance(p, str):
            midis.extend(map(n2m, p.split()))
        elif isinstance(p, (mc.Note, int, float)):
            midis.append(asmidi(p))
        elif isinstance(p, (list, tuple)):
            midis.extend(_parsePitches(*p))
        else:
            raise TypeError(p)
    return midis


def ringmodWithAmps(pitches: Sequence[pitch_t], amps: Sequence[float], unique=True
                   ) -> list[tuple[float, float]]:
    """
    Ringmodulation between pitches with amps

    Args:
        pitches: a midinote or notename
        amps: the amps corresponding to each pitch
        unique: if True, repeated sidebands are removed

    Returns:
        a list of tuples (midinote, amp), where each tuple represents
        one of the ring-mod sidebands

    """
    midinotes = _parsePitches(*pitches)
    freqs = list(map(m2f, midinotes))
    allbands = []
    allamps = []
    for f1, f2 in combinations(freqs, 2):
        lower, upper = _ringmod2f(f1, f2)
        midinotes = f2m(lower), f2m(upper)
        allbands.extend(midinotes)
    for amp1, amp2 in combinations(amps, 2):
        amp = amp1 * amp2
        allamps.extend((amp, amp))
    sidebands = zip(allbands, allamps)
    pairs = list(set(sidebands)) if unique else list(sidebands)
    pairs.sort()
    return pairs


def ringmod(pitches: Sequence[pitch_t]) -> list[float]:
    """
    Calculate the ring-modulation between the given pitches

    Args:
        pitches: a midinote or a notename. Many notenames can be given
            as one string, separated by spaces

    Returns:
        the sidebands, as midinotes

    Example
    ~~~~~~~

        >>> from maelzel.music import combtones
        >>> import pitchtools as pt
        >>> notes = [pt.m2n(m) for m in combtones.ringmod(('4C', '4E', '4A'))]
        >>> notes
        ['2Db-33', '2A+06', '3F+37', '5D+12', '5F+08', '5G-32']


    """
    midinotes = _parsePitches(*pitches)
    freqs = list(map(m2f, midinotes))
    allbands = []
    for f1, f2 in combinations(freqs, 2):
        bands = _ringmod2f(f1, f2)
        allbands.extend(bands)
    freqs = list(set(allbands))
    freqs.sort()
    return [f2m(freq) for freq in freqs]


def sumtones(*pitches: pitch_t) -> list[float]:
    """
    Calculate the summation tones generated by all the pair combinations of notes

    Args:
        pitches: a seq. of pitches as midinote or notename

    Returns:
        the summation tones, as midinotes

    """
    midis = _parsePitches(pitches)
    freqs = list(map(m2f, midis))
    return [f2m(f1+f2) for f1, f2 in combinations(freqs, 2)]
    

class Difftone:
    _fields = ("note0", "note1", "freq0", "freq1", "diff", "freq", "desired", "beating")

    def __init__(self, note0: pitch_t, note1: pitch_t, desired: pitch_t | None = None):
        self.note0: mc.Note = _asnote(note0)
        self.note1: mc.Note = _asnote(note1)
        self.freq0: float = round(self.note0.freq)
        self.freq1: float = round(self.note1.freq)
        self.diff: mc.Note = mc.Note(f2m(abs(self.note0.freq - self.note1.freq)))
        self.desired: mc.Note = _asnote(desired) if desired is not None else self.diff
        self.beatings:int = round(abs(self.desired.freq - self.diff.freq))
        self._notes = None

    def __iter__(self):
        # these correspond to _fields
        return iter((self.note0, self.note1, self.freq0, self.freq1, self.diff,
                     round(self.diff.freq), self.desired, self.beatings))

    def __repr__(self):
        if self.desired == self.diff:
            return f"Difftone(note0={self.note0}, note1={self.note1}, diff={self.diff})"
        return f"Difftone(note0={self.note0}, note1={self.note1}, diff={self.diff}, desired={self.desired}, beatings={self.beatings})"
        
    def transpose(self, interval:float) -> Difftone:
        note0 = self.note0 + interval
        note1 = self.note1 + interval 
        return Difftone(note0.name, note1.name)

    def asChord(self, diff=True) -> mc.Chord:
        """
        Convert this Difftone to a maelzel.core.Chord

        Args:
            diff: if True, the resulting difftone is included in the chord

        Returns:
            the resulting mc.Chord
        """
        if diff:
            ch = mc.Chord([self.note0, self.note1, self.diff])
        else:
            ch = mc.Chord([self.note0, self.note1])
        return ch

    def show(self) -> None:
        ch = self.asChord(diff=True)
        ch.show()

    def play(self, *args, **kws):
        return self.asChord(diff=False).play(*args, **kws)

    def shiftRatio(self, result):
        """
        Find a ratio r to multiply the freq. of the sources of this difftone so
        that they produce a difftone `result`

        .. note::

            if note1 and note2 produce a difftone d, when note1 and note2
            both are transposed by the same interval i, the resulting 
            difftone is also transposed by the same interval i

        Example

        d = difftoneSource("A3", interval=1)
        """
        return interval2ratio(self.diff.pitch - asmidi(result))


def difftones(*pitches: pitch_t) -> list[float]:
    """
    Calculate the difference tones generated by all possible pairs

    Args:
        pitches: the pitches to calculate difference tones for

    Returns:
        the difference tones as midinotes

    See Also
    ~~~~~~~~

    difftones_sources, difftone_sources_in_range
    """
    midis = _parsePitches(*pitches)
    freqs = [m2f(m) for m in midis]
    return [f2m(abs(f2 - f1)) for f1, f2 in combinations(freqs, 2)]
    

def difftonesCubic(*pitches: pitch_t) -> list[float]:
    """
    Return the cubic difference tones
    
    if f1 and f2 are pure tones and f1 < f2, the cubic diff-tone is ``2*f1 - f2``

    Args:
        pitches: the source pitches

    Returns:
        the cubic difference tones, as midinotes
    """
    freqs = [m2f(asmidi(note)) for note in pitches]
    out = []
    for f1, f2 in combinations(freqs, 2):
        if f2 < f1:
            f1, f2 = f2, f1
        f0 = f1*2 - f2
        out.append(f2m(f0))
    return out


def difftoneSource(difftone: pitch_t, interval: float, resolution=0.0
                   ) -> Difftone:
    """
    Find the source for the given difference tone

    Args:
        difftone: the resulting difference tone
        interval: the interval between the two notes which should produce a
            difference tone close to `difftone`
        resolution: the resolution of the pitch grid for the source notes. This
            also defines the acceptable error of the resulting difference tone

    Returns:
        a Difftone
    """
    diffFreq = m2f(asmidi(difftone))
    ratio = interval2ratio(interval)
    f0 = diffFreq / (ratio - 1)
    f1 = f0 * ratio
    midi0, midi1 = f2m(f0), f2m(f1)
    if resolution > 0:
        midi0 = float(misc.snap_to_grid(midi0, resolution))
        midi1 = float(misc.snap_to_grid(midi1, resolution))
    return Difftone(midi0, midi1, difftone)


def _difftoneFindSources(pitch,
                         minmidi: float,
                         maxmidi: float,
                         maxdeviation=0.5,
                         intervals=None,
                         resolution=1.
                         ) -> list[tuple[float, float]]:
    """

    Args:
        pitch: the resulting difftone
        maxdeviation: the max. deviation between the generated difftone
            and the desired one
        intervals: a list of possible intervals (None to allow any interval)
        minnote: the lowest note to search
        maxnote: the highest note to search
        resolution: the pitch resolution of the search space (1=semitone)

    Returns:
        a list of (lowest note, highest note))
    """
    pitch = asmidi(pitch)
    pitchfreq = m2f(pitch)
    intervals = intervals or list(frange(resolution, 8, resolution))
    f0 = m2f(pitch-maxdeviation)
    f1 = m2f(pitch+maxdeviation)
    out = []
    for interval in intervals:
        for m0 in frange(minmidi, maxmidi+resolution, resolution):
            m1 = m0 + interval
            if m1 >= maxmidi+resolution:
                continue
            diffFreq = abs(m2f(m0) - m2f(m1))
            if diffFreq > f1:
                break
            elif f0 <= diffFreq <= f1:
                out.append((abs(diffFreq - pitchfreq), (m0, m1)))
    if out:
        out.sort()
        out = [pair for error, pair in out]
        out.sort()
    return out


def _sumtone_find_source(pitch, 
                         maxdist=0.5, 
                         intervals: Sequence[int | float] | None = None,
                         minnote: pitch_t = 'A0', 
                         maxnote: pitch_t = 'C8', 
                         difftonegap=0.
                         ) -> list[tuple[str, str]]:
    """
    pitch: a note str or a midinote
    maxdist: the max. distance between the given note and #
                 the produced sumtone
    intervals: allowed intervals for the source pitches
    minnote, maxnote: the range to look for sumtones

    Returns: a list of pairs (note1:str, note2:str) representing
             notes which produce
             the given pitch as a sumtone
             (or an empty list if no pairs are found)
    """
    intervals = intervals or [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    m0 = asmidi(pitch)
    midimin = int(n2m(minnote)) if isinstance(minnote, str) else int(minnote)
    midimax = int(n2m(maxnote)) if isinstance(maxnote, str) else int(maxnote)
    minValidFreq = 1
    results = []
    for interval in intervals:
        for midi1 in range(midimin, midimax):
            midi2 = midi1 + interval
            if midi2 > midimax:
                continue
            sumFreq = m2f(midi1) + m2f(midi2)
            if sumFreq <= minValidFreq:
                continue
            diffFreq = abs(m2f(midi1) - m2f(midi2))
            if diffFreq < 20 or (abs(f2m(diffFreq) - f2m(sumFreq)) < difftonegap):
                continue
            midiError = abs(f2m(sumFreq) - m0)
            if midiError <= maxdist:
                results.append((midiError, (midi1, midi2)))
    if not results:
        return []
    results.sort()  # secondary sort by minimal difference
    pairs = list(set(midipair for diff, midipair in results))
    pairs.sort()    # primary sort by pitch
    out = [(m2n(m1), m2n(m2)) for m1, m2 in pairs]
    assert all(isinstance(n0, str) and isinstance(n1, str) for n0, n1 in out)
    return out


def difftoneSourcesFromSet(difftone: pitch_t,
                           sources: list[pitch_t],
                           maxdeviation=0.5,
                           exclude_intervals=None,
                           exclude_intervals_maxdeviation=0.5,
                           gap=0
                           ) -> list[tuple[float, float]]:
    difftone_pitch = asmidi(difftone)
    f0 = m2f(difftone_pitch - maxdeviation)
    f1 = m2f(difftone_pitch + maxdeviation)
    possible_sources = [source for source in sources if asmidi(source) >= difftone_pitch + gap]
    results = []
    for s1, s2 in combinations(possible_sources, 2):
        p1 = asmidi(s1)
        p2 = asmidi(s2)
        if p1 > p2:
            p1, p2 = p2, p1
            s1, s2, = s2, s1
        ok = True
        if exclude_intervals:
            interval_now = abs(p1 - p2)
            for excluded in exclude_intervals:
                if abs(interval_now - excluded) < exclude_intervals_maxdeviation:
                    ok = False
                    break
        if not ok:
            continue
        diff_freq = abs(m2f(p1) - m2f(p2))
        if f0 <= diff_freq <= f1:
            results.append((s1, s2))
    results.sort(key=lambda pair: asmidi(pair[0]))
    return results


def difftoneSources(result: pitch_t,
                    maxdist=0.5,
                    gap=13.0,
                    minnote: pitch_t | None = None,
                    maxnote: pitch_t = "C8",
                    intervals: list[float] | None = None,
                    resolution=1.,
                    display=False
                    ) -> RecordList:
    """
    Find two notes which produce a difference tone near the given note

    Args:
        result: the resulting difference tone, as mc.Note, notename or midinote
        maxdist: the maximum distance between the expected result and the generated tone,
            in semitones (0.5 == 50 cents)
        gap: the distance between the resulting difference tone and the lowest of
            the two originating notes
        minnote: the min. note to considere as a source
        maxnote: the max. note to considere as a source
        intervals: accepted intervals between the notes. If None is given, a set of intervals
            based on resolution is used
        resolution: the resolution of the pitch grid, in semitones. A resolution of 1 will
            search for the given difftone along all semitones between minnote and
            maxnote
        display: if True, the result is printed

    Returns:
        a list of Difftones
    """
    midiresult = asmidi(result)
    maxpitch = asmidi(maxnote)
    minpitch = asmidi(minnote) if minnote is not None else midiresult + gap
    minpitch = max(minpitch, midiresult + gap)
    assert maxpitch > minpitch
    intervals = intervals or list(frange(resolution, 7, resolution))
    pairs = _difftoneFindSources(pitch=result,
                                 maxdeviation=maxdist,
                                 minmidi=minpitch,
                                 maxmidi=maxpitch,
                                 intervals=intervals,
                                 resolution=resolution)
    difftones = [Difftone(note0=midi0, note1=midi1, desired=midiresult)
                 for midi0, midi1 in pairs]
    reclist = RecordList(difftones, fields=Difftone._fields)
    if display:
        chordseq = mc.Chain([mc.Chord([diff.note0, diff.note1, diff.diff]) for diff in difftones])
        chordseq.show()
        if emlib.envir.inside_jupyter():
            from IPython.display import display
            display(reclist)
        else:
            print(reclist)
    return reclist


def ringmodExactSource(sideband1: pitch_t, sideband2: pitch_t
                       ) -> tuple[float, float]:
    """
    Find a pair of frequencies which produce the given sidebands when ringmodulated

    Args:
        sideband1, sideband2: the sidebands produced, as notename or midinote

    Returns:
        the original pitches, as midinotes
    """
    diffFreq = m2f(asmidi(sideband1))
    sumFreq = m2f(asmidi(sideband2))
    f1 = (diffFreq + sumFreq) / 2.0
    f0 = sumFreq - f1
    return f2m(f0), f2m(f1)
    

def _matchone(orig, new, maxdiff):
    """
    match at most one item from new with one item from orig

    orig, new and maxdiff should all have the same unit (freq, midi, etc)
    """
    mindiffs = {}
    matches = {}
    for o, n in emlib.iterlib.product(orig, new):
        diff = abs(o-n)
        if diff < maxdiff and diff < mindiffs.get(o, float('inf')):
            mindiffs[o] = diff
            matches[o] = n
    if not matches:
        return []
    return matches.items()


def ringmodSource(sidebands: pitch_t,
                  minnote: pitch_t = "A0",
                  maxnote: pitch_t = "C8",
                  maxdiff=0.5,
                  matchall=True,
                  constraints=None,
                  numsources=None) -> list[float]:
    """
    Find notes that, when ringmodulated together, produce the given sidebands

    Args:
        sidebands: a seq. of sidebands, as notenames or frequencies
        minnote: min. note of the source
        maxnote: max. note of the source
        maxdiff: the max distance between the desired sidebands and the actual sidebands
        matchall: if True, all sidebands should be matched
        constraints: if given, a seq. of functions. Each of these constraints is of
                     the form (midisources, sidebands) -> bool
                     where: midisources: the sources being modulated (as midinotes)
                            sidebands: the sidebands generated (as midinote)
        numsources: the number of sources can be specified. Otherwise it depends on
            the number of sidebands given

    Returns:
        a list of midinotes which, when ringmodulated, produce the given sidebands

    Example
    ~~~~~~~

    Find a seq. of frequencies which generate the folowing sidebands, given that
    all sidebands should lie within the interval C2-C6

        >>> sidebands = ["C4", "E4", "B5"]
        >>> constraints = [lambda freqs: all(n2f("C2") <= freq <= n2f("C6") for freq in freqs)]
        >>> source = ringmodSource(sidebands, matchall=True, constraints=constraints)
        >>> print(map(m2n, source))
        ['2C#', '4E', '5E']
        >>> newsidebands = ringmod(*sourcefreqs)
        >>> print(map(f2n, newsidebands))
        ['4C-09', '4E', '4G+30', '5D+08', '5Gb-27', '5B+02']
    """
    assert isinstance(sidebands, (list, tuple))
    sidemidis = [asmidi(sb) for sb in sidebands]
    midi0, midi1 = n2m(minnote), n2m(maxnote)
    if len(sidebands) == 2 and (numsources == 2 or numsources is None):
        note1, note2 = ringmodExactSource(sidebands[0], sidebands[1])
        return [note1, note2]
    elif len(sidebands) > 6 or numsources is not None and numsources > 3:
        raise NotImplementedError("too many sidebands...")
    bestmatch = []
    sourcemidis = []
    for m0, m1, m2 in combinations(range(int(midi0), int(ceil(midi1))), 3):
        newmidis = ringmod([m0, m1, m2])
        matching = _matchone(sidemidis, newmidis, maxdiff)
        if not matching:
            continue
        elif matchall and len(matching) < len(sidebands):
            continue
        elif constraints and not all(constr([m0, m1, m2], newmidis)
                                     for constr in constraints):
            continue
        if len(matching) > len(bestmatch):
            bestmatch = matching
            sourcemidis = newmidis
        elif len(matching) == len(bestmatch):
            newdiff = sum(abs(orig-new) for orig, new in matching)
            lastdiff = sum(abs(orig-last) for orig, last in bestmatch)
            if newdiff < lastdiff:
                sourcemidis = newmidis
                bestmatch = matching
    return sourcemidis

        
@dataclass
class SumTone:
    notes: tuple[str, str]
    sumnote: str
    sumfreq: float
    error: float


def sumtoneSources(result,
                   maxdist=0.5,
                   maxnote="C8",
                   intervals: Sequence[int | float] = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
                   show=False, minnote="A0", difftonegap=0):
    """
    find two notes which produce a sumation tone near the given note

    Args:
        result: the resulting difference tone, as note
        maxdist: the maximum distance between the expected result and the generated tone,
            in semitones (0.5 == 50 cents)
        maxnote: the highest note to considere
        intervals: accepted intervals between the notes. Only 'tempered' notes are used,
            but of course microtones produce also difference tones!
        minnote: the min. note to considere as source
        difftonegap : minimum gap (absolute value) between the sumtone and the difftone
            produced by the two notes
        show: either False or one of:
            'report': produce a text report
            'musicxml': convert to musicxml, show as Finale file
            'lily': convert to lilypond, show as PDF
    """
    result = asmidi(result)
    maxnote = asmidi(maxnote)
    pairs = _sumtone_find_source(result, maxdist, minnote=minnote, maxnote=maxnote,
                                 intervals=intervals, difftonegap=difftonegap)
    if not pairs:
        if show == 'report':
            import warnings
            warnings.warn("No source pair of notes found that satisfy the given conditions")
        return []
    out = []
    resultfreq = m2f(result)
    for note0, note1 in pairs:
        sumfreq = n2f(note0) + n2f(note1)
        sumtone = SumTone((note0, note1), f2n(sumfreq), sumfreq, abs(sumfreq-resultfreq))
        out.append(sumtone)
    if show == 'report':
        raise NotImplementedError()
        # _sumtones_sources_report(result, out)
    elif show:
        from maelzel import scoring
        events = []
        for pair in out:
            notes = []
            notes.extend(pair.notes)
            notes.append(pair.sumnote)
            chord = scoring.Notation(pitches=notes, duration=1)
            chord.addArticulation('sum: %.2f' % pair.sumfreq)
            events.append(chord)
        r = scoring.render.render(events)
        r.show()
    return RecordList(out)


def overtoneRelativeAmplitude(wavetype="saw", overtone=2) -> float:
    """
    Return the relative amplitude of the overtone given the wavetype

    Args:
        wavetype: the kind of soundwave. One of 'saw', 'square', 'tri'

    Available wavetypes are: saw, sine, square, and tri (triangle)
    """
    if overtone < 1:
        raise ValueError("Overtone should be >= 1 (f0 = overtone 1)")
    if wavetype == "saw":
        return 1/overtone 
    elif wavetype == "square":
        return 1 / overtone * (overtone % 2)
    elif wavetype == "tri":
        return 1 / (overtone**2) * (overtone % 2)
    else:
        raise KeyError("wavetype not known, should be one of saw, square or tri")
    

# #------------------------------------------------------------
# #
# #    estimate beatings
# #
# #------------------------------------------------------------


@dataclass
class DifftoneBeating:
    """
    A class representing the beatings produced within a difference tone
    """
    beatings: float
    "beatings per second"

    note1: mc.Note
    "first note of the pair producing a difference tone"

    note2: mc.Note
    "second note of the pair producing a difference tone"

    relativeAmplitude: float = 1


def difftoneBeatings(pitch1: pitch_t, pitch2: pitch_t, maxbeatings=20, minbeatings=0.3,
                     wave="saw",
                     ) -> list[DifftoneBeating]:
    """
    estimate the beatings generated by the difference tone(s) between note1 and note2
    
    for each pair of notes other than sinus-tones, many difference tones are generated:
       * between the fundamentals
       * between the overtones
    
    The calculation is based on the relative amplitude of the overtones and presuposes
    simple waveforms. Any other routine would have to work with the actual sample data
    to calculate the beatings.
    
    Using saw waves gives a sort of upper limit to the beatings which can be generated
    between two notes. In reality, the amplitude of the beatings will always be less than
    what is reported here.
    """
    defaultwave = wave
    freq1 = m2f(asmidi(pitch1))
    freq2 = m2f(asmidi(pitch2))
    overtones1 = [freq1 * i for i in range(1, 7)]
    overtones2 = [freq2 * i for i in range(1, 7)]
    pairs = []
    for i, o1 in enumerate(overtones1):
        for j, o2 in enumerate(overtones2):
            if minbeatings <= abs(o1 - o2) <= maxbeatings:
                amp1 = overtoneRelativeAmplitude(defaultwave, i)
                amp2 = overtoneRelativeAmplitude(defaultwave, j)
                amp = amp1 * amp2
                if amp > 0:
                    pairs.append((amp, o1, o2))
    pairs.sort(reverse=True)
    pairs2 = [DifftoneBeating(abs(o1-o2), mc.Note(f2m(o1)), mc.Note(f2m(o2)))
              for amp, o1, o2 in pairs]
    return pairs2


def fmbands(carrierfreq: float,
            modfreq: float,
            index: float,
            minamp=0.01,
            minfreq=0,
            maxfreq=0
            ) -> list[tuple[float, float]]:
    """
    A list of sidebands with min. amp at the given modulation index

    Args:
        carrierfreq: the fm carrier frequency
        modfreq: the fm modulation frequency
        index: the fm modulation index
        minamp: min. relative amp
        minfreq: min. sideband frequency
        maxfreq: max. sideband frequency

    Returns:
        a list of sidebands, where each sideband is a tuple (freq, amp)
    """
    # calcular las amplitudes con scipy.special.jv(n, index),
    # donde n es el indice de la banda
    from scipy.special import jv
    bands = []
    for n in range(0, 200):
        freq0 = carrierfreq + modfreq*n
        amp = abs(jv(n, index))
        freq1 = abs(carrierfreq - modfreq*n)
        if amp > minamp:
            bands.append((freq0, amp))
            if freq1 != freq0 and freq1 > 0:
                bands.append((freq1, amp))
    bands.sort()
    if minfreq > 0 or maxfreq > 0:
        maxfreq = maxfreq if maxfreq > 0 else 24000
        bands = [band for band in bands if minfreq <= band[0] < maxfreq]
    return bands


def _checkpitch(p: str | int | float) -> bool:
    if isinstance(p, str):
        return True
    if isinstance(p, (int, float)):
        if p > 127:
            raise ValueError(f"Expected a pitch (a notename or a midinote) but got {p}")
        return True
    raise TypeError(f"Expected a str or a float, but got {type(p)}")


def fmchord(carrierfreq: float, modfreq: float, index: float, minamp=0.01,
            minpitch: pitch_t = "A0", maxpitch: pitch_t = "C8"
            ) -> mc.Chord:
    """
    Similar to fmbands but the result is a maelzel.core.Chord

    Each sideband is represented as a note within the chord, with the corresponding
    pitch and relative amplitude

    Args:
        carrierfreq: the carrier frequency
        modfreq: the modulator frequency
        index: the modulation index
        minamp: the mininum amplitude of a sideband to be included
        minpitch: the min. pitch for any sideband
        maxpitch: the max. pitch for any sideband

    Returns:
        a maelzel.core.Chord with the sidebands and their corresponding amplitude

    """
    maxfreq = 24000 if maxpitch is None else int(_asnote(maxpitch).freq)
    minfreq = 0 if minpitch is None else int(_asnote(minpitch).freq)
    bands = fmbands(carrierfreq=carrierfreq, modfreq=modfreq, index=index,
                    minamp=minamp, minfreq=minfreq, maxfreq=maxfreq)
    notes = [mc.Note(f2m(freq), amp=amp) for freq, amp in bands]
    return mc.Chord(notes)
    

def difftoneEvaluateInharmonicity(pitch1: pitch_t, pitch2: pitch_t) -> float:
    """
    Evaluates the inharmonicity of the difftone generated by pitch1 and pitch2

    Args:
        pitch1: the first pitch
        pitch2: the second pitch

    Returns:
        the inharmonicity

    """
    m1 = asmidi(pitch1)
    m2 = asmidi(pitch2)
    if m1 > m2:
        m1, m2 = m2, m1
    md = f2m(abs(m2f(m1) - m2f(m2)))
    f1, f2, fd = map(m2f, (m1, m2, md))
    rat_2_1 = f2/f1
    rat_2_d = f2/fd
    rat_1_d = f1/fd
    ratq_2_1 = misc.snap_to_grid(rat_2_1, 0.5)
    ratq_2_d = misc.snap_to_grid(rat_2_d, 0.5)
    ratq_1_d = misc.snap_to_grid(rat_1_d, 0.5)
    delta_2_1 = abs(rat_2_1 - ratq_2_1) / rat_2_1
    delta_2_d = abs(rat_2_d - ratq_2_d) / rat_2_d
    delta_1_d = abs(rat_1_d - ratq_1_d) / rat_1_d
    delta = sqrt(delta_2_1**2 + delta_2_d**2 + delta_1_d**2)
    return delta
