Skip to content

Commit d301463

Browse files
committed
Add vibrato
1 parent 9c88d83 commit d301463

File tree

3 files changed

+146
-21
lines changed

3 files changed

+146
-21
lines changed

README.rst

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ into usable musical data.
99

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

1515

1616
API documentation
@@ -95,16 +95,18 @@ contents.
9595
*default values* section
9696
========================
9797

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

101101
::
102102

103-
b=123, d=8, o=4
103+
b=123, d=8, o=4, f=7, v=10
104104

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

109111
*data* section
110112
==============
@@ -160,6 +162,19 @@ Octave
160162

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

165+
Vibrato
166+
-------
167+
168+
Optionally, vibrato maybe enabled and configured for an individual note. This is
169+
done by adding a ``v`` at the end of the note, and optionally a frequency and variance
170+
value seperated by a ``-`` character. For example:
171+
172+
* ``4c#v`` refers to a C# quarter note with vibrato enabled, using default settings
173+
* ``4c#v10`` refers to a C# quarter note with vibrato enabled, using a vibrato frequency of 10Hz,
174+
and the default value for vibrato variance
175+
* ``4c#v10-15`` refers to a C# quarter note with vibrato enabled, using a vibrato frequency of 10Hz,
176+
with a maximum vibrato variance of 15Hz from the main pitch.
177+
163178
Example
164179
=======
165180

ptttl/audio.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ def _generate_samples(parsed, amplitude, wavetype):
4242
if note.pitch <= 0.0:
4343
mixer.add_silence(i, duration=note.duration)
4444
else:
45-
mixer.add_tone(i, frequency=note.pitch, duration=note.duration)
45+
mixer.add_tone(i, frequency=note.pitch, duration=note.duration,
46+
vibrato_frequency=note.vibrato_frequency,
47+
vibrato_variance=note.vibrato_variance)
4648

4749
return mixer.mix()
4850

ptttl/parser.py

Lines changed: 124 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
"b": 493.883301256
2323
}
2424

25+
26+
DEFAULT_VIBRATO_FREQ_HZ = 7.0
27+
DEFAULT_VIBRATO_VAR_HZ = 20.0
28+
DEFAULT_OCTAVE = 4
29+
DEFAULT_DURATION = 8
30+
DEFAULT_BPM = 123
31+
32+
2533
class PTTTLSyntaxError(Exception):
2634
"""
2735
Raised by PTTTLParser when ptttl data is malformed and cannot be parsed
@@ -58,14 +66,29 @@ def _invalid_note(note):
5866
def _invalid_octave(note):
5967
raise PTTTLValueError("invalid octave in note '%s'" % note)
6068

69+
def _invalid_vibrato(vdata):
70+
raise PTTTLValueError("invalid vibrato settings: '%s'" % note)
71+
72+
6173
def _int_setting(key, val):
6274
ret = None
6375

6476
try:
6577
ret = int(val)
6678
except ValueError:
6779
raise PTTTLValueError("expecting an integer for '%s' setting in "
68-
"PTTTL script" % key)
80+
"PTTTL script" % key)
81+
82+
return ret
83+
84+
def _float_setting(key, val):
85+
ret = None
86+
87+
try:
88+
ret = float(val)
89+
except ValueError:
90+
raise PTTTLValueError("expecting a floating point value for '%s' setting in "
91+
"PTTTL script" % key)
6992

7093
return ret
7194

@@ -79,15 +102,38 @@ class PTTTLNote(object):
79102
80103
:ivar float pitch: Note pitch in Hz
81104
:ivar float duration: Note duration in seconds
105+
:ivar float vfreq: Vibrato frequency in Hz
106+
:ivar float vvar: Vibrato variance from main pitch in Hz
82107
"""
83-
def __init__(self, pitch, duration):
108+
def __init__(self, pitch, duration, vfreq=None, vvar=None):
84109
self.pitch = pitch
85110
self.duration = duration
111+
self.vibrato_frequency = vfreq
112+
self.vibrato_variance = vvar
113+
114+
def has_vibrato(self):
115+
"""
116+
Returns True if vibrato frequency and variance are non-zero
117+
118+
:return: True if vibrato is non-zero
119+
:rtype: bool
120+
"""
121+
if None in [self.vibrato_frequency, self.vibrato_variance]:
122+
return False
123+
124+
if 0.0 in [self.vibrato_frequency, self.vibrato_variance]:
125+
return False
126+
127+
return (self.vibrato_frequency > 0.0) and (self.vibrato_variance > 0.0)
86128

87129
def __str__(self):
88-
return "%s(pitch=%.4f, duration=%.4f)" % (self.__class__.__name__,
89-
self.pitch,
90-
self.duration)
130+
ret = "%s(pitch=%.4f, duration=%.4f" % (self.__class__.__name__,
131+
self.pitch,
132+
self.duration)
133+
if self.has_vibrato():
134+
ret += ", vibrato=%.1f:%.1f" % (self.vibrato_frequency, self.vibrato_variance)
135+
136+
return ret + ")"
91137

