# -*- coding: utf-8 -*-
# This file is part of AudioLazy, the signal processing Python package.
# Copyright (C) 2012-2016 Danilo de Jesus da Silva Bellini
#
# AudioLazy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
MIDI representation data & note-frequency relationship
"""
import itertools as it
# Audiolazy internal imports
from .lazy_misc import elementwise
from .lazy_math import log2, nan, isinf, isnan
__all__ = ["MIDI_A4", "FREQ_A4", "SEMITONE_RATIO", "str2freq",
"str2midi", "freq2str", "freq2midi", "midi2freq", "midi2str",
"octaves"]
# Useful constants
MIDI_A4 = 69 # MIDI Pitch number
FREQ_A4 = 440. # Hz
SEMITONE_RATIO = 2. ** (1. / 12.) # Ascending
@elementwise("midi_number", 0)
[docs]def midi2freq(midi_number):
"""
Given a MIDI pitch number, returns its frequency in Hz.
"""
return FREQ_A4 * 2 ** ((midi_number - MIDI_A4) * (1./12.))
@elementwise("note_string", 0)
[docs]def str2midi(note_string):
"""
Given a note string name (e.g. "Bb4"), returns its MIDI pitch number.
"""
if note_string == "?":
return nan
data = note_string.strip().lower()
name2delta = {"c": -9, "d": -7, "e": -5, "f": -4, "g": -2, "a": 0, "b": 2}
accident2delta = {"b": -1, "#": 1, "x": 2}
accidents = list(it.takewhile(lambda el: el in accident2delta, data[1:]))
octave_delta = int(data[len(accidents) + 1:]) - 4
return (MIDI_A4 +
name2delta[data[0]] + # Name
sum(accident2delta[ac] for ac in accidents) + # Accident
12 * octave_delta # Octave
)
[docs]def str2freq(note_string):
"""
Given a note string name (e.g. "F#2"), returns its frequency in Hz.
"""
return midi2freq(str2midi(note_string))
@elementwise("freq", 0)
[docs]def freq2midi(freq):
"""
Given a frequency in Hz, returns its MIDI pitch number.
"""
result = 12 * (log2(freq) - log2(FREQ_A4)) + MIDI_A4
return nan if isinstance(result, complex) else result
@elementwise("midi_number", 0)
[docs]def midi2str(midi_number, sharp=True):
"""
Given a MIDI pitch number, returns its note string name (e.g. "C3").
"""
if isinf(midi_number) or isnan(midi_number):
return "?"
num = midi_number - (MIDI_A4 - 4 * 12 - 9)
note = (num + .5) % 12 - .5
rnote = int(round(note))
error = note - rnote
octave = str(int(round((num - note) / 12.)))
if sharp:
names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
else:
names = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]
names = names[rnote] + octave
if abs(error) < 1e-4:
return names
else:
err_sig = "+" if error > 0 else "-"
err_str = err_sig + str(round(100 * abs(error), 2)) + "%"
return names + err_str
[docs]def freq2str(freq):
"""
Given a frequency in Hz, returns its note string name (e.g. "D7").
"""
return midi2str(freq2midi(freq))
[docs]def octaves(freq, fmin=20., fmax=2e4):
"""
Given a frequency and a frequency range, returns all frequencies in that
range that is an integer number of octaves related to the given frequency.
Parameters
----------
freq :
Frequency, in any (linear) unit.
fmin, fmax :
Frequency range, in the same unit of ``freq``. Defaults to 20.0 and
20,000.0, respectively.
Returns
-------
A list of frequencies, in the same unit of ``freq`` and in ascending order.
Examples
--------
>>> from audiolazy import octaves, sHz
>>> octaves(440.)
[27.5, 55.0, 110.0, 220.0, 440.0, 880.0, 1760.0, 3520.0, 7040.0, 14080.0]
>>> octaves(440., fmin=3000)
[3520.0, 7040.0, 14080.0]
>>> Hz = sHz(44100)[1] # Conversion unit from sample rate
>>> freqs = octaves(440 * Hz, fmin=300 * Hz, fmax = 1000 * Hz) # rad/sample
>>> len(freqs) # Number of octaves
2
>>> [round(f, 6) for f in freqs] # Values in rad/sample
[0.062689, 0.125379]
>>> [round(f / Hz, 6) for f in freqs] # Values in Hz
[440.0, 880.0]
"""
# Input validation
if any(f <= 0 for f in (freq, fmin, fmax)):
raise ValueError("Frequencies have to be positive")
# If freq is out of range, avoid range extension
while freq < fmin:
freq *= 2
while freq > fmax:
freq /= 2
if freq < fmin: # Gone back and forth
return []
# Finds the range for a valid input
return list(it.takewhile(lambda x: x > fmin,
(freq * 2 ** harm for harm in it.count(0, -1))
))[::-1] \
+ list(it.takewhile(lambda x: x < fmax,
(freq * 2 ** harm for harm in it.count(1))
))