User:Tamas Bates/NetProto/PyAudio

From XPUB & Lens-Based wiki

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))