Skip to content

Commit

Permalink
Add vibrato
Browse files Browse the repository at this point in the history
  • Loading branch information
eriknyquist committed Jan 2, 2021
1 parent 9c88d83 commit d301463
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 21 deletions.
23 changes: 19 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ into usable musical data.

The Polyphonic Tone Text Transfer Language (PTTTL) is a way to describe polyphonic
melodies, and is a superset of Nokia's
`RTTTL <https://en.wikipedia.org/wiki/Ring_Tone_Transfer_Language>`_ format,
used for monophonic ringtones.
`RTTTL <https://en.wikipedia.org/wiki/Ring_Tone_Transfer_Language>`_ format, extending
it to enable polyphony and vibrato.


API documentation
Expand Down Expand Up @@ -95,16 +95,18 @@ contents.
*default values* section
========================

The very first statement is the *default value* section and is identical to
The very first statement is the *default value* section and is the same as
the section of the same name from the RTTTL format.

::

b=123, d=8, o=4
b=123, d=8, o=4, f=7, v=10

* *b* - beat, tempo: tempo in BPM (Beats Per Minute)
* *d* - duration: default duration of a note if none is specified
* *o* - octave: default octave of a note if none is specified
* *f* - frequency: default vibrato frequency if none is specified, in Hz
* *v* - variance: default vibrato variance from the main pitch if none is specified, in Hz

*data* section
==============
Expand Down Expand Up @@ -160,6 +162,19 @@ Octave

Valid values for note octave are between **0** and **8**.

Vibrato
-------

Optionally, vibrato maybe enabled and configured for an individual note. This is
done by adding a ``v`` at the end of the note, and optionally a frequency and variance
value seperated by a ``-`` character. For example:

* ``4c#v`` refers to a C# quarter note with vibrato enabled, using default settings
* ``4c#v10`` refers to a C# quarter note with vibrato enabled, using a vibrato frequency of 10Hz,
and the default value for vibrato variance
* ``4c#v10-15`` refers to a C# quarter note with vibrato enabled, using a vibrato frequency of 10Hz,
with a maximum vibrato variance of 15Hz from the main pitch.

Example
=======

Expand Down
4 changes: 3 additions & 1 deletion ptttl/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def _generate_samples(parsed, amplitude, wavetype):
if note.pitch <= 0.0:
mixer.add_silence(i, duration=note.duration)
else:
mixer.add_tone(i, frequency=note.pitch, duration=note.duration)
mixer.add_tone(i, frequency=note.pitch, duration=note.duration,
vibrato_frequency=note.vibrato_frequency,
vibrato_variance=note.vibrato_variance)

return mixer.mix()

Expand Down
140 changes: 124 additions & 16 deletions ptttl/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
"b": 493.883301256
}


DEFAULT_VIBRATO_FREQ_HZ = 7.0
DEFAULT_VIBRATO_VAR_HZ = 20.0
DEFAULT_OCTAVE = 4
DEFAULT_DURATION = 8
DEFAULT_BPM = 123


class PTTTLSyntaxError(Exception):
"""
Raised by PTTTLParser when ptttl data is malformed and cannot be parsed
Expand Down Expand Up @@ -58,14 +66,29 @@ def _invalid_note(note):
def _invalid_octave(note):
raise PTTTLValueError("invalid octave in note '%s'" % note)

def _invalid_vibrato(vdata):
raise PTTTLValueError("invalid vibrato settings: '%s'" % note)


def _int_setting(key, val):
ret = None

try:
ret = int(val)
except ValueError:
raise PTTTLValueError("expecting an integer for '%s' setting in "
"PTTTL script" % key)
"PTTTL script" % key)

return ret

def _float_setting(key, val):
ret = None

try:
ret = float(val)
except ValueError:
raise PTTTLValueError("expecting a floating point value for '%s' setting in "
"PTTTL script" % key)

return ret

Expand All @@ -79,15 +102,38 @@ class PTTTLNote(object):
:ivar float pitch: Note pitch in Hz
:ivar float duration: Note duration in seconds
:ivar float vfreq: Vibrato frequency in Hz
:ivar float vvar: Vibrato variance from main pitch in Hz
"""
def __init__(self, pitch, duration):
def __init__(self, pitch, duration, vfreq=None, vvar=None):
self.pitch = pitch
self.duration = duration
self.vibrato_frequency = vfreq
self.vibrato_variance = vvar

def has_vibrato(self):
"""
Returns True if vibrato frequency and variance are non-zero
:return: True if vibrato is non-zero
:rtype: bool
"""
if None in [self.vibrato_frequency, self.vibrato_variance]:
return False

if 0.0 in [self.vibrato_frequency, self.vibrato_variance]:
return False

return (self.vibrato_frequency > 0.0) and (self.vibrato_variance > 0.0)

def __str__(self):
return "%s(pitch=%.4f, duration=%.4f)" % (self.__class__.__name__,
self.pitch,
self.duration)
ret = "%s(pitch=%.4f, duration=%.4f" % (self.__class__.__name__,
self.pitch,
self.duration)
if self.has_vibrato():
ret += ", vibrato=%.1f:%.1f" % (self.vibrato_frequency, self.vibrato_variance)

return ret + ")"

