User:Tamas Bates/NetProto/PyAudio: Difference between revisions

From XPUB & Lens-Based wiki
(Created page with "== RAW Wav generation == This script generates semi-random WAV audio by mixing together several flexible sound structures (e.g. play a scale of 5 notes at the same time as a ...")
 
No edit summary
 
(11 intermediate revisions by the same user not shown)
Line 1: Line 1:
== RAW Wav generation ==
__NOINDEX__
== Random Raw WAV generation ==


This script generates semi-random WAV audio by mixing together several flexible sound structures (e.g. play a scale of 5 notes at the same time as a chord made up of 2 other notes). To keep the sound output from being too horrible, a core frequency is defined, and all notes generated in the output are made from frequencies harmonic to the core. This is not quite sufficient, and the program still simply generates horrible noise nine times out of ten.
This script generates semi-random WAV audio by mixing together several flexible sound structures. To keep the sound output from being too horrible, a core frequency is defined (randomly chosen at runtime), and all notes generated in the output are made from frequencies harmonic to the core. This is not quite sufficient, and the program still simply generates horrible noise nine times out of ten. The sound structures mentioned above simply play one or more notes according to a predefined pattern (e.g. play a set of notes in sequence, play a combination of notes as a chord, and so on). The behavior of each is parametrized to allow for some degree of flexibility, and many instances of each structure are created with random values each time the script runs in order to generate different types of output.
 
Output samples:
[[File:PyAudio4.ogg]]
[[File:PyAudio2.ogg]]
[[File:PyAudio1.ogg]]
[[File:PyAudio3.ogg]]


   '''Usage:'''
   '''Usage:'''
     audio.py [-f Filename] [-l Duration]
     '''audio.py''' [-f Filename] [-l Duration]
     This will output Duration seconds (approx.) of data to Filename.
     This will output Duration seconds (approx.) of data to Filename.
     Omitting the -f argument will cause the program to send all data to stdout. Unfortunately, it doesn't generate samples fast enough to play the content in real-time, so this is of limited use...
     Omitting the -f argument will cause the program to send all data to stdout.  
    Unfortunately, it doesn't generate samples fast enough to play the content in real-time, so this is of limited use...
 
