def add_partial(self, partial: sndtrck.Partial): """ Raises TrackFull if the partial can't be added """ for p in self.partials: if p.t0 < partial.t0: if p.t1 + self.mingap > partial.t0: if p.t1 > partial.t0: raise TrackFullError( f"Partial doesn't fit. Own partial {p}, t1+mingap={p.t1+self.mingap} > new partial.t0 {partial.t0}" ) else: if partial.t1 + self.mingap > p.t0: raise TrackFullError("partial does not fit") self.partials.append(partial) self.partials.sort(key=lambda p: p.t0) assert not self.has_overlap() minnote = f2m(partial.minfreq) maxnote = f2m(partial.maxfreq) if len(self.partials) == 1: # track was empty self.minnote = minnote self.maxnote = maxnote else: assert self.minnote is not None and self.maxnote is not None self.minnote = min(self.minnote, minnote) self.maxnote = max(self.maxnote, maxnote) self._changed() return True
def rate_partial(self, partial: sndtrck.Partial, maxrange: int, minmargin: float = None) -> float: """ Rates how good this partial fits in this track. Returns > 0 if partial fits, the value returned indicates how good it fits """ if minmargin is None: minmargin = self.mingap assert minmargin is not None isempty = self.isempty() if isempty: margin = partial.t0 elif not self.partial_fits(partial.t0, partial.t1): return -1 else: partial_left_idx = self.partial_left_to(partial.t0) if partial_left_idx is None: assert all(p.t0 > partial.t1 for p in self.partials) margin = partial.t0 else: # the found partial is either the last partial, or the partial next # to it shoudl start AFTER the partial being rated assert partial_left_idx == len( self.partials) - 1 or self.partials[partial_left_idx + 1].t0 > partial.t1 p0 = self.partials[partial_left_idx] # Partials should be left indented margin = partial.t0 - p0.t1 assert margin >= minmargin # Try to pack as tight as possible margin_rating = bpf.halfcos(minmargin, 1, 1, 0.01, exp=0.6)(margin) margin_weight, range_weight, wrange_weight = 3, 1, 1 if isempty: return euclidian_distance( [margin_rating, 1, 1], [margin_weight, range_weight, wrange_weight]) trackminnote, trackmaxnote = self.track_range() minnote = f2m(partial.minfreq) maxnote = f2m(partial.maxfreq) range_with_note = max(trackmaxnote, maxnote) - min( trackminnote, minnote) if range_with_note > maxrange: return -1 range_rating = bpf.expon(0, 1, maxrange, 0.0001, exp=1)(range_with_note) avgpitch = self.avgpitch() avgdiff = abs(avgpitch - f2m(partial.meanfreq_weighted)) wrange_rating = bpf.halfcos(0, 1, maxrange, 0.0001, exp=0.5)(avgdiff) total = euclidian_distance( [margin_rating, range_rating, wrange_rating], [margin_weight, range_weight, wrange_weight]) return total
def ringmod_sources2(sideband1: pitch_t, sideband2: pitch_t, minnote: pitch_t = "A0", maxnote: pitch_t = "C8", maxdiff=0.5) -> List[Tuple[Note, Note]]: """ Find all pairs of two frequencies which produce the given sidebands when ringmodulated Returns a list of Notes minnote, maxnote: the range for possible answers maxdiff: the max. difference between the given sidebands and the resulting sidebands, in midi (1=semitone) """ difm = asmidi(sideband1) summ = asmidi(sideband2) midimin = n2m(minnote) if isinstance(minnote, str) else minnote midimax = n2m(maxnote) if isinstance(maxnote, str) else maxnote results = [] for midi1, midi2 in combinations(range(int(midimin), int(ceil(midimax))), 2): note0, note1 = ringmod_exactsource(m2f(midi1), m2f(midi2)) f0 = note0.freq f1 = note1.freq if abs(f2m(f0 + f1) - summ) <= maxdiff and abs(f2m(abs(f1 - f0)) - difm) <= maxdiff: results.append((note0, note1)) results.sort() return results
def _note_deviates(b0, b1, b2, pitchdelta: float, dbdelta: float): """ True if b1 deviates from the interpolation of b0 and b2 dbdelta and pitchdelta can be negative, in which case they are not taken into account bx: a tuplet of (time, freq, amp, ...) """ t0 = b0[0] t = b1[0] t1 = b2[0] dt = (t - t0) / (t1 - t0) if dbdelta >= 0: a0 = b0[2] a = b1[2] a1 = b2[2] a_t = a0 + dt * (a1 - a0) if abs(amp2db(a) - amp2db(a_t)) >= dbdelta: return True if pitchdelta >= 0: f0 = b0[1] f = b1[1] f1 = b2[1] f_t = f0 + dt * (f1 - f0) if abs(f2m(f_t) - f2m(f)) >= pitchdelta: return True return False
def ringmod_exactsource(sideband1: pitch_t, sideband2: pitch_t) -> Tuple[Note, Note]: """ Find a pair of frequencies which, when ringmodulated, result in the given sidebands 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 Note(f2m(f0)), Note(f2m(f1))
def readSpectrumAsChords(path, numsteps=8, maxNotesPerChord=inf) -> List[chord_t]: """ Reads the spectrum saved in `path`, splits it into at most `numsteps` chords, depending on their amplitude. The information saved by audacity represents the spectrum of the selected audio Args: path: the path of the saved spectrum (a .txt file) numsteps: the number of steps to split the spectral information into, according to their amplitude. Each step can be seen as a "layer" maxNotesPerChord: the max. number of bins for each "layer". Normally the loudest layers will have fewer components """ data = readSpectrum(path) notes = [] for bin_ in data: note = Note(note=f2n(bin_.freq), midi=f2m(bin_.freq), freq=bin_.freq, level=bin_.level, step=dbToStep(bin_.level, numsteps)) notes.append(note) chords = [[] for _ in range(numsteps)] notes2 = sorted(notes, key=lambda n: n.level, reverse=True) for note in notes2: chord = chords[note.step] if len(chord) <= maxNotesPerChord: chord.append(note) for chord in chords: chord.sort(key=lambda n: n.level, reverse=True) return chords
def _sumtone_find_source(pitch, maxdist=0.5, intervals: List[U[int, float]] = None, minnote: pitch_t = 'A0', maxnote: pitch_t = 'C8', difftonegap=0.): """ 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 sumtones(*pitches: pitch_t) -> List[Note]: """ Calculate the summation tones generated by all the pair combinations of notes pitches: a seq. of Notes notes: a seq of notes as strings "C4", "C5+", etc. or frequencies. """ midis = _parsePitches(pitches) freqs = list(map(m2f, midis)) return [Note(f2m(f1 + f2)) for f1, f2 in combinations(freqs, 2)]
def difftones(*pitches: pitch_t) -> List[Note]: """ Calculate the difference tones generated by all the pair combinations of notes SEE ALSO: difftones_sources, difftone_sources_in_range """ midis = _parsePitches(*pitches) freqs = [m2f(m) for m in midis] return [Note(f2m(abs(f2 - f1))) for f1, f2 in combinations(freqs, 2)]
def difftones_beatings( 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 = wave_overtone_relative_amplitude(defaultwave, i) amp2 = wave_overtone_relative_amplitude(defaultwave, j) amp = amp1 * amp2 if amp > 0: pairs.append((amp, o1, o2)) pairs.sort(reverse=True) pairs2 = [ DifftoneBeating(abs(o1 - o2), Note(f2m(o1)), Note(f2m(o2)), freq1, freq2, amp) for amp, o1, o2 in pairs ] return pairs2
def _breakpoint_deviates(b0, b1, b2, dbdelta, pitchdelta, bwdelta): """ True if b1 deviates from the interpolation of b0 and b2 bx: a tuplet of (time, freq, amp, bw) """ t0, f0, a0, bw0 = b0 t, f, a, bw = b1 t1, f1, a1, bw1 = b2 a_t = interpol_linear(t, t0, a0, t1, a1) varamp = abs(amp2db(a) - amp2db(a_t)) if varamp >= dbdelta: return True f_t = interpol_linear(t, t0, f0, t1, f1) varpitch = abs(f2m(f_t) - f2m(f)) if varpitch >= pitchdelta: return True bw_t = interpol_linear(t, t0, bw0, t1, bw1) if abs(bw_t - bw) >= bwdelta: return True return False
def difftone_source(difftone: pitch_t, interval: float, resolution=0.0) -> Difftone: """ difftone: the resulting difftone 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 difftone 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 = misc.snap_to_grid(midi0, resolution) midi1 = misc.snap_to_grid(midi1, resolution) return Difftone(midi0, midi1, difftone)
def avgpitch(self) -> float: """ Returns: This track's average pitch (midinote) """ if self.isempty(): raise IndexError("This Track is empty") if self._avgpitch < 0: self._avgpitch = sum( f2m(partial.meanfreq_weighted) for partial in self.partials) / len(self.partials) # assert self.minnote <= self._avgpitch <= self.maxnote, f"minnote={self.minnote}, avg={self._avgpitch}, maxnote={self.maxnote}" return self._avgpitch
def __init__(self, note0: pitch_t, note1: pitch_t, desired: pitch_t = None): self.note0: Note = asNote(note0) self.note1: Note = asNote(note1) self.freq0: float = round(self.note0.freq) self.freq1: float = round(self.note1.freq) self.diff: Note = Note(f2m(abs(self.note0.freq - self.note1.freq))) self.desired: 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 fm_chord(carrierfreq: float, modfreq: float, index: float, minamp=0.01, minpitch=None, maxpitch=None) -> Chord: maxfreq = 24000 if maxpitch is None else asNote(maxpitch).freq minfreq = 0 if minpitch is None else asNote(minpitch).freq bands = fm_sidebands(carrierfreq=carrierfreq, modfreq=modfreq, index=index, minamp=minamp, minfreq=minfreq, maxfreq=maxfreq) notes = [Note(f2m(freq), amp=amp) for freq, amp in bands] return Chord(notes)
def difftones_cubic(*notes: pitch_t) -> List[Note]: """ Return the cubic difference tones if f1 and f2 are pure tones and f1 < f2, the cubic diff-tone is 2*f1 - f2 """ freqs = [m2f(asmidi(note)) for note in notes] out = [] for f1, f2 in combinations(freqs, 2): if f2 < f1: f1, f2 = f2, f1 f0 = f1 * 2 - f2 out.append(Note(f2m(f0))) return out
def sound2harmonic(self, note, kind='all', tolerance=0.5): """ find the harmonics in this string which can produce the given sound as result. note: the note to produce (a string note) kind: kind of harmonic. One of [4, 3M, 3m, natural, all] tolerance: the acceptable difference between the desired note and the result (in semitones) """ midinote = n2m(note) if kind == '4' or kind == 4: f0 = midinote - 24 out = self.sound2note(m2n(f0)) elif kind == '3M' or kind == 3: f0 = midinote - 28 out = self.sound2note(m2n(f0)) elif kind == '3m': f0 = midinote - 31 out = self.sound2note(m2n(f0)) elif kind in ('n', 'natural'): fundamental = m2f(self._sounding_midi) harmonics = [f2m(fundamental * harmonic) for harmonic in range(12)] acceptable_harmonics = [] for harmonic in harmonics: if abs(harmonic - midinote) <= tolerance: acceptable_harmonics.append(harmonic) if len(acceptable_harmonics) > 0: # now find the position of the node in the string results = [] nodes = [] for harmonic in acceptable_harmonics: fret = self._flageolet_string.ratio2fret( m2f(harmonic) / fundamental) nodes.append(fret) for node in nodes: for fret_pos in node.frets_pos: results.append(fret_pos[1]) # we only append the pitch out = [self.sound2note(result) for result in results] else: out = None elif kind == 'all': out = [] for kind in ('4 3M 3m n'.split()): out.append( self.sound2harmonic(note, kind=kind, tolerance=tolerance)) return out return kind, out
def ringmod(*pitches: pitch_t) -> List[Note]: """ Calculate the ring-modulation between the given notes pitches: a midinote, a notename or a Note (no frequencies!) Many notenames can be given as one string, separated by spaces Returns the notes of the sidebands, as Notes """ for p in pitches: _checkpitch(p) midis = _parsePitches(*pitches) freqs = list(map(m2f, midis)) sidebands = [_ringmod2f(f1, f2) for f1, f2 in combinations(freqs, 2)] all_sidebands: List[float] = list(set(flatten(sidebands))) all_sidebands.sort() return [Note(f2m(sideband)) for sideband in all_sidebands]
def difftone_evaluate_inharmonicity(pitch1: pitch_t, pitch2: pitch_t) -> float: 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 print(delta_2_1, delta_2_d, delta_1_d) delta = sqrt(delta_2_1**2 + delta_2_d**2 + delta_1_d**2) return delta
def gradient(t, f): semitones = curve(t) f2 = m2f(f2m(f) + semitones) return f2
def transpose(spectrum: sp.Spectrum, semitones:float) -> sp.Spectrum: """ Transpose spectrum by a fixed number of semitones """ curve = bpf.asbpf(lambda f: m2f((f2m(f)+semitones))) return spectrum.freqwarp(curve)
def ringmod_sources(sidebands: pitch_t, minnote: pitch_t = "A0", maxnote: pitch_t = "C8", maxdiff=0.5, matchall=True, constraints=None, numsources=None) -> List[Note]: """ Given a seq. of sidebands, find notes that, when ringmodulated together, include these sidebands as the result. sidebands: a seq. of sidebands, as notenames or frequencies minnote, maxnote: limit the possible range of the sourcefreqs 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) 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)] sourcefreqs = ringmod_sources(sidebands, matchall=True, constraints=constraints) print(map(f2n, sourcefreqs)) --> ['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 = ringmod_exactsource(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 = [] sourcefreqs = None for m0, m1, m2 in combinations(range(int(midi0), int(ceil(midi1))), 3): newbands = ringmod(m0, m1, m2) newmidis = [band.midi for band in newbands] 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 sourcefreqs = [m2f(m) for m in 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: sourcefreqs = [m2f(m) for m in newmidis] bestmatch = matching return [Note(f2m(freq)) for freq in sourcefreqs]
def transpose(spectrum: sp.Spectrum, semitones: float) -> sp.Spectrum: """ Transpose spectrum by a fixed number of semitones """ curve = bpf.asbpf(lambda f: m2f((f2m(f) + semitones))) return spectrum.freqwarp(curve)
def addpartial(self, partial: sndtrck.Partial) -> bool: """ Returns True if partial could be added """ assert not self.isrendered() # max_overlap = R(1, 16) max_overlap = 0 if not self.isempty(): if partial.t0 < self.end: raise ValueError( f"Overlap detected: partials starts at {partial.t0}, voice ends at {self.end}" ) # partialdata: 2D numpy with columns [time, freq, amp, phase, bw] partialdata: np.ndarray = partial.toarray() amps = partialdata[:, 2] assert np.all(amps[1:-1] > 0) freqs = partialdata[:, 1] assert np.all(freqs[1:-1] > 0) if len(partialdata) < 2: logger.error("Trying to add an empty partial, skipping") return False # TODO: hacer minamp configurable minamp = db2amp(-90) # The 1st and last bp can have amp=0, used to avoid clicks. Should we include them? if len(partialdata) > 2 and partialdata[0, 2] == 0 and partialdata[ 0, 1] == partialdata[1, 1]: partialdata = partialdata[1:] notes: List[Note] = [] for i in range(len(partialdata) - 1): t0, freq0, amp0, phase0, bw0 = partialdata[i, 0:5] t1, freq1, amp1 = partialdata[i + 1, 0:3] dur = t1 - t0 if dur < 1e-12: logger.error("small note: " + str((t0, t1, freq0, amp0))) pitch = f2m(freq0) amp = amp0 note = Note(pitch, t0, dur, max(amp, minamp), bw0, tied=False, color="@addpartial") notes.append(note) # The last breakpoint was not added: add it if it would make a # difference in pitch and is not just a closing bp (with amp=0) t0, f0, a0 = partialdata[-2, 0:3] # butlast t1, f1, a1 = partialdata[-1, 0:3] # last if a1 > 0 and abs(f2m(f1) - f2m(f0)) > 0.5: lastnote_dur = min(self.lastnote_duration, notes[-1].dur) notes.append( Note(f2m(f1), start=t1, dur=lastnote_dur, amp=a1, color="@lastnote")) notes.sort(key=lambda n: n.start) mindur = R(1, 128) if has_short_notes(notes, mindur): logger.error(">>>>> short notes detected") logger.error("\n ".join( str(n) for n in notes if n.dur < mindur)) raise ValueError("short notes detected") if any(n.amp == 0 for n in notes): logger.error("Notes with amp=0 detected: ") logger.error("\n ".join( str(n) for n in notes if n.amp == 0)) raise ValueError("notes with amp=0 detected") self.addnotes(notes) self.added_partials += 1 return True
def _calculate_range(self): self.minnote = f2m(min(p.minfreq for p in self.partials)) self.maxnote = f2m(max(p.maxfreq for p in self.partials))