Playing tones in Python

A place to discuss the implementation and style of computer programs.

Moderators: phlip, Moderators General, Prelates

User avatar
darkspork
Posts: 532
Joined: Tue Sep 23, 2008 12:43 am UTC
Location: Land of Trains and Suburbs

Playing tones in Python

Postby darkspork » Thu Nov 19, 2009 3:30 am UTC

You may remember the random name generator I tried making a while back. This time, I've decided on a slightly more ambitious project: a composer... a song writer. I'll post the python source later when it works better, and is better documented. Anyway, one of the major problems I've been having is the inability to test the code. The thing spits out 70+ notes at a time...

Code: Select all

4 measures @ 16 ticks per measure.
Bass Line:
-1C                                                             
-1E                                                             
-1G                                                             
-1D
Melody:
  C       E       G             -1A             -1A             
-1B             -1A     -1G       E               E             
  D             -1B             -1B             -1B             
-1G             -1F             -1A               D
Harmony 1:
  E       E       C       C       C       C       E     -1A     
-1B       E       C       C       G     -1A       C       B     
  B     -1B       E       D       D       D       B       B     
  B       G       F       F       A       A     -1B       A
Harmony 2:
+1C       G       G       F       E   E   F   E +1E +1E +1F     
  B       B     +1G     +1E     +1A     +1A       B     +1G     
+1G       B       G       G +1D   B     +1G     +1D     +1D   B
  G       B     +1D       A       A     +1A +1A +1A     +1F

... and it's not pretty. I have to put each note into a MIDI sequencing program in order to hear the music. Actually creating a MIDI file is beyond me, so I was wondering if there's a way to play a tone (tone, not a sound file) in Python. (or C, or Java, or... whatever) The only language I've ever figured out how to play a tone in is QBasic, which I haven't touched in years.

P.S. If anyone's wondering what I did to create the song, it's completely random... sort of. It knows about harmonic/melodic dissonance. It picks a note at random (though a closer note to the previous is more likely) and then rejects that note if it is out of the proper scale or has too much average dissonance with the other notes.
Shameless Website Promotion: Gamma Energy
My new esoteric programming language: GLOBOL
An experiment to mess with Google Search results: HARDCORE PORNOGRAPHY HARDCORE PORNOGRAPHY

User avatar
PM 2Ring
Posts: 3619
Joined: Mon Jan 26, 2009 3:19 pm UTC
Location: Mid north coast, NSW, Australia

Re: Playing tones in Python

Postby PM 2Ring » Thu Nov 19, 2009 10:47 am UTC

If you're on Linux, you can do something like this:

python -c "freq=220;sec=2;from math import sin,pi;rate=8000;w=[chr(127+int(127*sin(i*2*pi*freq/rate))) for i in xrange(rate)]*sec;s=''.join(w);print s" >/dev/dsp

The dsp device uses unsigned byte samples, with a sampling rate of 8000 samples/second. So the above snippet should play 2 seconds of a 220 Hz sine wave. If I haven't stuffed up the arithmetic. :)

User avatar
darkspork
Posts: 532
Joined: Tue Sep 23, 2008 12:43 am UTC
Location: Land of Trains and Suburbs

Re: Playing tones in Python

Postby darkspork » Thu Nov 19, 2009 5:03 pm UTC

PM 2Ring wrote:If you're on Linux, you can do something like this:

python -c "freq=220;sec=2;from math import sin,pi;rate=8000;w=[chr(127+int(127*sin(i*2*pi*freq/rate))) for i in xrange(rate)]*sec;s=''.join(w);print s" >/dev/dsp

The dsp device uses unsigned byte samples, with a sampling rate of 8000 samples/second. So the above snippet should play 2 seconds of a 220 Hz sine wave. If I haven't stuffed up the arithmetic. :)

This code makes sense, though I personally prefer the square wave because it requires less processing to create, and sounds more awesome. I'll have to head over to the Ubuntu machine to test this. Windows fails at piping, and there seems to be no equivalent device on OSX.

Is there a more universal way to do this? As long as we're synthesizing raw waves here, I think I'll take a look at the .wav format. It appears to be the simplest of the audio formats. Still, I get the feeling there's an easier way.
Shameless Website Promotion: Gamma Energy
My new esoteric programming language: GLOBOL
An experiment to mess with Google Search results: HARDCORE PORNOGRAPHY HARDCORE PORNOGRAPHY

qbg
Posts: 586
Joined: Tue Dec 18, 2007 3:37 pm UTC

Re: Playing tones in Python

Postby qbg » Thu Nov 19, 2009 5:58 pm UTC

darkspork wrote:As long as we're synthesizing raw waves here, I think I'll take a look at the .wav format.

