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

Source Code for Module jazzparser.harmonical.input

  1  from __future__ import absolute_import 
  2  """Harmonical input file processing. 
  3   
  4  Simple input reading to give tone specifications directly to the  
  5  harmonical. 
  6   
  7  """ 
  8  """ 
  9  ============================== License ======================================== 
 10   Copyright (C) 2008, 2010-12 University of Edinburgh, Mark Granroth-Wilding 
 11    
 12   This file is part of The Jazz Parser. 
 13    
 14   The Jazz Parser is free software: you can redistribute it and/or modify 
 15   it under the terms of the GNU General Public License as published by 
 16   the Free Software Foundation, either version 3 of the License, or 
 17   (at your option) any later version. 
 18    
 19   The Jazz Parser is distributed in the hope that it will be useful, 
 20   but WITHOUT ANY WARRANTY; without even the implied warranty of 
 21   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 22   GNU General Public License for more details. 
 23    
 24   You should have received a copy of the GNU General Public License 
 25   along with The Jazz Parser.  If not, see <http://www.gnu.org/licenses/>. 
 26   
 27  ============================ End license ====================================== 
 28   
 29  """ 
 30  __author__ = "Mark Granroth-Wilding <mark.granroth-wilding@ed.ac.uk>"  
 31   
 32  from jazzparser.utils.base import abstractmethod 
 33  from .tones import ToneMatrix, SineChordEvent, SineClusterEvent, ENVELOPES 
 34  from .midi import tonal_space_note_events 
 35  from .midi.chords import events_for_chord 
 36  from jazzparser.utils.tonalspace import tonal_space_et_pitch, tonal_space_pitch 
 37  from jazzparser.data import Fraction 
 38  from midi import write_midifile, EventStream, SetTempoEvent, \ 
 39                      ProgramChangeEvent, NoteOnEvent, NoteOffEvent 
