3

I'm currently working on a music-playing program on Python 3. I managed to get it to play single notes with the following code (which was ripped of pitch perfect 0.3.2):

import pygame, time
import numpy as np

notes_dct = {
        'c': -9.0, 'c#': -8.0, 'db': -8.0, 'd': -7.0, 'd#': -6.0, 'eb': -6.0,
        'e': -5.0, 'f': -4.0, 'f#': -3.0, 'gb': -3.0, 'g': -2.0, 'g#': -1.0,
        'ab': -1.0, 'a': 0.0, 'a#': 1.0, 'bb': 1.0, 'b': 2.0,
        }

def getExponent(note):
    """ Returns a float needed to obtain the frequency in Hz based on
        'note', which is a string with note name defaulting to 'A', and
        an optional trailing octave value, defaulting to 4; each octave
        begins at the C tone.

        Examples:
            # np is short for numpy
            GetExponent('A4') returns a value 'v' where
                2 ** (np.log2(440) + v) == 440.0  # in Hz

            GetExponent('C') (or C4) returns v where
                2 ** (np.log2(440) + v) == 261.6  # approximate;
                                                  # note that C4 is below A4

            GetExponent('Gb-1') (or G flat, octave -1) returns v where
                2 ** (np.log2(440) + v) == 11.6  # below usual hearing range
    """

    i = 0
    while i < len(note) and note[i] not in '1234567890-':
        i += 1

    if i == 0:
        name = 'a'
    else:
        name = note[: i].lower()

    if i == len(note):
        octave = 4
    else:
        octave = int(note[i: ])

    return notes_dct[name] / 12.0 + octave - 4


def generateTone(freq=440.0, vol=1.0, shape='sine'):
    """ GenerateTone( shape='sine', freq=440.0, vol=1.0 )
            returns pygame.mixer.Sound object

        shape:  string designating waveform type returned; one of
                'sine', 'sawtooth', or 'square'
        freq:  frequency; can be passed in as int or float (in Hz),
               or a string (see GetExponent documentation above for
               string usage)
        vol:  relative volume of returned sound; will be clipped into
              range 0.0 to 1.0
    """

    # Get playback values that mixer was initialized with.
    (pb_freq, pb_bits, pb_chns) = pygame.mixer.get_init()

    if type(freq) == str:
        # Set freq to frequency in Hz; GetExponent(freq) is exponential
        # difference from the exponent of note A4: log2(440.0).
        freq = 2.0 ** (np.log2(440.0) + getExponent(freq))

    # Clip range of volume.
    vol = np.clip(vol, 0.0, 1.0)

    # multiplier and length pan out the size of the sample to help
    # keep the mixer busy between calls to channel.queue()
    multiplier = int(freq / 24.0)
    length = max(1, int(float(pb_freq) / freq * multiplier))
    # Create a one-dimensional array with linear values.
    lin = np.linspace(0.0, multiplier, num=length, endpoint=False)
    if shape == 'sine':
        # Apply a sine wave to lin.
        ary = np.sin(lin * 2.0 * np.pi)
    elif shape == 'sawtooth':
        # sawtooth keeps the linear shape in a modded fashion.
        ary = 2.0 * ((lin + 0.5) % 1.0) - 1.0
    elif shape == 'square':
        # round off lin and adjust to alternate between -1 and +1.
        ary = 1.0 - np.round(lin % 1.0) * 2.0
    else:
        print("shape param should be one of 'sine', 'sawtooth', 'square'.")
        print()
        return None

    # If mixer is in stereo mode, double up the array information for
    # each channel.
    if pb_chns == 2:
        ary = np.repeat(ary[..., np.newaxis], 2, axis=1)

    if pb_bits == 8:
        # Adjust for volume and 8-bit range.
        snd_ary = ary * vol * 127.0
        return pygame.sndarray.make_sound(snd_ary.astype(np.uint8) + 128)
    elif pb_bits == -16:
        # Adjust for 16-bit range.
        snd_ary = ary * vol * float((1 << 15) - 1)
        return pygame.sndarray.make_sound(snd_ary.astype(np.int16))
    else:
        print("pygame.mixer playback bit-size unsupported.")
        print("Should be either 8 or -16.")
        print()
        return None

but I'm having problems making the program play multiple notes at once (a chord). Running several generateTones at once is a memorable experience, and so I found this snippet of code on the web:

import math
import wave
import struct
def synthComplex(freq=[440],coef=[1], datasize=10000, fname="test.wav"):
    frate = 44100.00  
    amp=8000.0 
    sine_list=[]
    for x in range(datasize):
        samp = 0
        for k in range(len(freq)):
            samp = samp + coef[k] * math.sin(2*math.pi*freq[k]*(x/frate))
        sine_list.append(samp)
    wav_file=wave.open(fname,"w")
    nchannels = 1
    sampwidth = 2
    framerate = int(frate)
    nframes=datasize
    comptype= "NONE"
    compname= "not compressed"
    wav_file.setparams((nchannels, sampwidth, framerate, nframes, comptype, compname))
    for s in sine_list:
        wav_file.writeframes(struct.pack('h', int(s*amp/2)))
    wav_file.close()

and then I can play the sound file with winsound or pygame. However, it takes about a second to compile the sound file (which is way too long for me), and making several thousand pre-made sound files seems rather ineffecient.

Is there an easy way for me to solve this problem?

Thanks in advance for any help!

EDIT:

I tried doing something like this:

pygame.mixer.init(frequency=22050,size=-16,channels=4)
chan1 = pygame.mixer.Channel(0)
chan1.play(generateTone('C4'), 10)
chan2 = pygame.mixer.Channel(1)
chan2.play(generateTone('G5'), 10)

but that had the same effect as playing simply using:

generateTone('C4').play(10)
generateTone('G5').play(10)

Changing chan1.play to chan1.queue or changing chan1 = pygame.mixer.Channel(0) to chan1 = pygame.mixer.find_channel() didn't change anything.

4
  • 3
    Just wanted to say this is pretty cool. Commented Mar 12, 2016 at 3:27
  • @idjaw Thanks, but most of this is not my code >:/ Commented Mar 12, 2016 at 3:29
  • 1
    Make sure that when you're playing multiple sounds on pygame, each sound is on its on channel. A channel can only support one note at a time. Commented Mar 12, 2016 at 18:58
  • @NeonWizard Let's say my sounds are in the form of pygame.sndarray. How would I play one of them on a channel? (Sorry, I'm rather new to the audio part of pygame). Commented Mar 12, 2016 at 19:56

1 Answer 1

1

I managed to solve the problem with the pygame.midi module:

import pygame.midi
import time

pygame.midi.init()
player = pygame.midi.Output(0)
player.set_instrument(0)
player.note_on(60, 127)
player.note_on(64, 127)
player.note_on(67, 127)
time.sleep(1)
player.note_off(60, 127)
player.note_off(64, 127)
player.note_off(67, 127)
del player
pygame.midi.quit()
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.