+#!/usr/bin/env python3
+#
+# Music - an utility class to deal with musical details
+#
+# Copyright (C) 2016 Antonio Ospite <ao2@ao2.it>
+#
+# This program 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, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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/>.
+
+# The 88 piano keyboard goes from A0 (-39) to C8 (+48), see:
+# https://en.wikipedia.org/wiki/Piano_key_frequencies
+PIANO_88_KEYS_RANGE = range(-39, 48 + 1)
+
+
+class SpnNote(object):
+ def __init__(self, note_number):
+ # Scientific Pitch Notation values are from -48 to +83
+ # https://en.wikipedia.org/wiki/Scientific_pitch_notation
+ if note_number < -48 or note_number > 83:
+ raise ValueError("Invalid Scientific Pitch Notation number")
+
+ self.note_number = note_number
+ self.name = self._name()
+ self.frequency = self._frequency()
+
+ def _name(self):
+ note_names = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"]
+
+ note_offset = self.note_number % 12
+ name = note_names[note_offset]
+ octave = (self.note_number + 48 - note_offset) // 12
+ note_name = "%s%d" % (name, octave)
+ return note_name
+
+ def _frequency(self):
+ # https://en.wikipedia.org/wiki/A440_(pitch_standard)
+ return round(440 * pow(2, (self.note_number - 9) / 12), 4)
+
+ def __repr__(self):
+ return "(%+3d): %-3s frequency: %9.4f" % (self.note_number,
+ self.name,
+ self.frequency)
+
+
+class MidiNote(SpnNote):
+ def __init__(self, note_number):
+ # midi notes go from 0 to 127
+ if note_number < 0 or note_number > 127:
+ raise ValueError("Invalid midi note")
+
+ # In Scientific Pitch Notation C4 is 0
+ # In MIDI it's 60
+ SpnNote.__init__(self, note_number - 60)
+
+
+def test():
+ A0 = SpnNote(-39)
+ print("Note A0? %s" % A0)
+ del A0
+
+ C4 = SpnNote(0)
+ print("Note C4? %s" % C4)
+ del C4
+
+ A4 = SpnNote(9)
+ print("Note A4? %s" % A4)
+ del A4
+
+ C8 = SpnNote(+48)
+ print("Note C8? %s" % C8)
+ del C8
+
+ print()
+
+ midi_C4 = MidiNote(60)
+ print("Midi C4? %s" % midi_C4)
+ del midi_C4
+
+
+
+if __name__ == "__main__":
+ test()