92138
def __repr__(self):
93139
return self.__str__()
@@ -99,10 +145,22 @@ class PTTTLData(object):
99145
May contain multiple tracks, where each track is a list of PTTTLNote objects.
100146
101147
:ivar [[PTTTLNote]] tracks: List of tracks. Each track is a list of PTTTLNote objects.
148+
:ivar float bpm: playback speed in BPM (beats per minute).
149+
:ivar int default_octave: Default octave to use when none is specified
150+
:ivar int default_duration: Default note duration to use when none is specified
151+
:ivar float default_vibrato_freq: Default vibrato frequency when none is specified, in Hz
152+
:ivar float default_vibrato_var: Default vibrato variance when none is specified, in Hz
102153
"""
103-
def __init__(self):
154+
def __init__(self, bpm=DEFAULT_BPM, default_octave=DEFAULT_OCTAVE,
155+
default_duration=DEFAULT_DURATION, default_vibrato_freq=DEFAULT_VIBRATO_FREQ_HZ,
156+
default_vibrato_var=DEFAULT_VIBRATO_VAR_HZ):
157+
self.bpm = bpm
158+
self.default_octave = default_octave
159+
self.default_duration = default_duration
160+
self.default_vibrato_freq = default_vibrato_freq
161+
self.default_vibrato_var = default_vibrato_var
104162
self.tracks = []
105-
163+
106164
def add_track(self, notes):
107165
self.tracks.append(notes)
108166

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

210+
bpm = None
211+
default = None
212+
octave = None
213+
vfreq = None
214+
vvar = None
215+
152216
for value in values:
153217
fields = value.split('=')
154218
if len(fields) != 2:
@@ -170,6 +234,10 @@ def _parse_config_line(self, conf):
170234
octave = _int_setting(key, val)
171235
if not self._is_valid_octave(octave):
172236
_invalid_value(key, val)
237+
elif key == 'f':
238+
vfreq = _float_setting(key, val)
239+
elif key == 'v':
240+
vvar = _float_setting(key, val)
173241
else:
174242
_unrecognised_setting(key)
175243

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

185-
return bpm, default, octave
253+
return bpm, default, octave, vfreq, vvar
186254

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

191259
return whole / float(note_time)
192260

193-
def _parse_note(self, string, bpm, default, octave):
261+
def _parse_note(self, string, bpm, default, octave, vfreq, vvar):
194262
i = 0
195263
orig = string
196264
sawdot = False
197265
dur = default
266+
vibrato_freq = None
267+
vibrato_var = None
268+
vdata = None
269+
270+
fields = string.split('v')
271+
if len(fields) == 2:
272+
string = fields[0]
273+
vdata = fields[1]
198274

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

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

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

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

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

236315
if note == 'p':
316+
# This note is a rest
237317
pitch = -1
238-
239318
else:
240319
if note not in NOTES:
241320
_invalid_note(orig)
@@ -261,13 +340,37 @@ def _parse_note(self, string, bpm, default, octave):
261340
else:
262341
pitch = raw_pitch
263342

343+
string = string[i:].strip()
344+
i = 0
345+
264346
if sawdot or ((i < len(string)) and string[-1] == '.'):
265347
duration += (duration / 2.0)
266348

267-
return PTTTLNote(pitch, duration)
349+
if vdata is not None:
350+
if vdata.strip() == '':
351+
vibrato_freq = vfreq
352+
vibrato_var = vvar
353+
else:
354+
fields = vdata.split('-')
355+
if len(fields) == 2:
356+
try:
357+
vibrato_freq = float(fields[0])
358+
vibrato_var = float(fields[1])
359+
except:
360+
_invalid_vibrato(vdata)
361+
362+
elif len(fields) == 1:
363+
try:
364+
vibrato_freq = float(vdata)
365+
except:
366+
_invalid_vibrato(vdata)
268367

269-
def _parse_notes(self, track_list, bpm, default, octave):
270-
ret = PTTTLData()
368+
vibrato_var = vvar
369+
370+
return PTTTLNote(pitch, duration, vibrato_freq, vibrato_var)
371+
372+
def _parse_notes(self, track_list, bpm, default, octave, vfreq, vvar):
373+
ret = PTTTLData(bpm, octave, default, vfreq, vvar)
271374

272375
for track in track_list:
273376
if track.strip() == "":
@@ -276,7 +379,7 @@ def _parse_notes(self, track_list, bpm, default, octave):
276379
buf = []
277380
fields = track.split(',')
278381
for note in fields:
279-
note = self._parse_note(note.strip(), bpm, default, octave)
382+
note = self._parse_note(note.strip(), bpm, default, octave, vfreq, vvar)
280383
buf.append(note)
281384

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

301404
self.name = fields[0].strip()
302-
bpm, default, octave = self._parse_config_line(fields[1])
405+
bpm, default, octave, vfreq, vvar = self._parse_config_line(fields[1])
303406

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

325-
return self._parse_notes(tracks, bpm, default, octave)
428+
if vfreq is None:
429+
vfreq = DEFAULT_VIBRATO_FREQ_HZ
430+
if vvar is None:
431+
vvar = DEFAULT_VIBRATO_VAR_HZ
432+
433+
return self._parse_notes(tracks, bpm, default, octave, vfreq, vvar)

0 commit comments

Comments
 (0)