def __repr__(self):
return self.__str__()
Expand All @@ -99,10 +145,22 @@ class PTTTLData(object):
May contain multiple tracks, where each track is a list of PTTTLNote objects.
:ivar [[PTTTLNote]] tracks: List of tracks. Each track is a list of PTTTLNote objects.
:ivar float bpm: playback speed in BPM (beats per minute).
:ivar int default_octave: Default octave to use when none is specified
:ivar int default_duration: Default note duration to use when none is specified
:ivar float default_vibrato_freq: Default vibrato frequency when none is specified, in Hz
:ivar float default_vibrato_var: Default vibrato variance when none is specified, in Hz
"""
def __init__(self):
def __init__(self, bpm=DEFAULT_BPM, default_octave=DEFAULT_OCTAVE,
default_duration=DEFAULT_DURATION, default_vibrato_freq=DEFAULT_VIBRATO_FREQ_HZ,
default_vibrato_var=DEFAULT_VIBRATO_VAR_HZ):
self.bpm = bpm
self.default_octave = default_octave
self.default_duration = default_duration
self.default_vibrato_freq = default_vibrato_freq
self.default_vibrato_var = default_vibrato_var
self.tracks = []

def add_track(self, notes):
self.tracks.append(notes)

Expand Down Expand Up @@ -149,6 +207,12 @@ def _parse_config_line(self, conf):
if not values:
raise PTTTLSyntaxError("no valid configuration found in PTTTL script")

bpm = None
default = None
octave = None
vfreq = None
vvar = None

for value in values:
fields = value.split('=')
if len(fields) != 2:
Expand All @@ -170,6 +234,10 @@ def _parse_config_line(self, conf):
octave = _int_setting(key, val)
if not self._is_valid_octave(octave):
_invalid_value(key, val)
elif key == 'f':
vfreq = _float_setting(key, val)
elif key == 'v':
vvar = _float_setting(key, val)
else:
_unrecognised_setting(key)

Expand All @@ -182,23 +250,32 @@ def _parse_config_line(self, conf):
if not default:
_missing_setting('d')

return bpm, default, octave
return bpm, default, octave, vfreq, vvar

def _note_time_to_secs(self, note_time, bpm):
# Time in seconds for a whole note (4 beats) given current BPM.
whole = (60.0 / float(bpm)) * 4.0

return whole / float(note_time)

def _parse_note(self, string, bpm, default, octave):
def _parse_note(self, string, bpm, default, octave, vfreq, vvar):
i = 0
orig = string
sawdot = False
dur = default
vibrato_freq = None
vibrato_var = None
vdata = None

fields = string.split('v')
if len(fields) == 2:
string = fields[0]
vdata = fields[1]

if len(string) == 0:
raise PTTTLSyntaxError("Missing notes after comma")

# Get the note duration; if there is one, it should be the first thing
while i < len(string) and string[i].isdigit():
if i > 1:
_invalid_note_duration(orig)
Expand All @@ -214,8 +291,10 @@ def _parse_note(self, string, bpm, default, octave):
if not string[0].isalpha():
_invalid_note(orig)

# Calculate note duration in real seconds
duration = self._note_time_to_secs(dur, bpm)

# Now, look for the musical note value, which should be next
string = string[i:]

i = 0
Expand All @@ -234,8 +313,8 @@ def _parse_note(self, string, bpm, default, octave):
i = 0

if note == 'p':
# This note is a rest
pitch = -1

else:
if note not in NOTES:
_invalid_note(orig)
Expand All @@ -261,13 +340,37 @@ def _parse_note(self, string, bpm, default, octave):
else:
pitch = raw_pitch

string = string[i:].strip()
i = 0

if sawdot or ((i < len(string)) and string[-1] == '.'):
duration += (duration / 2.0)

return PTTTLNote(pitch, duration)
if vdata is not None:
if vdata.strip() == '':
vibrato_freq = vfreq
vibrato_var = vvar
else:
fields = vdata.split('-')
if len(fields) == 2:
try:
vibrato_freq = float(fields[0])
vibrato_var = float(fields[1])
except:
_invalid_vibrato(vdata)

elif len(fields) == 1:
try:
vibrato_freq = float(vdata)
except:
_invalid_vibrato(vdata)

def _parse_notes(self, track_list, bpm, default, octave):
ret = PTTTLData()
vibrato_var = vvar

return PTTTLNote(pitch, duration, vibrato_freq, vibrato_var)

def _parse_notes(self, track_list, bpm, default, octave, vfreq, vvar):
ret = PTTTLData(bpm, octave, default, vfreq, vvar)

for track in track_list:
if track.strip() == "":
Expand All @@ -276,7 +379,7 @@ def _parse_notes(self, track_list, bpm, default, octave):
buf = []
fields = track.split(',')
for note in fields:
note = self._parse_note(note.strip(), bpm, default, octave)
note = self._parse_note(note.strip(), bpm, default, octave, vfreq, vvar)
buf.append(note)

ret.add_track(buf)
Expand All @@ -299,7 +402,7 @@ def parse(self, ptttl_string):
raise PTTTLSyntaxError('expecting 3 colon-seperated fields')

self.name = fields[0].strip()
bpm, default, octave = self._parse_config_line(fields[1])
bpm, default, octave, vfreq, vvar = self._parse_config_line(fields[1])

numtracks = -1
blocks = fields[2].split(';')
Expand All @@ -322,4 +425,9 @@ def parse(self, ptttl_string):
if i < (len(trackdata) - 1):
tracks[j] += ","

return self._parse_notes(tracks, bpm, default, octave)
if vfreq is None:
vfreq = DEFAULT_VIBRATO_FREQ_HZ
if vvar is None:
vvar = DEFAULT_VIBRATO_VAR_HZ

return self._parse_notes(tracks, bpm, default, octave, vfreq, vvar)

0 comments on commit d301463

Please sign in to comment.