[Tutor] Musical note on python

eryksun eryksun at gmail.com
Thu Sep 13 21:03:45 CEST 2012


On Thu, Sep 13, 2012 at 11:48 AM, D.V.N.Sarma డి.వి.ఎన్.శర్మ
<dvnsarma at gmail.com> wrote:
>
> As far as programming volume is concerned winsound.Beep has only frequency
> and duration. pyaudio module appears to have provision for volume control.

You should be able to add a wave header to a raw byte string. For example:


    import wave
    import winsound
    from cStringIO import StringIO

    def get_wave(data):
        f = StringIO()
        w = wave.open(f, 'w')
        w.setnchannels(1) # mono
        w.setsampwidth(2) # 2 bytes
        w.setframerate(48000) # samples/second
        w.writeframes(data)
        return f.getvalue()


Then play the sound like this (_untested_):


    wave_data = get_wave(data)
    windsound.PlaySound(wave_data, winsound.SND_MEMORY)



I've modified my previous script to add simple polyphonic sound. I
included a basic square wave and a sawtooth wave, plus a random
polyphonic wave. It's the kind of sound you might hear in a cheesy
greeting card. Also, since you're looking to use winsound.PlaySound(),
I changed the byte packing to use a little endian signed short
(int16). I think that's what Windows wave files use.

My demo below still uses pyaudio, which wraps the cross-platform
PortAudio library. To me it seems like a better solution than relying
on the standard library (e.g. the standard lib uses OSS on Linux; I
don't even have that installed).

Hopefully it performs OK on your computer. Using NumPy arrays would
speed up the number crunching.


    import pyaudio
    from math import sin, pi
    from struct import pack
    from random import sample, randrange

    def polywave(freqs, amps, f0, fs):
        """create a polyphonic waveform

        freqs: sequence of frequencies (Hz)
        amps: sequence of amplitudes
        f0: fundamental frequency (Hz)
        fs: samples/second

        output is normalized to the range [-1,1].
        """
        N = int(fs / f0)
        rad_step = 2 * pi / fs  # radians / (Hz * sample)
        wave = [0.0] * N
        for f, a in zip(freqs, amps):
            for n in xrange(N):
                wave[n] += a * sin(rad_step * f * n)
        maxamp = abs(max(wave, key=lambda x: abs(x)))
        return [samp / maxamp for samp in wave]

    def packwave(wave, vol=1, duration=None, fs=None):
        """pack a waveform as bytes (int16)

        wave: sample sequence, |mag| <= 1
        vol: optional volume scale, |mag| <= 1
        duration: optional loop time (seconds)
        fs: samples/second

        fs is required to set duration.
        """
        scale = min(vol * 32767, 32767)
        ncycles = 1
        if not (duration is None or fs is None):
            ncycles = int(round(1.0 * duration * fs / len(wave)))
        data = b''.join(pack('<h', scale * samp) for samp in wave)
        return data * ncycles

    def make_square(num, f0, fs):
        """generate square wave components

        num: number of harmonics
        f0: funamental frequency (Hz)
        fs: samples/second

        fs/2 is the upper bound.
        """
        stop = min(2*num, int(fs / (2 * f0)))
        freqs = [n * f0 for n in xrange(1, stop, 2)]
        amps = [1.0 / n for n in xrange(1, stop, 2)]
        return freqs, amps

    def make_saw(num, f0, fs):
        """generate sawtooth wave components

        num: number of harmonics
        f0: funamental frequency (Hz)
        fs: samples/second

        fs/2 is the upper bound.
        """
        stop = min(num + 1, int(fs / (2 * f0)))
        freqs = [n * f0 for n in xrange(1, stop)]
        amps = [(-1.0) ** (n + 1) / n for n in xrange(1, stop)]
        return freqs, amps

    def make_rand(num, f0, fs):
        """generate wave with random harmonics/amplitudes"""
        ftop = min(fs // 2, 12000)
        nmax = int(ftop / f0)
        num = min(num, nmax)
        freqs = [n * f0 for n in sample(xrange(1, nmax+1), num)]
        amps = [randrange(32768)/32767.0 for n in xrange(num)]
        return freqs, amps

    def play(data, stream):
        chunks = (data[i:i+1024] for i in xrange(0, len(data), 1024))
        for chunk in chunks:
            stream.write(chunk)

    if __name__ == "__main__":
        from time import sleep

        fs = 48000

        p = pyaudio.PyAudio()
        stream = p.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=fs,
            frames_per_buffer=fs//4,
            output=True)

        # http://en.wikipedia.org/wiki/
        # Equal_temperament#Calculating_absolute_frequencies

        note = lambda n: 440 * (2**(1/12.)) ** (-21 + n)

        scale = [note(n) for n in range(12)]
        rscale = [note(13-n) for n in range(12)]
        vols = [0.2 + 0.05*n for n in range(len(scale))]
        rvols = list(reversed(vols))

        def play_scale(scale, vols, wave_func, master_vol=1):
            duration = 0.5
            nharmonics = 30
            for f0, vol in zip(scale, vols):
                freqs, amps = wave_func(nharmonics, f0, fs)
                wave = polywave(freqs, amps, f0, fs)
                data = packwave(wave, master_vol * vol, duration, fs)
                play(data, stream)
            sleep(0.5)

        play_scale(scale, vols, make_square, 0.5)
        play_scale(rscale, rvols, make_saw, 0.5)
        play_scale(scale, vols, make_rand, 0.75)
        play_scale(rscale, rvols, make_rand, 0.75)

        stream.close()
        p.terminate()


More information about the Tutor mailing list