Package jazzparser :: Package harmonical :: Module tones
[hide private]
[frames] | no frames]

Source Code for Module jazzparser.harmonical.tones

  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  # Get the logger from the logging system 
 48  logger = logging.getLogger("main_logger") 
 49   
50 -class ToneMatrix(object):
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 """
59 - def __init__(self, sample_rate=DEFAULT_SAMPLE_RATE):
60 self.sample_rate = sample_rate 61 self._events = {}
62
63 - def add_tone(self, time, event):
64 self._events.setdefault(int(time*self.sample_rate), []).append(event)
65
66 - def render(self):
67 """ 68 Renders the wave and returns a list of samples. 69 70 """ 71 samples = [] 72 def _add_samples(new_samples, offset): 73 # Pad out until we reach the start time 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 # We've not added any samples at this time yet 80 samples.append(new_samples[intime]) 81 else: 82 # This timestep already has a sample in it - just add 83 samples[cursor] += new_samples[intime]
84 for time in sorted(self._events.keys()): 85 # Get the samples for the given tones 86 for tone in self._events[time]: 87 tone_samples = tone.get_samples(self.sample_rate) 88 # Add them to the global matrix 89 _add_samples(tone_samples, time) 90 # This mustn't go over the max sample, so we normalize to 1.0 91 # If you want a different volume, renormalize afterwards 92 samples = normalize(samples, level=1.0) 93 return samples
94
95 -class BaseToneEvent(object):
96 """ 97 A single event in the tone matrix. This is an abstract class to 98 define the interface for tone events. 99 100 """
101 - def get_samples(self, sample_rate=DEFAULT_SAMPLE_RATE):
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
109 -class SineToneEvent(BaseToneEvent):
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
120 - def get_samples(self, sample_rate=DEFAULT_SAMPLE_RATE):
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 # Apply an envelope to shape the wave 127 wave = apply_envelope(wave, self.envelope) 128 return wave
129
130 -class MultiSineToneEvent(BaseToneEvent):
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=[]):
141 self.duration = duration 142 self.amplitude = amplitude 143 self.envelope = envelope 144 self.tones = tones
145
146 - def get_samples(self, sample_rate=DEFAULT_SAMPLE_RATE):
147 waves = [] 148 for frequency,amplitude in self.tones: 149 waves.append(generate_sine_wave(frequency, self.duration, amplitude, sample_rate)) 150 wave = sum_signals(waves, norm=self.amplitude) 151 # Apply an envelope to the final wave 152 if self.envelope is not None: 153 wave = apply_envelope(wave, self.envelope) 154 return wave
155
156 -class SineClusterEvent(MultiSineToneEvent):
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
201 -class SineChordEvent(SineClusterEvent):
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
222 -def generate_sine_wave(frequency, duration, amplitude, sample_rate):
223 samples = duration*sample_rate 224 period = sample_rate / float(frequency) # in sample points 225 omega = numpy.pi * 2 / period 226 227 # A x values to serve as input to the sin wave 228 xaxis = numpy.arange(samples, dtype = numpy.float) * omega 229 # Run this through the sin function to get a sine wave 230 wave = MAX_SAMPLE * numpy.sin(xaxis) * amplitude 231 return wave
232
233 -def apply_envelope(wave, envelope):
234 return [s * envelope[i*len(envelope)/len(wave)] for i,s in enumerate(wave)]
235
236 -def sum_signals(sigs, norm=1.0):
237 """ 238 Sum two wave signals. 239 240 """ 241 wave = normalize([sum(samps) for samps in zip(*sigs)], level=norm) 242 return wave
243
244 -def normalize(wave, level=0.8):
245 """ 246 Normalize the amplitude of the wave data. 247 248 """ 249 if len(wave) == 0: 250 return wave 251 # This should be the amplitude of the highest sample 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 ######################### Envelopes #############################
258 -def fade_in_out_envelope(precision=200, hold_ratio=10):
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)]
267 -def smooth_fade_in_out_envelope(precision=200, hold_ratio=10):
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
278 -def fade_in_envelope(precision=200, hold_ratio=10):
279 return [1.0*i/precision for i in range(precision)] + \ 280 [1.0]*(precision*hold_ratio)
281 -def fade_out_envelope(precision=200, hold_ratio=10):
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
297 -def piano_envelope():
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
311 -def no_envelope():
312 return None
313 # Dictionary of envelopes, so we can access them by name 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 # Use this envelope for all notes 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 # Work out when each root change occurs 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 # Move through root until we get the first one that 366 # occurs after the previous time 367 if rtime > time: 368 return current_root 369 current_root = root 370 # If we're beyond the time of the last root, use that one 371 return current_root
372 373 if chord_types is None: 374 # Default to just pure tones 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 # Build the tone matrix by adding the tones one by one 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 # We want all enharmonic equivs of I to come out close to I, 390 # not an octave above 391 if not equal_temperament and coordinate_to_et_2d(coord) == 0 \ 392 and pitch_ratio > 1.5: 393 pitch_ratio /= 2.0 394 # Use a sine tone for each note 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
400 -def render_path_to_file(filename, path, *args, **kwargs):
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 # Generate the audio samples 410 samples = tones.render() 411 # Output to a wave file 412 save_wave_data(samples, filename)
413