If you are going to do that, why not write a midi file instead? It wouldn't be nearly as slow.

User avatar
lulzfish
Posts: 1214
Joined: Tue Dec 16, 2008 8:17 am UTC

Re: Playing tones in Python

Postby lulzfish » Thu Nov 19, 2009 6:34 pm UTC

darkspork wrote:
PM 2Ring wrote:If you're on Linux, you can do something like this:

python -c "freq=220;sec=2;from math import sin,pi;rate=8000;w=[chr(127+int(127*sin(i*2*pi*freq/rate))) for i in xrange(rate)]*sec;s=''.join(w);print s" >/dev/dsp

The dsp device uses unsigned byte samples, with a sampling rate of 8000 samples/second. So the above snippet should play 2 seconds of a 220 Hz sine wave. If I haven't stuffed up the arithmetic. :)

This code makes sense, though I personally prefer the square wave because it requires less processing to create, and sounds more awesome. I'll have to head over to the Ubuntu machine to test this. Windows fails at piping, and there seems to be no equivalent device on OSX.

Is there a more universal way to do this? As long as we're synthesizing raw waves here, I think I'll take a look at the .wav format. It appears to be the simplest of the audio formats. Still, I get the feeling there's an easier way.

It took me a while to remember, since my mind has been filled with GStreamer stuff lately, but SDL totally has a simple audio output system that works on Mac, Windows, and Linux.

Try downloading pygame, it looks like their sound sample output is in the sndarray bit:
http://www.pygame.org/docs/ref/sndarray.html

User avatar
darkspork
Posts: 532
Joined: Tue Sep 23, 2008 12:43 am UTC
Location: Land of Trains and Suburbs

Re: Playing tones in Python

Postby darkspork » Thu Nov 19, 2009 7:47 pm UTC

qbg wrote:
darkspork wrote:As long as we're synthesizing raw waves here, I think I'll take a look at the .wav format.

If you are going to do that, why not write a midi file instead? It wouldn't be nearly as slow.

Because the material I've found to explain the .WAV format makes a lot more sense than the material I've found to explain the .MIDI format.
lulzfish wrote:Try downloading pygame, it looks like their sound sample output is in the sndarray bit:
http://www.pygame.org/docs/ref/sndarray.html

This will be investigated...
Shameless Website Promotion: Gamma Energy
My new esoteric programming language: GLOBOL
An experiment to mess with Google Search results: HARDCORE PORNOGRAPHY HARDCORE PORNOGRAPHY

DYRE
Posts: 98
Joined: Sun Dec 30, 2007 2:49 pm UTC

Re: Playing tones in Python

Postby DYRE » Thu Nov 19, 2009 7:53 pm UTC

pygame is probably unnecessary though, for this.


pyaudiere is probably more suited to this. It's only one command to create a square wave.

Kulshrax
Posts: 7
Joined: Mon Aug 24, 2009 5:52 pm UTC

Re: Playing tones in Python

Postby Kulshrax » Fri Nov 20, 2009 2:45 am UTC

On Windows, you can use winsound.

Code: Select all

import winsound
winsound.Beep(frequency,duration)


However, this uses the system beep and it is Windows-only, so its usefulness is rather limited.

User avatar
Squid Tamer
Posts: 220
Joined: Fri Apr 03, 2009 3:59 am UTC
Location: Over there
Contact:

Re: Playing tones in Python

Postby Squid Tamer » Fri Nov 20, 2009 3:19 am UTC

Kulshrax wrote:On Windows, you can use winsound.

Code: Select all

import winsound
winsound.Beep(frequency,duration)


However, this uses the system beep and it is Windows-only, so its usefulness is rather limited.

For those on Linux, the "beep" command is available for Linux, and does more or less exactly the same thing.

Code: Select all

import os
os.system("beep -f [hertz] -l [length in milliseconds]")

User avatar
PM 2Ring
Posts: 3619
Joined: Mon Jan 26, 2009 3:19 pm UTC
Location: Mid north coast, NSW, Australia

Re: Playing tones in Python

Postby PM 2Ring » Fri Nov 20, 2009 3:33 am UTC

This code makes sense, though I personally prefer the square wave because it requires less processing to create, and sounds more awesome.

Square waves sound very fat, but I prefer to feed audio circuitry less brutal waveforms. :) Maybe compromise & use triangular waves, which are very cheap to calculate. But sione waves are no big deal. You really only need one waveform per octave for quite good sound quality.