40 41 -class HarmonicalInputFile(object):
42 """ 43 Base class for input files to the harmonical. 44 45 """ 46 @staticmethod
47 - def from_file(filename, formats=None):
48 """ 49 Read in an input file. This method detects the type of input 50 and returns an instance of the appropriate subclass. 51 52 @type formats: list of strings 53 @param formats: list of formats to allow. By default, accepts any 54 format. If a list is given and the file's format isn't in it, 55 raises an error. 56 57 """ 58 infile = open(filename, 'r') 59 lines = infile.readlines() 60 # Remove all comments from the lines 61 lines = [line.partition("#")[0].strip() for line in lines] 62 # Remove empty lines 63 lines = [line for line in lines if len(line) > 0] 64 # Make sure there's some content - a type line and at least one more 65 if len(lines) < 2: 66 raise HarmonicalInputFileReadError, "empty file" 67 # Read any global directives and don't pass them through to the subclass 68 dirlines = [line.lstrip("@") for line in lines if line.startswith("@")] 69 directives = {} 70 for dirline in dirlines: 71 name,__,value = dirline.partition(":") 72 value = value.strip() 73 directives[name] = value 74 75 # Check the @format directive is given 76 if 'format' not in directives: 77 raise HarmonicalInputFileReadError, "no format descriptor "\ 78 "found in the input file." 79 format = directives.pop('format') 80 # Make sure this is a format we know about 81 if format not in INPUT_TYPES: 82 raise HarmonicalInputFileReadError, "invalid harmonic "\ 83 "input format: '%s'. Should be one of: %s" % \ 84 (format, ", ".join(INPUT_TYPES.keys())) 85 if formats is not None and format not in formats: 86 raise HarmonicalInputFileReadError, "harmonical input format "\ 87 "%s not allowed here. You can only use %s" % (format, 88 ", ".join(formats)) 89 90 lines = [line for line in lines if not line.startswith("@")] 91 # First process the directives 92 directives = INPUT_TYPES[format].process_global_directives(directives) 93 # Use the subclass' loader to get an appropriate instance 94 inst = INPUT_TYPES[format].from_data(lines, directives) 95 return inst
96 97 @staticmethod
99 output = {} 100 unused = data.keys() 101 if "tempo" in data: 102 output["tempo"] = int(data["tempo"]) 103 unused.remove("tempo") 104 if len(unused) != 0: 105 raise HarmonicalInputFileReadError, "unknown global " \ 106 "directive '%s'" % unused[0] 107 return output
108 109 @staticmethod 110 @abstractmethod
111 - def from_data(data, global_directives):
112 """ 113 Subclasses must provide this method to create an instance from 114 the lines of data read in from a file. 115 Should take a second argument which will be a dictionary of 116 the global directive values. 117 118 """ 119 pass
120 121 @abstractmethod
122 - def render(self):
123 """ 124 Subclasses must provide a way of building an audio signal. 125 126 """ 127 pass
128 129 @abstractmethod
130 - def render_midi(self):
131 """ 132 Subclasses may provide a way of building a tuned midi file. 133 134 @raise NotImplementedError: if this input format doesn't support 135 midi generation. 136 137 """ 138 raise NotImplementedError, "input format does not support midi file "\ 139 "generation"
140
141 142 ############ Utility functions for file reading ########## 143 -def _get_duration(tokens):
144 """Get a 'for' duration specification out of a line's tokens. 145 Returns duration in quarter notes.""" 146 duration = 1 147 if "for" in tokens: 148 forpos = tokens.index("for") 149 try: 150 duration = float(Fraction(tokens.pop(forpos+1))) 151 except: 152 raise HarmonicalInputFileReadError, "'for' "\ 153 "must be followed by an integer number "\ 154 "of beats." 155 tokens.pop(forpos) 156 return duration
157 -def _qn_to_seconds(duration):
158 """ 159 Converts from quarter note duration to number of seconds. 160 """ 161 return float(duration) * 60 / directives['tempo']
162
163 -def _get_root(tokens):
164 """Get a 'at' root specification out of a line's tokens.""" 165 if "at" in tokens: 166 atpos = tokens.index("at") 167 try: 168 root = _read_coord(tokens.pop(atpos+1)) 169 except: 170 raise HarmonicalInputFileReadError, "'at' "\ 171 "must be followed by an integer number "\ 172 "of beats." 173 tokens.pop(atpos) 174 else: 175 root = (0,0,0) 176 return root
177
178 -def _read_coord(token):
179 """Parse a 3D coordinate string.""" 180 if not token.startswith("(") or not token.endswith(")"): 181 raise HarmonicalInputFileReadError, \ 182 "invalid coordinate: %s" % token 183 parts = token[1:-1].split(",") 184 if len(parts) != 3: 185 raise HarmonicalInputFileReadError, \ 186 "coordinate must be 3D: %s" % token 187 return int(parts[0]), int(parts[1]), int(parts[2])
188
189 -def _get_pitch(point):
190 """Get the pitch of a point in the space.""" 191 if state['equal_temperament']: 192 return state['origin'] * tonal_space_et_pitch(point) 193 else: 194 return state['origin'] * tonal_space_pitch(point)
195
196 -def _get_volume(tokens):
197 """Reads a volume specifier.""" 198 if "vol" in tokens: 199 atpos = tokens.index("vol") 200 try: 201 volume = int(tokens.pop(atpos+1)) 202 except ValueError: 203 raise HarmonicalInputFileReadError, "'vol' "\ 204 "must be followed by an integer volume "\ 205 "between 0 and 100." 206 if volume < 0 or volume > 100: 207 raise HarmonicalInputFileReadError, "volume "\ 208 "value must be between 0 and 100, not %s" % volume 209 tokens.pop(atpos) 210 volume = float(volume)/100 211 else: 212 volume = 0.8 213 return volume
214
215 ############################################### 216 217 218 -class ChordInputFile(HarmonicalInputFile):
219 """ 220 Type of harmonical input file that simply specifies a list of 221 chords, with durations, defined by a list of tonal space points. 222 223 For documentation of the file syntax, see 224 U{http://jazzparser.granroth-wilding.co.uk/HarmonicalChordInput}. 225 226 """
227 - def __init__(self, tone_matrix, midi_file=None):
228 self.__tone_matrix = tone_matrix 229 self.__midi_file = midi_file
230
231 - def render(self):
232 # This is built when the file's read, so just return it now 233 matrix = self.__tone_matrix 234 # Now render the matrix to audio 235 return matrix.render()
236
237 - def render_midi(self):
238 # Midi file is actually generated during parsing, so we just return it 239 return self.__midi_file
240 241 @staticmethod
242 - def from_data(data, directives):
243 # Build the tone matrix straight up 244 state = { 245 'equal_temperament' : False, 246 'double_root' : False, 247 'envelope' : None, 248 'origin' : 440, 249 'time' : 0.0, 250 } 251 tone_matrix = ToneMatrix() 252 253 #### Prepare a midi stream 254 mid = EventStream() 255 mid.add_track() 256 # Add a tempo event 257 tempo = SetTempoEvent() 258 tempo.tempo = directives['tempo'] 259 tempo.tick = 0 260 mid.add_event(tempo) 261 262 # Each line represents a single chord or some kind of directive 263 for line in data: 264 first_word = line.split()[0].lower() 265 if "=" in line: 266 # This is an assignment 267 key, __, value = line.partition("=") 268 key = key.strip() 269 value = value.strip() 270 # Check it's valid 271 if key == "equal_temperament": 272 if value not in ['off','on']: 273 raise HarmonicalInputFileReadError, \ 274 "equal_temperament must be 'off' or 'on', "\ 275 "not '%s'" % value 276 value = (value == 'on') 277 elif key == "origin": 278 try: 279 value = int(value) 280 except ValueError: 281 # Try interpreting as a coordinate 282 try: 283 coord = _read_coord(value) 284 except HarmonicalInputFileReadError: 285 raise HarmonicalInputFileReadError, "origin "\ 286 "value must be an integer or a coordinate." 287 value = _get_pitch(coord) 288 elif key == "double_root": 289 if value not in ['off','on']: 290 raise HarmonicalInputFileReadError, \ 291 "double_root must be 'off' or 'on', "\ 292 "not '%s'" % value 293 value = (value == 'on') 294 elif key == "envelope": 295 if value not in ENVELOPES: 296 raise HarmonicalInputFileReadError, "unknown "\ 297 "envelope '%s'. Must be one of: %s" % \ 298 (value, ", ".join(ENVELOPES.keys())) 299 value = ENVELOPES[value]() 300 elif key == "program": 301 # Midi program change 302 try: 303 value = int(value) 304 if value > 127 or value < 0: 305 raise ValueError 306 except ValueError: 307 raise HarmonicalInputFileReadError, "invalid program "\ 308 "change: %s. Should be an integer 0-127" % value 309 pchange = ProgramChangeEvent() 310 pchange.value = value 311 pchange.tick = int(state['time'] * mid.resolution) 312 mid.add_event(pchange) 313 else: 314 raise HarmonicalInputFileReadError, "invalid "\ 315 "assignment key: '%s'" % key 316 # Make this assignment when we get to it in the score 317 state[key] = value 318 elif first_word == "rest": 319 tokens = line.split() 320 duration = _get_duration(tokens) 321 322 # Just move the time counter on without doing anything 323 state['time'] += duration 324 elif first_word == "chord": 325 tokens = line.lstrip("chord").split() 326 duration = _get_duration(tokens) 327 root = _get_root(tokens) 328 volume = _get_volume(tokens) 329 sec_duration = _qn_to_seconds(duration) 330 331 # Must be just a chord type left 332 if len(tokens) > 1: 333 raise HarmonicalInputFileReadError, "chord must "\ 334 "include just a chord type" 335 if len(tokens) == 0: 336 ctype = '' 337 else: 338 ctype = tokens[0] 339 340 # Add the chord to the tone matrix 341 tone_matrix.add_tone( 342 _qn_to_seconds(state['time']), 343 SineChordEvent( 344 _get_pitch(root), 345 ctype, 346 duration=sec_duration, 347 amplitude=volume, 348 equal_temperament=state['equal_temperament'], 349 double_root=state['double_root'], 350 envelope=state['envelope'] 351 ) 352 ) 353 354 # Add the same chord to the midi file 355 tick_time = int(mid.resolution * state['time']) 356 tick_duration = int(duration * mid.resolution) 357 # TODO: currently this will always treat C as the origin 358 # even if you change it with directives 359 events = events_for_chord(root, 360 ctype, 361 tick_time, 362 tick_duration, 363 velocity = int(volume*127), 364 equal_temperament=state['equal_temperament'], 365 double_root=state['double_root']) 366 for ev in events: 367 mid.add_event(ev) 368 369 # Move the timer on ready for the next chord 370 state['time'] += duration 371 elif first_word in ["tones", "t"]: 372 # Must be a chord made up of coordinates 373 tokens = line.lstrip("tones").split() 374 duration = _get_duration(tokens) 375 root = _get_root(tokens) 376 volume = _get_volume(tokens) 377 sec_duration = _qn_to_seconds(duration) 378 379 # The rest should be the list of coordinates 380 coordinates = [_read_coord(token) for token in tokens] 381 382 # Add the chord to the tone matrix 383 tone_matrix.add_tone( 384 _qn_to_seconds(state['time']), 385 SineClusterEvent( 386 _get_pitch(root), 387 coordinates, 388 duration=sec_duration, 389 amplitude=volume, 390 equal_temperament=state['equal_temperament'], 391 double_root=state['double_root'], 392 envelope=state['envelope'] 393 ) 394 ) 395 396 397 # Add the same chord to the midi file 398 tick_time = int(mid.resolution * state['time']) 399 tick_duration = int(duration * mid.resolution) 400 for note in coordinates: 401 # TODO: currently this will always treat C as the origin 402 # even if you change it with directives 403 events = tonal_space_note_events(note, 404 tick_time, 405 tick_duration, 406 velocity = int(volume*127)) 407 if state['equal_temperament']: 408 # Omit tuning event (the first one) 409 events = events[1:] 410 for ev in events: 411 mid.add_event(ev) 412 413 # Move the timer on ready for the next chord 414 state['time'] += duration 415 else: 416 raise HarmonicalInputFileReadError, "could not make sense "\ 417 "of the line: %s" % line 418 return ChordInputFile(tone_matrix, midi_file=mid)
419
420 421 -class ChordDictionaryFile(HarmonicalInputFile):
422 """ 423 Rather like the ChordInputFile format, but specifies a list of chords 424 with names. This is used by the interactive tonal space to specify 425 chord clusters. 426 427 For documentation of the file syntax, see 428 U{http://jazzparser.granroth-wilding.co.uk/HarmonicalChordVocab}. 429 430 """
431 - def __init__(self, chord_dict, names=None):
432 self.chords = chord_dict 433 if names is None: 434 self.chord_names = chord_dict.keys() 435 else: 436 self.chord_names = names
437 438 @staticmethod
439 - def from_data(data, directives):
440 chords = {} 441 names = [] 442 443 # Each line represents a single chord type (TS cluster), or some 444 # kind of directive 445 for line in data: 446 if ":" in line: 447 # Definition of a chord type 448 name, __, value = line.partition(":") 449 name = name.strip() 450 tokens = value.strip().split() 451 notes = [_read_coord(tok) for tok in tokens] 452 chords[name] = notes 453 names.append(name) 454 else: 455 raise HarmonicalInputFileReadError, "could not make sense "\ 456 "of the line: %s" % line 457 return ChordDictionaryFile(chords, names)
458 459 INPUT_TYPES = { 460 'chord' : ChordInputFile, 461 'chord-vocab' : ChordDictionaryFile, 462 }
463 464 -class HarmonicalInputFileReadError(Exception):
465 pass
466