1 """Tone generation tools.
2
3 This is the tone representation and rendering engine for the Harmonical.
4 The Harmonical is an instrument that can play music in any tuning
5 system, including just intonation, by allowing the music to specify
6 a precise pitch for each note.
7 Its name is that given by Helmholtz to his specially tuned harmonium
8 that allowed him to experiment with just tuning systems.
9
10 """
11 """
12 ============================== License ========================================
13 Copyright (C) 2008, 2010-12 University of Edinburgh, Mark Granroth-Wilding
14
15 This file is part of The Jazz Parser.
16
17 The Jazz Parser is free software: you can redistribute it and/or modify
18 it under the terms of the GNU General Public License as published by
19 the Free Software Foundation, either version 3 of the License, or
20 (at your option) any later version.
21
22 The Jazz Parser is distributed in the hope that it will be useful,
23 but WITHOUT ANY WARRANTY; without even the implied warranty of
24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 GNU General Public License for more details.
26
27 You should have received a copy of the GNU General Public License
28 along with The Jazz Parser. If not, see <http://www.gnu.org/licenses/>.
29
30 ============================ End license ======================================
31
32 """
33 __author__ = "Mark Granroth-Wilding <mark.granroth-wilding@ed.ac.uk>"
34
35 import numpy
36 import math, logging
37 from .files import DEFAULT_SAMPLE_RATE, save_wave_data
38 from . import CHORD_TYPES
39 from jazzparser.data import Fraction
40 from jazzparser.utils.base import group_pairs
41 from jazzparser.utils.tonalspace import coordinate_to_et, \
42 coordinate_to_et_2d, tonal_space_pitch, tonal_space_pitch_2d, \
43 tonal_space_et_pitch
44
45 MAX_SAMPLE = 32767
46
47
48 logger = logging.getLogger("main_logger")
49
51 """
52 A timesheet of tones, with pitches, onset times and durations,
53 designed for specifying input to the tone generator. This allows
54 music to be generated using precise pitches, rather than equal
55 temperament note values, as with MIDI or something similar.
56 Time values are discretized according to the sample rate.
57
58 """
60 self.sample_rate = sample_rate
61 self._events = {}
62
64 self._events.setdefault(int(time*self.sample_rate), []).append(event)
65
67 """
68 Renders the wave and returns a list of samples.
69
70 """
71 samples = []
72 def _add_samples(new_samples, offset):
73
74 while len(samples) < offset:
75 samples.append(0.0)
76 for intime in range(len(new_samples)):
77 cursor = offset + intime
78 if len(samples) <= cursor:
79
80 samples.append(new_samples[intime])
81 else:
82
83 samples[cursor] += new_samples[intime]
84 for time in sorted(self._events.keys()):
85
86 for tone in self._events[time]:
87 tone_samples = tone.get_samples(self.sample_rate)
88
89 _add_samples(tone_samples, time)
90
91
92 samples = normalize(samples, level=1.0)
93 return samples
94
96 """
97 A single event in the tone matrix. This is an abstract class to
98 define the interface for tone events.
99
100 """
102 """
103 This should return a list of samples for the event's whole
104 duration at the given sample rate.
105
106 """
107 raise NotImplementedError
108
110 """
111 A single event in the tone matrix.
112
113 """
114 - def __init__(self, frequency, duration=1, amplitude=0.8, envelope=None):
115 self.frequency = frequency
116 self.duration = duration
117 self.amplitude = amplitude
118 self.envelope = envelope
119
121 """
122 Generates samples from a sine wave.
123 """
124 wave = generate_sine_wave(self.frequency, self.duration, self.amplitude, sample_rate)
125 if self.envelope is not None:
126
127 wave = apply_envelope(wave, self.envelope)
128 return wave
129
131 """
132 Generates a tone by summing several sine waves (simple additive
133 synthesis).
134 The tones are given as a list of (frequency,amplitude) pairs.
135
136 The result is normalized to the given amplitude, so absolute
137 scaling of the individual amplitudes makes no difference.
138
139 """
140 - def __init__(self, duration=1, amplitude=0.8, envelope=None, tones=[]):
145
155
157 """
158 Generates a tone by summing the notes of a tonal space cluster.
159
160 """
161 - def __init__(self, frequency, points, duration=1, \
162 amplitude=0.8, envelope=None, root_weight=1.2, root_octave=0, \
163 double_root=False, equal_temperament=False):
164 """
165 @type root_weight: float
166 @param root_weight: amplitude ratio between the root note and
167 any other note. Set >1.0 to make the root louder than
168 other notes.
169 @type root_octave: int
170 @param root_octave: octave to transpose the root to relative
171 to other notes. Default (0) has the other notes in the
172 octave above the root.
173 @type double_root: bool
174 @param double_root: if True, an extra tone will be added an
175 octave below the root
176
177 @see: L{MultiSineToneEvent}
178
179 """
180 tones = []
181
182 if equal_temperament:
183 _pitch_ratio = tonal_space_et_pitch
184 else:
185 _pitch_ratio = tonal_space_pitch
186
187 for x,y,z in points:
188 if x==0 and y==0:
189 tones.append((frequency*(2**(z+root_octave)), 1.0))
190 if double_root:
191 tones.append((frequency*(2**(z+root_octave-1)), 1.0/8.0))
192 else:
193 tones.append(
194 (frequency*_pitch_ratio((x,y,z)),
195 1.0/root_weight))
196 super(SineClusterEvent, self).__init__(duration=duration,
197 amplitude=amplitude,
198 envelope=envelope,
199 tones=tones)
200
202 """
203 Generates a tone by summing the notes of a chord of a standard
204 type.
205
206 """
207
208 - def __init__(self, frequency, chord_type='', *args, **kwargs):
209 """
210 @see: L{SineClusterEvent}
211
212 """
213 if chord_type in CHORD_TYPES:
214 ts_notes = CHORD_TYPES[chord_type]
215 else:
216 logger.warn("harmonical could not find realisation for chord "\
217 "type '%s'. Using plain major instead." % chord_type)
218 ts_notes = CHORD_TYPES['']
219
220 super(SineChordEvent, self).__init__(frequency, ts_notes, *args, **kwargs)
221
223 samples = duration*sample_rate
224 period = sample_rate / float(frequency)
225 omega = numpy.pi * 2 / period
226
227
228 xaxis = numpy.arange(samples, dtype = numpy.float) * omega
229
230 wave = MAX_SAMPLE * numpy.sin(xaxis) * amplitude
231 return wave
232
234 return [s * envelope[i*len(envelope)/len(wave)] for i,s in enumerate(wave)]
235
237 """
238 Sum two wave signals.
239
240 """
241 wave = normalize([sum(samps) for samps in zip(*sigs)], level=norm)
242 return wave
243
245 """
246 Normalize the amplitude of the wave data.
247
248 """
249 if len(wave) == 0:
250 return wave
251
252 targ_max = level * MAX_SAMPLE
253 current_max = max(max(wave), -1*min(wave))
254 wave = [s*targ_max/current_max for s in wave]
255 return wave
256
257
259 """
260 Generates an envelope that fades in linearly, holds for a time
261 adjusted by hold_ratio, then fades out linearly.
262
263 """
264 return [1.0*i/precision for i in range(precision)] + \
265 [1.0]*(precision*hold_ratio) + \
266 [1.0*(precision-i)/precision for i in range(precision)]
268 """
269 Generates an envelope that fades in with a log curve, holds for a time
270 adjusted by hold_ratio, then fades out similarly.
271
272 """
273 sq_prec = precision**2
274 return [1.0*(i+1)**2/sq_prec for i in range(precision)] + \
275 [1.0]*(precision*hold_ratio) + \
276 [1.0*(1.0-float(i+1)**2/sq_prec) for i in range(precision)]
277
279 return [1.0*i/precision for i in range(precision)] + \
280 [1.0]*(precision*hold_ratio)
282 return [1.0]*(precision*hold_ratio) + \
283 [1.0*(precision-i)/precision for i in range(precision)]
284 -def adsr_envelope(attack_time, decay_time, sustain_time, release_time, sustain_level=0.6, sustain_level_end=0.4, pause_time=0):
285 """
286 Create an envelope according to ADSR (attack-decay-sustain-release)
287 specifications. Unlike real ADSR, the timings are all proportional.
288 Attack, decay and release should be absolute, and sustain shouldn't
289 have a time, but this kind of envelope can't do that.
290
291 """
292 return [1.0*i/attack_time for i in range(attack_time)] + \
293 [1.0-(1.0-sustain_level)*i/decay_time for i in range(decay_time)] + \
294 [sustain_level-(sustain_level-sustain_level_end)*i/sustain_time for i in range(sustain_time)] + \
295 [sustain_level_end*(1-float(i)/release_time) for i in range(release_time)] + \
296 [0.0] * pause_time
298 """
299 Produces an envelope designed to sound a tiny bit like a piano.
300 Don't expect too much of it!
301
302 """
303 attack = 50
304 decay = 200
305 sustain = 5000
306 release = 400
307 sus_level = 0.4
308 sus_end_level = 0.2
309 return adsr_envelope(attack, decay, sustain, release, sus_level, sus_end_level, pause_time=50)
310
313
314 ENVELOPES = {
315 'piano' : piano_envelope,
316 'in' : fade_in_envelope,
317 'out' : fade_out_envelope,
318 'inout' : fade_in_out_envelope,
319 'smooth' : smooth_fade_in_out_envelope,
320 'square' : no_envelope,
321 }
322
323
324
325 -def path_to_tones(path, tempo=120, chord_types=None, root_octave=0,
326 double_root=False, equal_temperament=False, timings=False):
327 """
328 Takes a tonal space path, given as a list of coordinates, and
329 generates the tones of the roots.
330
331 @type path: list of (3d-coordinate,length) tuples
332 @param path: coordinates of the points in the sequence and the length
333 of each, in beats
334 @type tempo: int
335 @param tempo: speed in beats per second (Maelzel's metronome)
336 @type chord_types: list of (string,length)
337 @param chord_types: the type of chord to use for each tone and the
338 time spent on that chord type, in beats. See
339 L{CHORD_TYPES} keys for possible values.
340 @type equal_temperament: bool
341 @param equal_temperament: render all the pitches as they would be
342 played in equal temperament.
343 @rtype: L{ToneMatrix}
344 @return: a tone matrix that can be used to render the sound
345
346 """
347
348 envelope = piano_envelope()
349
350 sample_rate = DEFAULT_SAMPLE_RATE
351
352 beat_length = 60.0 / tempo
353 if timings:
354 root_times = path
355 else:
356
357 time = Fraction(0)
358 root_times = []
359 for root,length in path:
360 root_times.append((root,time))
361 time += length
362 def _root_at_time(time):
363 current_root = root_times[0][0]
364 for root,rtime in root_times[1:]:
365
366
367 if rtime > time:
368 return current_root
369 current_root = root
370
371 return current_root
372
373 if chord_types is None:
374
375 chord_types = [('prime',length) for __,length in path]
376
377 if equal_temperament:
378 _pitch_ratio = tonal_space_et_pitch
379 else:
380 _pitch_ratio = tonal_space_pitch_2d
381
382
383 matrix = ToneMatrix(sample_rate=sample_rate)
384 time = Fraction(0)
385 for ctype,length in chord_types:
386 coord = _root_at_time(time)
387 pitch_ratio = _pitch_ratio(coord)
388 duration = beat_length * float(length)
389
390
391 if not equal_temperament and coordinate_to_et_2d(coord) == 0 \
392 and pitch_ratio > 1.5:
393 pitch_ratio /= 2.0
394
395 tone = SineChordEvent(220*pitch_ratio, chord_type=ctype, duration=duration, envelope=envelope, root_octave=root_octave, root_weight=1.2, double_root=double_root)
396 matrix.add_tone(beat_length * float(time), tone)
397 time += length
398 return matrix
399
401 """
402 Convenience function that takes a path and set of timings and
403 uses the harmonical to render audio from it and writes it to a
404 file.
405 Additional args/kwargs are passed to the L{path_to_tones} function.
406
407 """
408 tones = path_to_tones(path, *args, **kwargs)
409
410 samples = tones.render()
411
412 save_wave_data(samples, filename)
413