Another interesting option is to use Shepard tones.
A Shepard tone, named after Roger Shepard, is a sound consisting of a superposition of sine waves separated by octaves. When played with the base pitch of the tone moving upwards or downwards, it is referred to as the Shepard scale. This creates the auditory illusion of a tone that continually ascends or descends in pitch, yet which ultimately seems to get no higher or lower.
For best results, use 16 bit samples or better, but I used to get adequate results on the Amiga with 8 bit Shepard tones, using only 4 octaves per note (like the example in Godel, Escher, Bach).

Sorry, I don't know much about programming on Windows, so I can't answer your Windows-oriented questions.

User avatar
Outchanter
Posts: 668
Joined: Mon Dec 17, 2007 8:40 am UTC
Location: South African in Americaland

Re: Playing tones in Python

Postby Outchanter » Fri Nov 20, 2009 4:21 am UTC

Outputting to a midi file isn't actually all that difficult. If you want, I can try edit some code from an older project of mine to give you an example, but all it really takes is a bit of experimentation (i.e. output the simplest file you can think of, then once that works, expand on it). You should find all the information you need on these pages:

http://www.sonicspot.com/guide/midifiles.html
http://www.midi.org/techspecs/gm1sound.php
http://docs.python.org/3.1/library/struct.html
http://docs.python.org/3.1/library/array.html
~ You will eat a tasty fortune cookie. Oh look, it came true already! ~

User avatar
You, sir, name?
Posts: 6974
Joined: Sun Apr 22, 2007 10:07 am UTC
Location: Chako Paul City
Contact:

Re: Playing tones in Python

Postby You, sir, name? » Fri Nov 20, 2009 2:22 pm UTC

Outchanter wrote:Outputting to a midi file isn't actually all that difficult. If you want, I can try edit some code from an older project of mine to give you an example, but all it really takes is a bit of experimentation (i.e. output the simplest file you can think of, then once that works, expand on it). You should find all the information you need on these pages:

http://www.sonicspot.com/guide/midifiles.html
http://www.midi.org/techspecs/gm1sound.php
http://docs.python.org/3.1/library/struct.html
http://docs.python.org/3.1/library/array.html


This is also pretty helpful. At least that's what I thought when I fiddled with MIDI programming: http://home.roadrunner.com/~jgglatt/tech/midispec.htm
I edit my posts a lot and sometimes the words wrong order words appear in sentences get messed up.

User avatar
phlip
Restorer of Worlds
Posts: 7543
Joined: Sat Sep 23, 2006 3:56 am UTC
Location: Australia
Contact:

Re: Playing tones in Python

Postby phlip » Mon Nov 23, 2009 2:56 am UTC

The MIDI file format is a crime against nature... it's just whatever would get sent down the wire in a realtime MIDI system, with timing information added. Which, sure, is great if all you're doing is interacting with real MIDI devices, but it's absolutely awful for any other purpose ever.