The code below is in need of some revision, but [http://thecodelesscode.com/case/63 such is life...]
 
<source lang="python">
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import wave, struct, math, argparse
import math, random, sys, threading
from abc import ABCMeta, abstractmethod, abstractproperty
 
debug = False
nframes=0
nchannels=2
sampwidth=2 # in bytes so 2=16bit, 1=8bit
samplerate=44100
bufsize=2048
max_amplitude = float(int((2 ** (sampwidth * 8)) / 2) - 1)
max_interval_length = 8000
 
def printDebug(str):
    if debug:
        print(str)
 
#--------------------------------#
# Notes generate a constant tone #
#--------------------------------#
class Note():
    def __init__(self, SampleRate, Frequency, Balance):
        self.timeup = ((SampleRate / Frequency) * Balance) / 2
        self.timedown = (SampleRate / Frequency) / 2
        self.time = 0
        self.Up = True
 
    @property
    def period(self):
        return self.timeup + self.timedown
 
    def getSample(self):
        self.time += 1
        if self.Up:
            if self.time > self.timeup:
                self.time = 0
                self.Up = False
            return 0.1
        else:
            if self.time > self.timedown:
                self.time = 0
                self.Up = True
            return -0.1
 
class Silence(Note):
    def __init__(self):
        return
 
    def getSample(self):
        return 0
 
#--------------------------------#
# Instruments generate sequences #
#  of multiple Notes            #
#--------------------------------#
class Instrument(object):
    __metaclass__ = ABCMeta
 
    def get_baseHarmonic(self):
        return self.baseHarmonic
 
    def set_baseHarmonic(self, value):
        self.baseHarmonic = value
    _baseHarmonic = abstractproperty(get_baseHarmonic, set_baseHarmonic)
 
    @abstractmethod
    def nextSample(self):
        return NotImplemented
   
    @abstractmethod
    def reset(self):
        pass
 
class ConstantInstrument(Instrument):
    def __init__(self, note):
        self.note = note
   
    def get_baseHarmonic(self):
        return super(ScaleInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(ScaleInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
   
    def reset(self):
        return
   
    def nextSample(self):
        return self.note.getSample()
 
class BeatInstrument(Instrument):
    def __init__(self, note, beat, samplerate):
        self.note = note
        self.silence = Silence()
        self.duration = (beat / 1000.0) * samplerate
        self.time = 0
        self.silent = False
   
    def get_baseHarmonic(self):
        return super(BeatInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(BeatInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
   
    def reset(self):
        return
   
    def nextSample(self):
        if self.silent:
            sample = self.silence.getSample()
            self.time += 1
            if self.time > self.duration:
                self.time = 0
                self.silent = False
        else:
            sample = self.note.getSample()
            self.time += 1
            if self.time > self.duration:
                self.time = 0
                self.silent = True
        return sample
 
class ScaleInstrument(Instrument):
    # notes should be a sorted list
    def __init__(self, notes, duration, sampleRate, bounce):
        self.scale = notes
        self.curNote = 0
        self.time = 0
        self.duration = (duration / 1000.0) * sampleRate # duration == number of samples to play each note
        self.dir = 1
        self.bounce = bounce
 
    def get_baseHarmonic(self):
        return super(ScaleInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(ScaleInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
   
    def reset(self):
        self.curNote = 0
        self.time = 0
   
    def nextSample(self):
        sample = self.scale[self.curNote].getSample()
        self.time += 1
        if self.time > self.duration:
            self.time = 0
            self.curNote += self.dir
            if (self.curNote >= len(self.scale)) or (self.curNote < 0):
                if self.bounce: # change direction
                    self.curNote = self.curNote - self.dir
                    self.dir = self.dir * -1
                else: # loop
                    self.curNote = 0
        return sample
 
class VibratoInstrument(Instrument):
    def __init__(self, note, stretch):
        self.note = note
        self.silence = Silence()
        self.silentDur = stretch
        self.silent = False
        self.time = 0
 
    def get_baseHarmonic(self):
        return super(ScaleInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(ScaleInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
   
    def reset(self):
        self.time = 0
   
    def nextSample(self):
        if self.silent:
            sample = self.silence.getSample()
            self.time += 1
            if self.time > self.silentDur:
                self.time = 0
                self.silent = False
        else:
            sample = self.note.getSample()
            self.time += 1
            if self.time > self.note.period:
                self.time = 0
                self.silent = True
        return sample
 
class ChordInstrument(Instrument):
    def __init__(self, notes):
        self.notes = notes
   
    def get_baseHarmonic(self):
        return super(ChordInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(ChordInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
   
    def reset(self):
        return
   
    def nextSample(self):
        sample = 0
        for n in self.notes:
            sample += n.getSample()
        return sample
 
class RandChordInstrument(ChordInstrument):
    def __init__(self, baseHarmonic, nNotes, sampleRate):
        self.baseHarmonic = baseHarmonic
        self.nNotes = nNotes
        self.sampleRate = sampleRate
        self.reset()
 
    def reset(self):
        self.notes = []
        for _ in range(self.nNotes):
            self.notes.append(Note(self.sampleRate, random.randint(1, 10) * self.baseHarmonic, 1))
 
class SampleManager():
    def __init__(self, SampleRate):
        self.sampleRate = SampleRate
        self.time = -1
        self.instruments = {}
        self.intervals = []
        self.curInterval = -1
        self.intervalEnd = 0
 
    def nextSample(self):
        amp = 0
        self.time += 1
 
        if self.time >= self.intervalEnd: # need to move to next interval
            self.curInterval += 1
            if self.curInterval == len(self.intervals): # out of intervals to play
                return None
            self.intervalEnd = self.time + self.intervals[self.curInterval]['length']
            printDebug("interval %s: %s" % (self.curInterval, self.intervals[self.curInterval]))
 
        for instrument in self.intervals[self.curInterval]['instruments']:
            amp += self.instruments[instrument].nextSample() * max_amplitude
        if amp > max_amplitude: amp = max_amplitude
        if amp < max_amplitude*-1: amp = max_amplitude*-1
        #sys.stderr.write('%s: %s\n' % (str(self.time), str(amp)))
        #printDebug("Sample #:%s; amp: %s" % (self.time, amp))
        return struct.pack('h', int(amp)) #+ struct.pack('h', int(amp))
 
    def nextFrame(self):
        output = self.nextSample()
        if output is not None:
            output += output #stereo
        return output
 
    # Adds an audio segment
    # length should be in ms, instruments should be a list of names
    def addInterval(self, length, instruments):
        nSamples = (length / 1000.0) * self.sampleRate
        self.intervals.append({'length' : nSamples, 'instruments' : instruments})
 
    # Adds the list of interval data
    def addIntervals(self, intervals):
        for i in intervals:
            nSamples = (i[0] / 1000.0) * self.sampleRate
            self.intervals.append({'length' : nSamples, 'instruments' : i[1]})
 
    # Adds an "instrument"
    def addInstrument(self, name, instrument):
        self.instruments[name] = instrument
 
    # Removes all instruments
    def clearInstruments(self):
        self.instruments.clear()
 
 
#--------------------#
# BEGIN MAIN PROGRAM #
#--------------------#
def RandIntervalsFromDuration(duration):
    intervals = []
    remaining = duration
    while remaining > 0:
        next = random.randint(1, max_interval_length)
        intervals.append(next)
        remaining -= next
    return intervals
 
def RandNote(baseFreq, samplerate):
    return Note(samplerate, random.randint(1,7) * baseFreq, 1)
 
def RandNotes(baseFreq, samplerate):
    notes = []
    nNotes = random.randint(2, 4)
    for _ in range(nNotes):
        notes.append(RandNote(baseFreq, samplerate))
    return notes
 
def RandScale(baseFreq, samplerate):
    scale = []
    nNotes = random.randint(2, 7)
    for i in range(nNotes):
        scale.append(Note(samplerate, baseFreq * (1 + i), 1))
    return scale
 
def RandInstruments(baseFreq, samplerate):
    instruments = []
    nInstruments = random.randint(5, 15)
 
    for _ in range(nInstruments):
        i = random.randint(0, 100)
        if i < 10:
            instruments.append(ConstantInstrument(RandNote(baseFreq, samplerate)))
        elif i < 50:
            instruments.append(ScaleInstrument(RandScale(baseFreq, samplerate), random.randint(20, 1000), samplerate, random.getrandbits(1)))
        elif i < 65:
            instruments.append(VibratoInstrument(RandNote(baseFreq, samplerate), random.randint(20, 800)))
        elif i < 85:
            instruments.append(BeatInstrument(RandNote(baseFreq, samplerate), random.randint(150, 1000), samplerate))
        elif i < 90:
            instruments.append(ChordInstrument(RandNotes(baseFreq, samplerate)))
        elif i < 100:
            instruments.append(RandChordInstrument(baseFreq, random.randint(2, 4), samplerate))
    return instruments
 
def OutputToFile(file, duration):
    duration = duration * 1000 if duration > 0 else 30000
    baseHarmonic = random.randint(20, 200)
 
    printDebug('opening file for output: ' + str(file))
    w = wave.open(file, 'w')
    w.setparams((nchannels, sampwidth, samplerate, nframes, 'NONE', 'not compressed'))
 
    intervals = RandIntervalsFromDuration(duration)
    printDebug('Generating %s intervals to fill %s ms of output: %s' % (len(intervals), duration, intervals))
    instruments = RandInstruments(baseHarmonic, samplerate)
    printDebug('Using %s randomly-generated "instruments": %s' % (len(instruments), instruments))
   
    samplemgr = SampleManager(samplerate)
    for i in range(len(instruments)):
        printDebug('Adding instrument "%s": %s' % (i, instruments[i]))
        samplemgr.addInstrument(str(i), instruments[i])
 
    for i in intervals:
        nInstruments = random.randint(1, len(instruments)) # number of instruments to use in this interval
        intervalInstruments = []
        for _ in range(nInstruments):
            intervalInstruments.append(str(random.randint(0, len(instruments)-1)))
        samplemgr.addInterval(i, intervalInstruments)
 
    data = samplemgr.nextSample()
    while data is not None:
        w.writeframesraw(data)
        data = samplemgr.nextFrame()
   
    w.close()
 
def WriteSamplesContinuously(file, samplemgr):
    data = samplemgr.nextSample()
    while(True):
        try:
            file.writeframesraw(data)
        except IOError:
            pass # swallow IOError from wave.py
        data = samplemgr.nextSample()
 
def AddIntervalsContinuously(samplemgr, instruments):
    while(True):
        intervals = RandIntervalsFromDuration(5000)
        for i in intervals:
            nInstruments = random.randint(1, len(instruments))
            intervalInstruments = []
            for _ in range(nInstruments):
                intervalInstruments.append(str(random.randint(0, len(instruments)-1)))
            samplemgr.addInterval(i, intervalInstruments)
 
def OutputToStdOut(duration):
    if duration > 0:
        OutputToFile(sys.stdout, duration)
        return
 
    baseHarmonic = random.randint(20, 200)
    samplemgr = SampleManager(samplerate)
 
    printDebug('opening stdout for endless output...')
    w = wave.open(sys.stdout, 'w')
    w.setparams((nchannels, sampwidth, samplerate, nframes, 'NONE', 'not compressed'))
 
    printDebug('generating next ~20 seconds of data, using %s Hz as base frequency' % (baseHarmonic))
    intervals = RandIntervalsFromDuration(20000)
    printDebug('Next %s intervals: %s' % (len(intervals), intervals))
    instruments = RandInstruments(baseHarmonic, samplerate)
    printDebug('Generated %s fresh "instruments": %s' % (len(instruments), instruments))
 
    for i in range(len(instruments)):
        printDebug('Adding instrument "%s": %s' % (i, instruments[i]))
        samplemgr.addInstrument(str(i), instruments[i])
    for i in intervals:
        nInstruments = random.randint(1, len(instruments))
        intervalInstruments = []
        for _ in range(nInstruments):
            intervalInstruments.append(str(random.randint(0, len(instruments)-1)))
        samplemgr.addInterval(i, intervalInstruments)
 
    IntervalThread = threading.Thread(target=AddIntervalsContinuously, args=(samplemgr,instruments))
    IntervalThread.daemon = True
    IntervalThread.start()
 
    sys.stderr.write('ready!')
    WriteSamplesContinuously(w, samplemgr)
 
def main(**args):
    global debug
    debug = args['d']
    filename = args['f']
    duration = args['l']
    if(debug):
        print('Debugmode ON')
        print('Output filename: "%s"' % (filename))
        print('Output duration: %s seconds' % (duration))
 
    if len(filename) > 0:
        OutputToFile(filename, duration)
    else:
        OutputToStdOut(duration)
 
#--------------------#
#  END MAIN PROGRAM  #
#--------------------#
 
if __name__ == '__main__':
    argparser = argparse.ArgumentParser(description='Generates semi-random WAV data')
    argparser.add_argument('-f', metavar='Filename', type=str, default='', help='Output file name (will output to stdout if no file is specified)')
    argparser.add_argument('-l', metavar='Length', type=int, default=-1, help='Length of file to produce, in seconds (defaults to 30 seconds for file output, infinite for stdout)')
    argparser.add_argument('-d', action='store_true', help='Turns on debugging spew')
    args = argparser.parse_args()
    main(**vars(args))
 
</source>

Latest revision as of 16:28, 11 November 2013

Random Raw WAV generation

This script generates semi-random WAV audio by mixing together several flexible sound structures. To keep the sound output from being too horrible, a core frequency is defined (randomly chosen at runtime), and all notes generated in the output are made from frequencies harmonic to the core. This is not quite sufficient, and the program still simply generates horrible noise nine times out of ten. The sound structures mentioned above simply play one or more notes according to a predefined pattern (e.g. play a set of notes in sequence, play a combination of notes as a chord, and so on). The behavior of each is parametrized to allow for some degree of flexibility, and many instances of each structure are created with random values each time the script runs in order to generate different types of output.

Output samples: File:PyAudio4.ogg File:PyAudio2.ogg File:PyAudio1.ogg File:PyAudio3.ogg

 Usage:
   audio.py [-f Filename] [-l Duration]
   This will output Duration seconds (approx.) of data to Filename.
   Omitting the -f argument will cause the program to send all data to stdout. 
   Unfortunately, it doesn't generate samples fast enough to play the content in real-time, so this is of limited use...

The code below is in need of some revision, but such is life...

#!/usr/bin/env python
#-*- coding:utf-8 -*-
 
import wave, struct, math, argparse
import math, random, sys, threading
from abc import ABCMeta, abstractmethod, abstractproperty

debug = False
nframes=0
nchannels=2
sampwidth=2 # in bytes so 2=16bit, 1=8bit
samplerate=44100
bufsize=2048
max_amplitude = float(int((2 ** (sampwidth * 8)) / 2) - 1) 
max_interval_length = 8000

def printDebug(str):
    if debug:
        print(str)

#--------------------------------#
# Notes generate a constant tone #
#--------------------------------#
class Note():
    def __init__(self, SampleRate, Frequency, Balance):
        self.timeup = ((SampleRate / Frequency) * Balance) / 2
        self.timedown = (SampleRate / Frequency) / 2
        self.time = 0
        self.Up = True

    @property
    def period(self):
        return self.timeup + self.timedown

    def getSample(self):
        self.time += 1
        if self.Up:
            if self.time > self.timeup:
                self.time = 0
                self.Up = False
            return 0.1
        else:
            if self.time > self.timedown:
                self.time = 0
                self.Up = True
            return -0.1

class Silence(Note):
    def __init__(self):
        return

    def getSample(self):
        return 0

#--------------------------------#
# Instruments generate sequences #
#  of multiple Notes             #
#--------------------------------#
class Instrument(object):
    __metaclass__ = ABCMeta

    def get_baseHarmonic(self):
        return self.baseHarmonic

    def set_baseHarmonic(self, value):
        self.baseHarmonic = value
    _baseHarmonic = abstractproperty(get_baseHarmonic, set_baseHarmonic)

    @abstractmethod
    def nextSample(self):
        return NotImplemented
    
    @abstractmethod
    def reset(self):
        pass

class ConstantInstrument(Instrument):
    def __init__(self, note):
        self.note = note
    
    def get_baseHarmonic(self):
        return super(ScaleInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(ScaleInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
    
    def reset(self):
        return
    
    def nextSample(self):
        return self.note.getSample()

class BeatInstrument(Instrument):
    def __init__(self, note, beat, samplerate):
        self.note = note
        self.silence = Silence()
        self.duration = (beat / 1000.0) * samplerate
        self.time = 0
        self.silent = False
    
    def get_baseHarmonic(self):
        return super(BeatInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(BeatInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
    
    def reset(self):
        return
    
    def nextSample(self):
        if self.silent:
            sample = self.silence.getSample()
            self.time += 1
            if self.time > self.duration:
                self.time = 0
                self.silent = False
        else:
            sample = self.note.getSample()
            self.time += 1
            if self.time > self.duration:
                self.time = 0
                self.silent = True
        return sample

class ScaleInstrument(Instrument):
    # notes should be a sorted list
    def __init__(self, notes, duration, sampleRate, bounce):
        self.scale = notes
        self.curNote = 0
        self.time = 0
        self.duration = (duration / 1000.0) * sampleRate # duration == number of samples to play each note
        self.dir = 1
        self.bounce = bounce

    def get_baseHarmonic(self):
        return super(ScaleInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(ScaleInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
    
    def reset(self):
        self.curNote = 0
        self.time = 0
    
    def nextSample(self):
        sample = self.scale[self.curNote].getSample()
        self.time += 1
        if self.time > self.duration:
            self.time = 0
            self.curNote += self.dir
            if (self.curNote >= len(self.scale)) or (self.curNote < 0):
                if self.bounce: # change direction
                    self.curNote = self.curNote - self.dir
                    self.dir = self.dir * -1
                else: # loop
                    self.curNote = 0
        return sample

class VibratoInstrument(Instrument):
    def __init__(self, note, stretch):
        self.note = note
        self.silence = Silence()
        self.silentDur = stretch
        self.silent = False
        self.time = 0

    def get_baseHarmonic(self):
        return super(ScaleInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(ScaleInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
    
    def reset(self):
        self.time = 0
    
    def nextSample(self):
        if self.silent:
            sample = self.silence.getSample()
            self.time += 1
            if self.time > self.silentDur:
                self.time = 0
                self.silent = False
        else:
            sample = self.note.getSample()
            self.time += 1
            if self.time > self.note.period:
                self.time = 0
                self.silent = True
        return sample

class ChordInstrument(Instrument):
    def __init__(self, notes):
        self.notes = notes
    
    def get_baseHarmonic(self):
        return super(ChordInstrument, self).get_baseHarmonic()
    def set_baseHarmonic(self, value):
        super(ChordInstrument, self).set_baseHarmonic(value)
    _baseHarmonic = property(get_baseHarmonic, set_baseHarmonic)
    
    def reset(self):
        return
    
    def nextSample(self):
        sample = 0
        for n in self.notes:
            sample += n.getSample()
        return sample

class RandChordInstrument(ChordInstrument):
    def __init__(self, baseHarmonic, nNotes, sampleRate):
        self.baseHarmonic = baseHarmonic
        self.nNotes = nNotes
        self.sampleRate = sampleRate
        self.reset()

    def reset(self):
        self.notes = []
        for _ in range(self.nNotes):
            self.notes.append(Note(self.sampleRate, random.randint(1, 10) * self.baseHarmonic, 1))

class SampleManager():
    def __init__(self, SampleRate):
        self.sampleRate = SampleRate
        self.time = -1
        self.instruments = {}
        self.intervals = []
        self.curInterval = -1
        self.intervalEnd = 0

    def nextSample(self):
        amp = 0
        self.time += 1

        if self.time >= self.intervalEnd: # need to move to next interval
            self.curInterval += 1
            if self.curInterval == len(self.intervals): # out of intervals to play
                return None
            self.intervalEnd = self.time + self.intervals[self.curInterval]['length']
            printDebug("interval %s: %s" % (self.curInterval, self.intervals[self.curInterval]))

        for instrument in self.intervals[self.curInterval]['instruments']:
            amp += self.instruments[instrument].nextSample() * max_amplitude
        if amp > max_amplitude: amp = max_amplitude
        if amp < max_amplitude*-1: amp = max_amplitude*-1
        #sys.stderr.write('%s: %s\n' % (str(self.time), str(amp)))
        #printDebug("Sample #:%s; amp: %s" % (self.time, amp))
        return struct.pack('h', int(amp)) #+ struct.pack('h', int(amp))

    def nextFrame(self):
        output = self.nextSample()
        if output is not None:
            output += output #stereo
        return output

    # Adds an audio segment
    # length should be in ms, instruments should be a list of names
    def addInterval(self, length, instruments):
        nSamples = (length / 1000.0) * self.sampleRate
        self.intervals.append({'length' : nSamples, 'instruments' : instruments})

    # Adds the list of interval data
    def addIntervals(self, intervals):
        for i in intervals:
            nSamples = (i[0] / 1000.0) * self.sampleRate
            self.intervals.append({'length' : nSamples, 'instruments' : i[1]})

    # Adds an "instrument"
    def addInstrument(self, name, instrument):
        self.instruments[name] = instrument

    # Removes all instruments
    def clearInstruments(self):
        self.instruments.clear()


#--------------------#
# BEGIN MAIN PROGRAM #
#--------------------#
def RandIntervalsFromDuration(duration):
    intervals = []
    remaining = duration
    while remaining > 0:
        next = random.randint(1, max_interval_length)
        intervals.append(next)
        remaining -= next
    return intervals

def RandNote(baseFreq, samplerate):
    return Note(samplerate, random.randint(1,7) * baseFreq, 1)

def RandNotes(baseFreq, samplerate):
    notes = []
    nNotes = random.randint(2, 4)
    for _ in range(nNotes):
        notes.append(RandNote(baseFreq, samplerate))
    return notes

def RandScale(baseFreq, samplerate):
    scale = []
    nNotes = random.randint(2, 7)
    for i in range(nNotes):
        scale.append(Note(samplerate, baseFreq * (1 + i), 1))
    return scale

def RandInstruments(baseFreq, samplerate):
    instruments = []
    nInstruments = random.randint(5, 15)

    for _ in range(nInstruments):
        i = random.randint(0, 100)
        if i < 10:
            instruments.append(ConstantInstrument(RandNote(baseFreq, samplerate)))
        elif i < 50:
            instruments.append(ScaleInstrument(RandScale(baseFreq, samplerate), random.randint(20, 1000), samplerate, random.getrandbits(1)))
        elif i < 65:
            instruments.append(VibratoInstrument(RandNote(baseFreq, samplerate), random.randint(20, 800)))
        elif i < 85:
            instruments.append(BeatInstrument(RandNote(baseFreq, samplerate), random.randint(150, 1000), samplerate))
        elif i < 90:
            instruments.append(ChordInstrument(RandNotes(baseFreq, samplerate)))
        elif i < 100:
            instruments.append(RandChordInstrument(baseFreq, random.randint(2, 4), samplerate))
    return instruments

def OutputToFile(file, duration):
    duration = duration * 1000 if duration > 0 else 30000
    baseHarmonic = random.randint(20, 200)

    printDebug('opening file for output: ' + str(file))
    w = wave.open(file, 'w')
    w.setparams((nchannels, sampwidth, samplerate, nframes, 'NONE', 'not compressed'))

    intervals = RandIntervalsFromDuration(duration)
    printDebug('Generating %s intervals to fill %s ms of output: %s' % (len(intervals), duration, intervals))
    instruments = RandInstruments(baseHarmonic, samplerate)
    printDebug('Using %s randomly-generated "instruments": %s' % (len(instruments), instruments))
    
    samplemgr = SampleManager(samplerate)
    for i in range(len(instruments)):
        printDebug('Adding instrument "%s": %s' % (i, instruments[i]))
        samplemgr.addInstrument(str(i), instruments[i])

    for i in intervals:
        nInstruments = random.randint(1, len(instruments)) # number of instruments to use in this interval
        intervalInstruments = []
        for _ in range(nInstruments):
            intervalInstruments.append(str(random.randint(0, len(instruments)-1)))
        samplemgr.addInterval(i, intervalInstruments)

    data = samplemgr.nextSample()
    while data is not None:
        w.writeframesraw(data)
        data = samplemgr.nextFrame()
    
    w.close()

def WriteSamplesContinuously(file, samplemgr):
    data = samplemgr.nextSample()
    while(True):
        try:
            file.writeframesraw(data)
        except IOError:
            pass # swallow IOError from wave.py
        data = samplemgr.nextSample()

def AddIntervalsContinuously(samplemgr, instruments):
    while(True):
        intervals = RandIntervalsFromDuration(5000)
        for i in intervals:
            nInstruments = random.randint(1, len(instruments))
            intervalInstruments = []
            for _ in range(nInstruments):
                intervalInstruments.append(str(random.randint(0, len(instruments)-1)))
            samplemgr.addInterval(i, intervalInstruments)

def OutputToStdOut(duration):
    if duration > 0:
        OutputToFile(sys.stdout, duration)
        return

    baseHarmonic = random.randint(20, 200)
    samplemgr = SampleManager(samplerate)

    printDebug('opening stdout for endless output...')
    w = wave.open(sys.stdout, 'w')
    w.setparams((nchannels, sampwidth, samplerate, nframes, 'NONE', 'not compressed'))

    printDebug('generating next ~20 seconds of data, using %s Hz as base frequency' % (baseHarmonic))
    intervals = RandIntervalsFromDuration(20000)
    printDebug('Next %s intervals: %s' % (len(intervals), intervals))
    instruments = RandInstruments(baseHarmonic, samplerate)
    printDebug('Generated %s fresh "instruments": %s' % (len(instruments), instruments))

    for i in range(len(instruments)):
        printDebug('Adding instrument "%s": %s' % (i, instruments[i]))
        samplemgr.addInstrument(str(i), instruments[i])
    for i in intervals:
        nInstruments = random.randint(1, len(instruments))
        intervalInstruments = []
        for _ in range(nInstruments):
            intervalInstruments.append(str(random.randint(0, len(instruments)-1)))
        samplemgr.addInterval(i, intervalInstruments)

    IntervalThread = threading.Thread(target=AddIntervalsContinuously, args=(samplemgr,instruments))
    IntervalThread.daemon = True
    IntervalThread.start()

    sys.stderr.write('ready!')
    WriteSamplesContinuously(w, samplemgr)

def main(**args):
    global debug
    debug = args['d']
    filename = args['f']
    duration = args['l']
    if(debug):
        print('Debugmode ON')
        print('Output filename: "%s"' % (filename))
        print('Output duration: %s seconds' % (duration))

    if len(filename) > 0:
        OutputToFile(filename, duration)
    else:
        OutputToStdOut(duration)

#--------------------#
#  END MAIN PROGRAM  #
#--------------------#

if __name__ == '__main__':
    argparser = argparse.ArgumentParser(description='Generates semi-random WAV data')
    argparser.add_argument('-f', metavar='Filename', type=str, default='', help='Output file name (will output to stdout if no file is specified)')
    argparser.add_argument('-l', metavar='Length', type=int, default=-1, help='Length of file to produce, in seconds (defaults to 30 seconds for file output, infinite for stdout)')
    argparser.add_argument('-d', action='store_true', help='Turns on debugging spew')
    args = argparser.parse_args()
    main(**vars(args))