I've written a couple of programs that generate MIDI files, and one thing that could read a heavily-cut-down one... but so far, I haven't had to code the matching up of note-on and note-off events (why are they separate in the file? Because they'd be sent separately down the wire, of course.) and hopefully I'll never have to.

Code: Select all

enum ಠ_ಠ {°□°╰=1, °Д°╰, ಠ益ಠ╰};
void ┻━┻︵​╰(ಠ_ಠ ⚠) {exit((int)⚠);}
[he/him/his]

User avatar
Cosmologicon
Posts: 1806
Joined: Sat Nov 25, 2006 9:47 am UTC
Location: Cambridge MA USA
Contact:

Re: Playing tones in Python

Postby Cosmologicon » Mon Nov 23, 2009 10:44 pm UTC

As of version 1.9.0, pygame has built-in MIDI support through the midi module. Meaning you can play MIDI sounds without a file. I've used it successfully, and it wasn't that hard, but I seem to recall having difficulty initially setting the device id.

Apparently the documentation for the midi module is a secret, because it doesn't show up in the header of the main pygame docs page. I was starting to think I'd imagined the whole thing....

User avatar
darkspork
Posts: 532
Joined: Tue Sep 23, 2008 12:43 am UTC
Location: Land of Trains and Suburbs

Re: Playing tones in Python

Postby darkspork » Thu Nov 26, 2009 6:28 pm UTC

I couldn't get pyaudiere working, so I ended up writing a WAV generator manually. WAV isn't that hard, especially in 8 bit mono.

As of now, all of this code works, with the exception that it won't use different instruments than the square wave when you tell it to.

Pythoven.py

Code: Select all

# Pythoven 1.4
# Writes music

# Notes stored as a list of tuples of the form (start, step)
# The first entry in a track is the track's length
# time in measure is on a scale of sixteenth notes (0x0 -> 0xF)
# measures are therefore measured hexadecimally as well.
# This convention is not required, but is recommended for anything
# written in a binary time (4/4, 2/4, etc...)

# note is an integer representing the number of half-steps off the
# base note

import random                   # Access random functions
from random import randint      # Used in wrand

def vprintno(s):
    """vprintno
        do nothing"""
    pass

def vprintyes(s):
    """vprintyes
        print s"""
    print s
def makeScale(rawScale):
    """makeScale
        rawScale - a raw version of the scale to use
        returns a scale: a tuple such that the
            first item is a list of acceptable offsets,
            and the second is the size of the scale"""
    note = 0
    li = []
    for n in rawScale:
        li.append(note)
        note += n
    return (li, sum(rawScale))

# The scale of notes, represented in text form
NOTES = ('C','C#','D','D#','E','F','F#','G','G#','A','A#','B')

# Various scales
MAJOR          = makeScale((2,2,1,2,2,2,1))
NATURAL_MINOR  = makeScale((2,1,2,2,1,2,2))
MELODIC_MINOR  = makeScale((2,1,2,2,2,2,1))
HARMONIC_MINOR = makeScale((2,1,2,2,1,3,1))
WHOLE_TONE     = makeScale((2,2,2,2,2,2))
PENTATONIC     = makeScale((2,3,2,2))
CHROMATIC      = makeScale((1,1,1,1,1,1,1,1,1,1,1,1))

# Notes and quality
# Basically, measure the amount of dissonance, in half-steps
# Higher number = worse sound
# If dissonance becomes too high, then remake this note
# dissonance = melodic dissonance + average harmonic dissonance

# Half-steps:              -C  -B  -A  -9  -8  -7  -6  -5  -4  -3  -2  -1   0   1   2   3   4   5   6   7   8   9   A   B   C
# In the key of C:         C   C#  D   D#  E   F   F#  G   G#  A   A#  B   C   C#  D   D#  E   F   F#  G   G#  A   A#  B   C

MELODIC_INTERVAL = ((-12,5),(-11,6),(-10,4),( -9,3),( -8,2),( -7,3),
                    ( -6,4),( -5,1),( -4,1),( -3,2),( -2,2),( -1,3),
                    (  0,0),
                    (  1,3),(  2,2),(  3,2),(  4,1),(  5,1),(  6,4),
                    (  7,3),(  8,2),(  9,3),( 10,4),( 11,6),( 12,5))

# Half-steps:               0   1   2   3   4   5   6   7   8   9   A   B
# In the key of C:         C   C#  D   D#  E   F   F#  G   G#  A   A#  B
HARMONIC_INTERVAL      = (  0, 10,  8,  3,  2,  1,  8,  1,  2,  3,  7,  9)

def getLastNote(track, position):
    """getLastNote
        track    - a list of notes
        position - the position (int) to start from
        return a tuple (time, note)
            of the most recent note from position"""
    return [(time, note) for (time, note) in track[1:] if time <= position][-1]

def noteString(note, key='C', padded = False):
    """noteString
        note - a number indicating the halfstep offset from the key
        key - the key to print the track in as a string (i.e. 'F#')
        return a string representation of a note"""
    i = note + NOTES.index(key)
    g = len(NOTES)
    n = 0
    s = ''
    while i < 0:
        i += g
        n -= 1
    while i >= g:
        i -= g
        n += 1
    if n > 0:
        s = "+%d%s"%(n,NOTES[i])
    elif n < 0:
        s = "%d%s"%(n,NOTES[i])
    else:
        s = "%s%s"%(padded and '  ' or '',NOTES[i])
    if padded:
        return s.ljust(4)
    return s

def trackString(track, key='C', measure=0):
    """trackString
        track   - a list of notes
        key     - the key to print the track in as a string (i.e. 'F#')
        measure - length of measure
                    note: if given, makes output easier to read
        return a string representation of a track"""
    #return "".join(["%s%s"%("".ljust(2*time),noteString(note, key).ljust(2)) for (time, note) in track]).replace(" ", "-")
    if measure:
        i  = 0
        li = []
        for (time, note) in track[1:]:
            while i < time:
                if not i % measure: # on measure break
                    li.append('\n')
                li.append('    ')
                i += 1
            if not i % measure: # on measure break
               li.append('\n')
            li.append(noteString(note, key, True))
            i += 1
        return "".join(li)
    return ", ".join(["0x%X:%s"%(time,noteString(note, key, False)) for (time, note) in track[1:]])

def wrap(li, i):
    """wrap
        li - a list
        i  - an index
        return li[i] (if i is out of range, wrap around)"""
    l = len(li)
    j = i
    while j < 0:
        j += l
    while j >= l:
        j -= l
    return li[j]

def inScale(note, scale=MAJOR):
    """inScale
        note  - a numeric key offset note
        scale - scale to investigate
        size  - # of half steps in scale
        return true if note is in scale"""
    n = note
    while n < 0:
        n += scale[1]
    while n > scale[1]:
        n -= scale[1]
    return n in scale[0]

def wrand(d):
    """Find a value with a weighted average
        d - a wrand dictionary
        returns a random value in d"""
    i = randint(0, d['max'])
    while i>=0 and not d.has_key(i):
        i -= 1
    return d[i]

def melodic2wrand(melodic=MELODIC_INTERVAL):
    """melodic2wrand
        melodic - a melodic interval to use"""
    d = {}
    weight = 0
    for (interval, freq) in melodic:
        d[weight] = interval
        weight += 10-freq
    d['max'] = weight
    return d

def mktimes(measure, beat, sync, length):
    """mktimes
        measure  - number of beats in a measure
        beat     - default beats per note (must be < measure, should be divisible by measure)
        sync     - % chance that each beat will be split
        length   - how many measures to create
        returns a list of times that a note should appear"""
    li = []
    for i in range(0,length*measure//beat):
        li.append(i * beat)
        if (randint(0,100) < sync):
            li.append(i * beat + beat // 2)
    return li

def loop(track, length):
    """loop
        track - the track to loop
        length - the number of ticks to loop to
        returns a new track"""
    newtrack=[length]
    i = 0;
    while i < length:
        newtrack.extend([(time+i,note) for (time,note) in track[1:]])
        i += track[0]
    while newtrack[-1][0] > length:
        del(newtrack[-1])
    return newtrack
   
def avgdissonance(sheet, time, mainnote, harmonic):   
    """avgdissonance
        sheet    - a music sheet
        time     - a time to count dissonance at
        mainnote - the note to check dissonance against
        harmonic - a harmonic interval to use
        returns the average dissonance between
            the last track's last note and the rest"""
    notes = [getLastNote(track, time) for track in sheet]
    return sum([wrap(harmonic,abs(n[1]-mainnote)) for n in notes]) // len(sheet)

def shift(track, halfsteps):
    """shift
        track     - the track to shift
        halfsteps - number of half-steps (positive or negative) to shift the track
        returns None, shifts track"""
    for i in range(1, len(track)):
        note=track[i]
        track[i] = (note[0],note[1]+halfsteps)

def compose(measure=16, beat=4, sync=14, length=1, stray=10, scale=MAJOR, melodic=MELODIC_INTERVAL, seed=None):
    """compose
        measure  - number of beats in a measure
        beat     - default beats per note (must be < measure, should be divisible by measure)
        sync     - % chance that each beat will be split
        length   - how many measures to create
        stray    - how many half-steps the notes are allowed to stray from 0
        scale    - a musical scale to use
        melodic  - melodic interval frequency
        seed     - random seed
        returns a track"""
    random.seed(seed) # initialize the random
    mwrand = melodic2wrand(melodic)
    track = [length*measure,(0,0)]
    times = mktimes(measure, beat, sync, length)
    del(times[0])
    for i in times:
        note = track[-1][1] + wrand(mwrand) # because the last note in the track will always be the prev.
        while note > stray or note < -stray or not inScale(note,scale):
            note = track[-1][1] + wrand(mwrand) # choose next note as a melodic of the previous
        track.append((i, note))
    return track

def counterpoint(sheet, start, measure=16, beat=4, sync=27, length=1, stray=7, dissonance=3,
                 scale=MAJOR, melodic=MELODIC_INTERVAL, harmonic=HARMONIC_INTERVAL, seed=None):
    """compose
        sheet      - a list of other tracks in the song
        start      - a note (offset) to begin on
        measure    - number of beats in a measure
        beat       - default beats per note (must be < measure, should be divisible by measure)
        sync       - % chance that each beat will be split
        length     - how many measures to create. Note: all tracks will be looped to appropriate
                        length before calculation
        stray      - how many half-steps the notes are allowed to stray from the first
        dissonance - maximum average dissonance allowed per note
        scale      - a musical scale to use
        melodic    - melodic interval frequency
        harmonic   - harmonic dissonance
        seed       - random seed
        returns None, adds new track to sheet"""
    random.seed(seed) # initialize the random
    mwrand = melodic2wrand(melodic)
    track = [length*measure,(0,start)]
    tsheet = [loop(t, measure*length) for t in sheet] # create a temporary bastardized loop version
    times = mktimes(measure, beat, sync, length)
    highstray = start + stray
    lowstray  = start - stray
    del(times[0])
    for i in times:
        note = track[-1][1] + wrand(mwrand)
        while note > highstray or note < lowstray or not inScale(note,scale) or avgdissonance(tsheet,i,note,harmonic) > dissonance:
            note = track[-1][1] + wrand(mwrand)
        track.append((i, note))
    sheet.append(track)

def bass(scale=MAJOR):
    track=compose(beat=16,sync=0,length=4,scale=scale)
    shift(track, -12)
    return track

def sing(sheet, key='C', ticktime=125, instruments = (), filename='./test.wav', verbose=0):
    """sing
        sheet       - a music sheet to sing
        key         - the key to sing in
        ticktime    - the time each tick in the
                        sheet should take, in milliseconds.
                        The default 125 with 16-tick measures
                        in 4/4 time will result in
                        120 beats per minute
        instruments - a list of instruments to use
        filename    - the name of the wav file to make
        return None, create a wav file from the sheet"""
    # import wav making tools
    from wavmaker import freq, makeWavFile, mergeWaves, divide
    # clone instrument list
    instrumentscpy = instruments[:]
    # set up verbosity
    vprint = vprintno
    if verbose:
        vprint = vprintyes
    vprint('Calculating track length...')
    length = max([track[0] for track in sheet])
    vprint('File will be approx. %d seconds' % (length*ticktime//1000))
    vprint('Looping tracks...')
    loopedsheet=[loop(track, length) for track in sheet]
    # Convert the sheet from absolute times to relative times, and
    # relative notes to MIDI style absolute notes
    vprint('Calculating cues...')
    cues = []
    offset = NOTES.index(key) + 48 # Middle C is MIDI note #48
    for track in loopedsheet:
        cuedtrack = track[1:]
        cuedtrack.append((track[0],0))
        cues.append([(cuedtrack[i+1][0] - cuedtrack[i][0], cuedtrack[i][1] + offset) for i in range(0,len(cuedtrack)-1)])
    # cues now contains the sheet in a usable format
    vprint('Generating waves...')
    waves = [divide(''.join([waveGenII(freq[note],duration*ticktime,instrumentscpy) for (duration, note) in track]), len(cues)) for track in cues]
    vprint('Creating mixdown...')
    wave = reduce(mergeWaves, waves[1:], waves[0])
    vprint('Writing file...')
    makeWavFile(wave, filename)
    vprint('Synth complete!')

def waveGenII(freq, length, instruments):
    """waveGenII
        (converts instruments to instrument)"""
    from wavmaker import waveGen
    if instruments:
        wave = waveGen(freq, length, instruments[0])
        del(instruments[0])
        return wave
    return waveGen(freq, length, '')


wavmaker.py

Code: Select all

# Create a WAV file from a wave
# Includes wave-making tools

from math import sin, pi
from itertools import izip_longest

# MIDI notes: 0 = lowest C, 48 is middle C

# Frequencies of notes
#           C        C#       D        D#       E        F        F#       G        G#       A        A#       B
freq = (  16.35,   17.32,   18.35,   19.45,   20.60,   21.83,   23.12,   24.50,   25.96,   27.50,   29.14,   30.87, # 0
          32.70,   34.65,   36.71,   38.89,   41.20,   43.65,   46.25,   49.00,   51.91,   55.00,   58.27,   61.74, # 1
          65.41,   69.30,   73.42,   77.78,   82.41,   87.31,   92.50,   98.00,  103.83,  110.00,  116.54,  123.47, # 2
         130.81,  138.59,  146.83,  155.56,  164.81,  174.61,  185.00,  196.00,  207.65,  220.00,  233.08,  246.94, # 3
         261.63,  277.18,  293.66,  311.13,  329.63,  349.23,  369.99,  392.00,  415.30,  440.00,  466.16,  493.88, # 4 (Middle C)
         523.25,  554.37,  587.33,  622.25,  659.26,  698.46,  739.99,  783.99,  830.61,  880.00,  932.33,  987.77, # 5
        1046.50, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760.00, 1864.66, 1975.53, # 6
        2093.00, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44, 3520.00, 3729.31, 3951.07, # 7
        4186.01, 4434.92, 4698.64, 4978.03)                                                                         # 8

DEFAULT_SAMPLE_RATE = 22050 # 22 kHz sample rate
#peaks = 0 # number of peaks counted

def sineWave(freq, length, sampleRate=DEFAULT_SAMPLE_RATE):
    """sineWave
        freq - the frequency of the wave
        length - the length to play the wave for in milliseconds
        sampleRate - the sample rate
        returns a string representing an 8 bit mono sine wave"""
    C = 2 * pi * freq / sampleRate # a constant to multiply i to get the sine
    return ''.join([chr(0x80+int(0x7F*sin(i*C))) for i in range(0,sampleRate*length//1000)])

def squareWave(freq, length, sampleRate=DEFAULT_SAMPLE_RATE):
    """squareWave
        freq - the frequency of the wave
        length - the length to play the wave for in milliseconds
        sampleRate - the sample rate
        returns a string representing an 8 bit mono square wave"""
    wavelength = freq / sampleRate # the wavelength
    return ''.join([(i * wavelength % 1 < .5 and '\xFF' or '\x00') for i in range(0,sampleRate*length//1000)])

def sawtoothWave(freq, length, sampleRate=DEFAULT_SAMPLE_RATE):
    """sawtoothWave
        freq - the frequency of the wave
        length - the length to play the wave for in milliseconds
        sampleRate - the sample rate
        returns a string representing an 8 bit mono sawtooth wave"""
    wavelength = freq / sampleRate # the wavelength
    return ''.join([chr(int(0xFF * (i * wavelength % 1))) for i in range(0,sampleRate*length//1000)])

def triangleWave(freq, length, sampleRate=DEFAULT_SAMPLE_RATE):
    """triangleWave
        freq - the frequency of the wave
        length - the length to play the wave for in milliseconds
        sampleRate - the sample rate
        returns a string representing an 8 bit mono triangle wave"""
    wavelength = freq / sampleRate # the wavelength
    return ''.join([chr(0x80+int(0x7F*tsin(i*wavelength))) for i in range(0,sampleRate*length//1000)])

WAVES = {'square':squareWave, 'triangle':triangleWave, 'sine':sineWave, 'sawtooth':sawtoothWave}

def waveGen(freq, length, waveType, sampleRate=DEFAULT_SAMPLE_RATE):
    """waveGen
        freq - the frequency of the wave
        length - the length to play the wave for in milliseconds
        waveType - what kind of wave to make. This is a string.
        sampleRate - the sample rate
        returns a string representing an 8 bit mono wave"""
    if WAVES.has_key(waveType):
        return WAVES[waveType](freq, length, sampleRate)
    else:
        return squareWave(freq, length, sampleRate)

def tsin(n):
    """tsin
        n - number
        returns the triangular sine of n"""
    x = n % 1
    if x <= .25:
        return x
    if x <= .75:
        return .5 - x
    return x - 1

def schr(n):
    """schr
        n - a number
        returns a character, prints a warning if the wave peaks"""
    if (n > 0xFF):
    #    peaks += 1
        return '\xFF'
    elif (n < 0x00):
    #    peaks += 1
        return '\x00'
    return chr(n)

def mergeWaves(wave1,wave2):
    """mergeWaves - merge two waves together
        wave1 - a wave string
        wave2 - a wave string
        returns a new wave string that is both combined"""
    li=[schr(ord(a)+ord(b)-0x80) for (a,b) in izip_longest(wave1,wave2,fillvalue='\x80')]
    #if peaks:
    #    print 'Peaking for %f seconds on merge!'%(peaks/sampleRate)
    #    peaks = 0
    return ''.join(li)

def divide(wave, n):
    """divide
        wave - a wave string
        n    - a number to divide by
        returns a new wave string"""
    return ''.join([chr((ord(c)-0x80)//n+0x80) for c in wave])

def littleEndian(n, bytes):
    """littleEndian
        n - the number to convert
        bytes - the number of bytes long this should ultimately be
        returns a string representing n in Little Endian format"""
    li=[]
    for i in range(0, bytes):
        li.append(chr(n % 0x100))
        n = n // 0x100
    return ''.join(li)

def makeWavFile(wave,filename,sampleRate=DEFAULT_SAMPLE_RATE):
    """makeWave
        wave - the wave to put into the file
        filename - the name of the file to open
        returns None, creates an 8 bit mono .wav file"""
    try:
        # The following information is heavily based on Wikipedia and
        # http://technology.niagarac.on.ca/courses/ctec1631/WavFileFormat.html
        # https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
        # http://www.sonicspot.com/guide/wavefiles.html
        f = open(filename, 'wb')
        try:
            length=len(wave)
            # RIFF chunk
            f.write('RIFF%sWAVE'%littleEndian(length + 36, 4))
            # FORMAT chunk
            #            (FORMAT  Length)( PCM  )( MONO )    (8bMONO)(8  BIT)
            f.write('fmt \x10\x00\x00\x00\x01\x00\x01\x00%s%s\x01\x00\x08\x00'%(littleEndian(sampleRate, 4),littleEndian(sampleRate, 4)))
            # DATA chunk
            f.write('data%s'%littleEndian(length, 4))
            f.write(wave)
        except IOError:
            print "There was an error writing the file"
        finally:
            f.close()
    except IOError:
        print "Could not open %s for writing"%filename


I'll probably rewrite this in a more object oriented way later. For now, this works well enough to compose the 5 minute piece I've been looking for.
Shameless Website Promotion: Gamma Energy
My new esoteric programming language: GLOBOL
An experiment to mess with Google Search results: HARDCORE PORNOGRAPHY HARDCORE PORNOGRAPHY

User avatar
phlip
Restorer of Worlds
Posts: 7543
Joined: Sat Sep 23, 2006 3:56 am UTC
Location: Australia
Contact:

Re: Playing tones in Python

Postby phlip » Thu Nov 26, 2009 11:38 pm UTC

Looking just at your .wav file generation code... you might want to take a look at the struct module... would save you from writing that "littleEndian" function... you could replace things like:

Code: Select all

'fmt \x10\x00\x00\x00\x01\x00\x01\x00%s%s\x01\x00\x08\x00'%(littleEndian(sampleRate, 4),littleEndian(sampleRate,4)
with:

Code: Select all

struct.pack('<4slhhllhh', 'fmt ', 0x10, 0x01, 0x01, sampleRate, sampleRate, 0x01, 0x08)


Maybe a helper function like:

Code: Select all

def riff_chunk(name, data):
        return struct.pack("<4sl", name, len(data)) + data + ('\x00' if len(data) % 2 == 1 else '')
could make it more readable.

Code: Select all

enum ಠ_ಠ {°□°╰=1, °Д°╰, ಠ益ಠ╰};
void ┻━┻︵​╰(ಠ_ಠ ⚠) {exit((int)⚠);}
[he/him/his]

User avatar
darkspork
Posts: 532
Joined: Tue Sep 23, 2008 12:43 am UTC
Location: Land of Trains and Suburbs

Re: Playing tones in Python

Postby darkspork » Thu Dec 10, 2009 1:32 am UTC

Well, I've got it up and running... here's something it wrote in about five seconds (WARNING:LOUD)
Shameless Website Promotion: Gamma Energy
My new esoteric programming language: GLOBOL
An experiment to mess with Google Search results: HARDCORE PORNOGRAPHY HARDCORE PORNOGRAPHY

User avatar
Squid Tamer
Posts: 220
Joined: Fri Apr 03, 2009 3:59 am UTC
Location: Over there
Contact:

Re: Playing tones in Python

Postby Squid Tamer » Thu Dec 10, 2009 11:15 pm UTC

darkspork wrote:Well, I've got it up and running... here's something it wrote in about five seconds (WARNING:LOUD)


That's amazing! It actually sounds really good.
Also reminds me of old 8 bit video games.

void_ptr
Posts: 1
Joined: Mon Jul 30, 2012 2:33 pm UTC

Re: Playing tones in Python

Postby void_ptr » Mon Jul 30, 2012 2:41 pm UTC

darkspork wrote:Well, I've got it up and running... here's (http://home.adelphi.edu/~cd17347/publish/misc/artificial_symphony.mp3)something it wrote in about five seconds (WARNING:LOUD)


Did the source code for this ever get put up? I'm working on something similar.

User avatar
Dropzone
Posts: 405
Joined: Sun Dec 30, 2007 10:12 pm UTC
Location: North Lincs., UK

Re: Playing tones in Python

Postby Dropzone » Tue Jul 31, 2012 7:35 pm UTC

That reminds me, I wrote some code for this thread, but never posted it for some reason. It's a Java utility class to allow easy creation of MIDI sequences, and an application that uses it to create MIDI from a text file of the format shown in the OP. I've attached the source code - bear in mind that it's pretty old and I haven't cleaned it up much, so the quality probably isn't that great. If anyone wants to do anything with it, consider it to be in the public domain. Here's what it produces when fed the data in the OP: http://dropzone.nfshost.com/music.mid
Attachments
music.zip
(3.38 KiB) Downloaded 150 times

Derek
Posts: 2148
Joined: Wed Aug 18, 2010 4:15 am UTC

Re: Playing tones in Python

Postby Derek » Wed Aug 01, 2012 3:48 am UTC

darkspork wrote:Well, I've got it up and running... here's something it wrote in about five seconds (WARNING:LOUD)

Wow, I can totally imagine that as the theme for some RPG.

hoppypress
Posts: 9
Joined: Thu Aug 02, 2012 6:26 pm UTC

Re: Playing tones in Python

Postby hoppypress » Thu Aug 02, 2012 6:35 pm UTC

If you Google "Music for Geeks and Nerds" (I can't post links ATM) someone wrote a book on generating music with Python (with a little bit of math mixed in). I believe the book was recently published - I saw it on the front page of Hacker News a few days ago and was warmly received. Hope that helps!

- Paul


Return to “Coding”

Who is online

Users browsing this forum: No registered users and 5 guests