5

My son and I are trying to write a program that will allow a user to input a sequence of musical notes, and save them into a list to be played back. We've come up with the following:

import math        #import needed modules
import pyaudio     #sudo apt-get install python-pyaudio

def playnote(char):
    octave = int(char[1])
    if char[0] == 'c':
        frequency = 16.35*octave+1
    elif char[0] =='C':
        frequency = 17.32*octave+1
    elif char[0] =='d':
        frequency = 18.35*octave+1
    elif char[0] == 'D':
        frequency = 19.45*octave+1
    elif char[0] =='e':
        frequency = 20.6*octave+1
    elif char[0] == 'f':
        frequency = 21.83*octave+1
    elif char[0] =='F':
        frequency = 23.12*octave+1
    elif char[0] == 'g':
        frequency = 24.5*octave+1
    elif char[0] == 'G':
        frequency = 25.96*octave+1
    elif char[0] == 'a':
        frequency = 27.5*octave+1
    elif char[0] == 'A':
        frequency = 29.14*octave+1
    elif char[0] == 'b':
        frequency = 30.87*octave+1
    elif char[0] == 'p':
        del song[-1]

    PyAudio = pyaudio.PyAudio     #initialize pyaudio

    #See https://en.wikipedia.org/wiki/Bit_rate#Audio
    bitrate = 256000     #number of frames per second/frameset.      

#    frequency = 220     #Hz, waves per second, 261.63=C4-note.
    LENGTH = 1     #seconds to play sound

    if frequency > bitrate:
        bitrate = frequency+100

    frames = int(bitrate * LENGTH)
#    RESTFRAMES = frames % bitrate
    wavedata = ''    

    #generating waves
    for x in range(frames):
     wavedata = wavedata+chr(int(math.sin(x/((bitrate/frequency)/math.pi))*127+128))    

#    for x in range(RESTFRAMES): 
#     wavedata = wavedata+chr(128)

    p = PyAudio()
    stream = p.open(format = p.get_format_from_width(1), 
                    channels = 1, 
                    rate = bitrate, 
                    output = True)

    stream.write(wavedata)
    stream.stop_stream()
    stream.close()
    p.terminate()

song = []
while True:
    try:
        note = str(input('''Enter note (A-G) (capital for sharp)
        and an octave (0-8) or any other key to play: '''))
        playnote(note)
        song.append(note)


    except:
        for note in song:
            playnote(note)
        break

It works fine as a starting point, but the notes don't sound very 'musical'.

From here, we're wondering:

  1. are there any existing Python scripts or modules which do something similar?

  2. is there a way to modify the waveforms to emulate different instruments?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Jim421616
  • 1,434
  • 3
  • 22
  • 47

4 Answers4

4

If you are looking for other work for producing music using Python, you might find the following program be a helpful inspiration. It uses the winsound module on Windows to produce beeps of a certain duration and frequency. The program shown below is old and not maintained -- an experiment really but may give you and your son some ideas for further related work.

#! /usr/bin/env python3
import msvcrt
import random
import time
import winsound

# CONSTANTS
KEYS = 'zsxcfvgbnjmk,l./\'q2we4r5t6yu8i9op-[=]'
A4 = 440
AUTHOR = '.\',zb'
NEW_SONG = ('vm',
            'zv',
            'cn',
            'vm',
            'xb',
            'cn',
            'zv')

# CONFIGURATION
NS_SP = 1
SPEED = 5
HOLD_RATIO = 0.95
TRANSPOSE = 0
PAUSE_TIME = 2
SHOW_FREQU = False

# RANDOM
NEIGHBOR_RATIO = 0.9
ODD_RATIO = 0.05
SWITCH_RATIO = 0.01
WHITE_KEYS = 'zxcvbnm,./qwertyuiop[]'
BLACK_KEYS = 'sfgjkl\'245689-='

EXAMPLE_SONG_1 = [('x', 1),
                  ('x', 2),
                  ('x', 1),
                  ('x', 1),
                  ('f', 1),
                  ('g', 1),
                  ('b', 2),
                  ('b', 1),
                  ('g', 2),
                  ('x', 1),
                  ('k', 2),
                  ('k', 1),
                  ('j', 2),
                  ('g', 1),
                  ('f', 5),
                  ('x', 1),
                  ('k', 2),
                  ('k', 1),
                  ('l', 1),
                  ('.', 1),
                  ("'", 1),
                  ('j', 2),
                  ('j', 1),
                  ('g', 2),
                  ('g', 1),
                  ('b', 2),
                  ('g', 1),
                  ('f', 1),
                  ('x', 1),
                  ('f', 1),
                  ('x', 5)]

EXAMPLE_SONG_2 = [('j', 2),
                  ('j', 1),
                  ('j', 2),
                  ('.', 1),
                  ('b', 2),
                  ('j', 1),
                  ('b', 1),
                  ('g', 1.5),
                  ('f', 0.5),
                  ('g', 2),
                  ('g', 1),
                  ('g', 1),
                  ('f', 1),
                  ('x', 1),
                  ('f', 6),
                  ('j', 2),
                  ('j', 1),
                  ('j', 2),
                  ('.', 1),
                  ('b', 2),
                  ('j', 1),
                  ('b', 1),
                  ('g', 1.5),
                  ('f', 0.5),
                  ('g', 2),
                  ('g', 1),
                  ('f', 1),
                  ('x', 1),
                  ('f', 1),
                  ('x', 5),
                  ('x', 1),
                  ('k', 2),
                  ('k', 1),
                  ('l', 3),
                  ('l', 2),
                  ("'", 1),
                  ('.', 2),
                  ('.', 1),
                  ('.', 2),
                  ('.', 1),
                  ('2', 1),
                  ("'", 1),
                  ('.', 1),
                  ('j', 6),
                  ('j', 2),
                  ('j', 1),
                  ('j', 2),
                  ('.', 1),
                  ('b', 2),
                  ('j', 1),
                  ('b', 1),
                  ('g', 1.5),
                  ('f', 0.5),
                  ('g', 2),
                  ('g', 1),
                  ('f', 1),
                  ('x', 1),
                  ('f', 1),
                  ('x', 6)]

EXAMPLE_SONG_3 = [(' ', 1),
                  ('x', 0.5),
                  ('f', 0.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('j', 1),
                  ('.', 3),
                  ('.', 0.5),
                  ('l', 0.5),
                  ('k', 0.5),
                  ('l', 0.5),
                  ('j', 3),
                  (' ', 1),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('b', 0.5),
                  ('k', 0.5),
                  ('j', 1),
                  ('x', 3),
                  ('f', 0.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('f', 3),
                  (' ', 1),
                  ('x', 0.5),
                  ('f', 0.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('j', 1),
                  ('.', 3),
                  ('.', 0.5),
                  ('l', 0.5),
                  ('k', 0.5),
                  ('l', 0.5),
                  ('j', 3),
                  (' ', 1),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('b', 0.5),
                  ('k', 0.5),
                  ('j', 1),
                  ('x', 2.5),
                  ('x', 0.5),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('f', 0.5),
                  ('g', 0.5),
                  ('x', 3),
                  ('z', 0.5),
                  ('x', 0.5),
                  ('f', 0.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('j', 0.5),
                  ('k', 1),
                  ('.', 0.5),
                  ('l', 0.5),
                  ('k', 0.5),
                  ('l', 0.5),
                  ('.', 1),
                  ('k', 2),
                  (' ', 1),
                  ('l', 0.5),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('k', 0.5),
                  ('l', 3),
                  (' ', 1),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('b', 0.5),
                  ('j', 0.5),
                  ('k', 1),
                  ('b', 2),
                  (' ', 1),
                  ('j', 0.5),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('j', 3),
                  (' ', 1),
                  ('.', 0.5),
                  ('l', 0.5),
                  ('k', 0.5),
                  ('l', 0.5),
                  ('.', 1),
                  ('k', 2),
                  ("'", 0.5),
                  ('.', 0.5),
                  ('l', 0.5),
                  ('.', 0.5),
                  ('j', 3),
                  (' ', 1),
                  ("'", 0.5),
                  ('.', 0.5),
                  ('l', 0.5),
                  ('.', 0.5),
                  ('j', 3),
                  (' ', 1),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('b', 0.5),
                  ('k', 0.5),
                  ('j', 1),
                  ('x', 2),
                  (' ', 1),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('f', 0.5),
                  ('g', 0.5),
                  ('x', 3)]

EXAMPLE_SONG_4 = [('j', 1.5),
                  ('j', 0.5),
                  ('j', 0.75),
                  ('b', 0.25),
                  ('g', 0.75),
                  ('b', 0.25),
                  ('j', 1),
                  ('k', 1),
                  ('j', 2),
                  ('l', 1.5),
                  ('l', 0.5),
                  ('l', 0.75),
                  ('k', 0.25),
                  ('j', 0.75),
                  ('b', 0.25),
                  ('g', 1),
                  ('k', 1),
                  ('j', 2),
                  ('j', 1.5),
                  ('j', 0.5),
                  ('j', 0.75),
                  ('b', 0.25),
                  ('g', 0.75),
                  ('b', 0.25),
                  ('j', 1),
                  ('k', 1),
                  ('j', 1),
                  ('.', 1),
                  ("'", 2),
                  ('l', 2),
                  ('.', 4),
                  ('.', 1.5),
                  ('l', 0.5),
                  ('.', 0.75),
                  ('l', 0.25),
                  ('.', 0.75),
                  ('k', 0.25),
                  ('k', 1),
                  ('j', 1),
                  ('j', 2),
                  ('l', 1.5),
                  ('k', 0.5),
                  ('l', 0.75),
                  ('k', 0.25),
                  ('l', 0.75),
                  ('k', 0.25),
                  ('j', 1),
                  ('.', 1),
                  ('.', 2),
                  ('.', 1.5),
                  ('l', 0.5),
                  ('.', 0.75),
                  ('l', 0.25),
                  ('.', 0.75),
                  ('k', 0.25),
                  ('k', 1),
                  ('j', 1),
                  ('j', 1),
                  ('.', 1),
                  ("'", 2),
                  ('l', 2),
                  ('.', 4)]

EXAMPLE_SONG_5 = [('g', 0.5),
                  ('g', 0.5),
                  ('g', 0.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('b', 0.5),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('f', 0.5),
                  ('f', 0.5),
                  ('j', 0.5),
                  ('f', 0.5),
                  ('g', 2),
                  ('b', 0.5),
                  ('b', 0.5),
                  ('j', 0.5),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('x', 0.5),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('f', 4),
                  ('g', 0.5),
                  ('g', 0.5),
                  ('g', 0.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('g', 0.5),
                  ('g', 0.5),
                  ('k', 0.5),
                  ('k', 0.5),
                  ('l', 0.5),
                  ('k', 0.5),
                  ('g', 1),
                  ('g', 1),
                  ('b', 0.5),
                  ('b', 0.5),
                  ('j', 0.5),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('x', 0.5),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('f', 5),
                  ('j', 1),
                  ('k', 1),
                  ('l', 1),
                  ('l', 0.5),
                  ('.', 0.5),
                  ('.', 0.5),
                  ('j', 0.5),
                  ('j', 1.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('x', 0.5),
                  ('f', 0.5),
                  ('g', 1.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('b', 0.5),
                  ('j', 0.5),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('.', 0.5),
                  ('.', 0.5),
                  ('l', 0.5),
                  ('k', 0.5),
                  ('k', 0.5),
                  ('l', 0.5),
                  ('.', 0.5),
                  ("'", 1.5),
                  ("'", 0.5),
                  ('2', 0.5),
                  ('2', 0.5),
                  ('2', 0.5),
                  ("'", 0.5),
                  ("'", 0.5),
                  ('.', 0.5),
                  ('l', 0.5),
                  ('.', 0.5),
                  ('k', 0.5),
                  ('k', 0.5),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('b', 1.5),
                  ('f', 0.5),
                  ('g', 0.5),
                  ('j', 0.5),
                  ('j', 0.5),
                  ('g', 0.5),
                  ('b', 0.5),
                  ('k', 0.5),
                  ('k', 0.5),
                  ('j', 0.5),
                  ('k', 2),
                  ('l', 2),
                  ('.', 4)]

EXAMPLE_SONG_6 = [('j', 2),
                  ('k', 1),
                  ('j', 2),
                  ('j', 1),
                  ('b', 2),
                  ('g', 1),
                  ('b', 0.5),
                  ('g', 0.5),
                  ('f', 2),
                  ('g', 2),
                  ('j', 1),
                  ('.', 2),
                  ('j', 1),
                  ('b', 2),
                  ('f', 1),
                  ('j', 3),
                  ('j', 2),
                  ('k', 1),
                  ('j', 2),
                  ('j', 1),
                  ('k', 2),
                  ('l', 1),
                  ('.', 1),
                  ('k', 2),
                  ('j', 2),
                  ('g', 1),
                  ('x', 2),
                  ('g', 1),
                  ('f', 2),
                  ('x', 1),
                  ('x', 3),
                  ('.', 2),
                  ("'", 1),
                  ('.', 2),
                  ('l', 1),
                  ('.', 2),
                  ("'", 1),
                  ('.', 1),
                  ('k', 2),
                  ('j', 2),
                  ('.', 1),
                  ('2', 2),
                  ('.', 1),
                  ("'", 2),
                  ('k', 1),
                  ('j', 3),
                  ('j', 1),
                  ('k', 1),
                  ('l', 1),
                  ('.', 2),
                  ('l', 1),
                  ('k', 2),
                  ('j', 1),
                  ('j', 1),
                  ('g', 2),
                  ('g', 2),
                  ('j', 1),
                  ('x', 2),
                  ('g', 1),
                  ('f', 2),
                  ('x', 1),
                  ('x', 3)]

# These next few songs were contributed by Mike Sperry.
TWINKLE_TWINKLE = [('c', 1),
                   ('c', 1),
                   ('m', 1),
                   ('m', 1),
                   (',', 1),
                   (',', 1),
                   ('m', 2),
                   ('n', 1),
                   ('n', 1),
                   ('b', 1),
                   ('b', 1),
                   ('v', 1),
                   ('v', 1),
                   ('c', 2),
                   ('m', 1),
                   ('m', 1),
                   ('n', 1),
                   ('n', 1),
                   ('b', 1),
                   ('b', 1),
                   ('v', 2),
                   ('m', 1),
                   ('m', 1),
                   ('n', 1),
                   ('n', 1),
                   ('b', 1),
                   ('b', 1),
                   ('v', 2),
                   ('c', 1),
                   ('c', 1),
                   ('m', 1),
                   ('m', 1),
                   (',', 1),
                   (',', 1),
                   ('m', 2),
                   ('n', 1),
                   ('n', 1),
                   ('b', 1),
                   ('b', 1),
                   ('v', 1),
                   ('v', 1),
                   ('c', 2)]

ABCS = [('c', 1),
        ('c', 1),
        ('m', 1),
        ('m', 1),
        (',', 1),
        (',', 1),
        ('m', 2),
        ('n', 1),
        ('n', 1),
        ('b', 1),
        ('b', 1),
        ('v', 0.5),
        ('v', 0.5),
        ('v', 0.5),
        ('v', 0.5),
        ('c', 2),
        ('m', 1),
        ('m', 1),
        ('n', 2),
        ('b', 1),
        ('b', 1),
        ('v', 2),
        ('m', 1),
        ('m', 1),
        ('n', 2),
        ('b', 1),
        ('b', 1),
        ('v', 2),
        ('c', 1),
        ('c', 1),
        ('m', 1),
        ('m', 1),
        (',', 1),
        (',', 1),
        ('m', 2),
        ('n', 1),
        ('n', 1),
        ('b', 1),
        ('b', 1),
        ('v', 1),
        ('v', 1),
        ('c', 2)]

BAH_BAH_BLACK_SHEEP = [('c', 1),
                       ('c', 1),
                       ('m', 1),
                       ('m', 1),
                       (',', 0.5),
                       (',', 0.5),
                       (',', 0.5),
                       (',', 0.5),
                       ('m', 2),
                       ('n', 1),
                       ('n', 1),
                       ('b', 1),
                       ('b', 1),
                       ('v', 1),
                       ('v', 1),
                       ('c', 2),
                       ('m', 1),
                       ('m', 0.5),
                       ('m', 0.5),
                       ('n', 1),
                       ('n', 1),
                       ('b', 1),
                       ('b', 0.5),
                       ('b', 0.5),
                       ('v', 2),
                       ('m', 1),
                       ('m', 0.5),
                       ('m', 0.5),
                       ('n', 0.5),
                       ('n', 0.5),
                       ('n', 0.5),
                       ('n', 0.5),
                       ('b', 1),
                       ('b', 0.5),
                       ('b', 0.5),
                       ('v', 2),
                       ('c', 1),
                       ('c', 1),
                       ('m', 1),
                       ('m', 1),
                       (',', 0.5),
                       (',', 0.5),
                       (',', 0.5),
                       (',', 0.5),
                       ('m', 2),
                       ('n', 1),
                       ('n', 1),
                       ('b', 1),
                       ('b', 1),
                       ('v', 1),
                       ('v', 1),
                       ('c', 2)]

HAPPY_BIRTHDAY = [('m', 0.75),
                  ('m', 0.25),
                  (',', 1),
                  ('m', 1),
                  ('/', 1),
                  ('.', 2),
                  ('m', 0.75),
                  ('m', 0.25),
                  (',', 1),
                  ('m', 1),
                  ('q', 1),
                  ('/', 2),
                  ('m', 0.75),
                  ('m', 0.5),
                  ('r', 1),
                  ('w', 1),
                  ('/', 1),
                  ('.', 1),
                  (',', 1),
                  ('n', 0.75),
                  ('n', 0.25),
                  ('b', 1),
                  ('c', 1),
                  ('v', 1),
                  ('c', 3)]

# KNOWN MUSIC
SONGS = EXAMPLE_SONG_1, \
        EXAMPLE_SONG_2, \
        EXAMPLE_SONG_3, \
        EXAMPLE_SONG_4, \
        EXAMPLE_SONG_5, \
        EXAMPLE_SONG_6, \
        TWINKLE_TWINKLE, \
        ABCS, \
        BAH_BAH_BLACK_SHEEP, \
        HAPPY_BIRTHDAY

def main():
    print('''
MENU
====
(R)andom
(S)huffle
(P)lay
(K)eyboard
(A)uthor
(N)ew Song''')
    while True:
        key = msvcrt.getwch()
        if key in 'rspk': print()
        if key == 'r': menu_help(random.random)
        if key == 's': menu_help(random.shuffle)
        if key == 'p': select_song()
        if key == 'k': menu_help()
        if key == 'a': author()
        if key == 'n': new_song()

def new_song():
    while True:
        sig = 0
        for notes in NEW_SONG:
            sig *= 2
            for note in random.sample(notes, 2):
                try:
                    winsound.Beep(get_frequency(note), int(100 / float(NS_SP)))
                except:
                    pass
            if notes[1] == note:
                sig += 1
            time.sleep((1.0 / 30) / NS_SP)
        if not SHOW_FREQU:
            print(sig + 1)

def select_song():
    songs = (('EXAMPLE_SONG_1', EXAMPLE_SONG_1),
             ('EXAMPLE_SONG_2', EXAMPLE_SONG_2),
             ('EXAMPLE_SONG_3', EXAMPLE_SONG_3),
             ('EXAMPLE_SONG_4', EXAMPLE_SONG_4),
             ('EXAMPLE_SONG_5', EXAMPLE_SONG_5),
             ('EXAMPLE_SONG_6', EXAMPLE_SONG_6),
             ('TWINKLE_TWINKLE', TWINKLE_TWINKLE),
             ('ABCS', ABCS),
             ('BAH_BAH_BLACK_SHEEP', BAH_BAH_BLACK_SHEEP),
             ('HAPPY_BIRTHDAY', HAPPY_BIRTHDAY))
    for index, data in enumerate(songs):
        print('(%s) %s' % (index + 1, data[0].replace('_', ' ').lower().title()))
    while True:
        try:
            index = int(input('\nSelect: '))
            assert 0 < index <= len(songs)
            play(songs[index - 1][1])
        except:
            pass

def menu_help(score=None):
    if isinstance(score, list):
        play(score)
    elif score is random.random:
        play_random()
    elif score is random.shuffle:
        play_songs()
    keyboard()

def play(score):
    for key, duration in score:
        duration /= float(SPEED)
        bd = int(duration * HOLD_RATIO * 1000)
        sd = duration * (1 - HOLD_RATIO)
        try:
            winsound.Beep(get_frequency(key), bd)
        except:
            time.sleep(duration * HOLD_RATIO)
        time.sleep(sd)

def keyboard():
    while msvcrt.kbhit():
        msvcrt.getwch()
    while True:
        try:
            winsound.Beep(get_frequency(msvcrt.getwch()), 1000)
        except:
            pass

def get_frequency(key):
    assert key[0] in KEYS
    if SHOW_FREQU:
        frequ = int((A4 * 2 ** ((KEYS.find(key[0]) + key.count('+') - (0 if key[0] == '-' else key.count('-')) + TRANSPOSE) / 12.0)) + 0.5)
        print(frequ)
        return frequ
    else:
        print(key, end=' ')
        return int((A4 * 2 ** ((KEYS.find(key[0]) + key.count('+') - (0 if key[0] == '-' else key.count('-')) + TRANSPOSE) / 12.0)) + 0.5)

def play_random():
    key = 'c'
    RANDOM_KEYS = WHITE_KEYS
    while not msvcrt.kbhit():
        if random.random() < SWITCH_RATIO:
            if RANDOM_KEYS is WHITE_KEYS:
                RANDOM_KEYS = BLACK_KEYS
            else:
                RANDOM_KEYS = WHITE_KEYS
            key = RANDOM_KEYS[random.randrange(len(RANDOM_KEYS))]
        if random.random() < NEIGHBOR_RATIO:
            index = RANDOM_KEYS.index(key[0]) + key.count('+') - key.count('-') + random.randrange(2) * 2 - 1
            if index < 0:
                key = RANDOM_KEYS[0] + '-' * (index * -1)
            elif index >= len(RANDOM_KEYS):
                key = RANDOM_KEYS[-1] + '+' * (index - len(RANDOM_KEYS) + 1)
            else:
                key = RANDOM_KEYS[index]
        else:
            key = RANDOM_KEYS[random.randrange(len(RANDOM_KEYS))]
        if random.random() < ODD_RATIO:
            if random.randrange(2):
                key += '+'
            else:
                key += '-'
        neg = key.count('-')
        pos = key.count('+')
        trans = pos - neg
        if trans > 0:
            key = key[0] + '+' * trans
        elif trans < 0:
            key = key[0] + '-' * (trans * -1)
        else:
            key = key[0]
        winsound.Beep(get_frequency(key), 100)

def play_songs():
    songs = list(SONGS)
    while True:
        random.shuffle(songs)
        for song in songs:
            play(song)
            time.sleep(PAUSE_TIME)

def author():
    for note in AUTHOR:
        winsound.Beep(get_frequency(note), 1000)
    time.sleep(1)
    while msvcrt.kbhit():
        msvcrt.getwch()
    author = random.sample(AUTHOR, len(AUTHOR))
    while not msvcrt.kbhit():
        for note in author:
            winsound.Beep(get_frequency(note), 100)
        last_note = author[-1]
        author = random.sample(AUTHOR, len(AUTHOR))
        while author[0] == last_note:
            author = random.sample(AUTHOR, len(AUTHOR))

if __name__ == '__main__':
    main()
Noctis Skytower
  • 21,433
  • 16
  • 79
  • 117
  • 1
    Your answer is excellent. However, you could easily have used simple secular songs like Twinkle Twinkle Little Star to illustrate this post instead of religious instruction songs. –  Aug 11 '19 at 18:48
  • Edit submitted with songs familiar to the general public, and with a note-mapping key as well to make it easier for a user to write a new song. –  Aug 12 '19 at 02:53
  • For anyone why doesn't want to figure out the note mapping themselves, here's a dict with it `{'z': 'A4', 'x': 'B4', 'c': 'C4', 'v': 'D4', 'b': 'E4', 'n': 'F4', 'm': 'G4', ',': 'A5', '.': 'B5', '/': 'C5', 'q': 'D5', 'w': 'E5', 'e': 'F5', 'r': 'G5', 't': 'A6', 'y': 'B6', 'u': 'C6', 'i': 'D6', 'o': 'E6', 'p': 'F6', '[': 'G6', ']': 'A7', 's': 'A#4', 'f': 'C#4', 'g': 'D#4', 'j': 'F#4', 'k': 'G#4', 'l': 'A#5', "'": 'C#5', '2': 'D#5', '4': 'F#5', '5': 'G#5', '6': 'A#6', '8': 'C#6', '9': 'D#6', '-': 'F#6', '=': 'G#6'}` –  Aug 13 '19 at 19:30
3

There are almost certainly plenty of synths or programs out there that can do various things like this. However, there is lots of fun and value in doing this on your own, and I honestly couldn't point you to anything specific. For your task, you can modify the wave form by doing things like adding additional harmonics to create square and sine waves, adding additional harmonics based on patters (as I did in some of the below cases) or doing things to vary the onset, phase, amplitude, or any other aspect you'd like.

import math  # import needed modules
import pyaudio  # sudo apt-get install python-pyaudio


scale_notes = {
    # pitch standard A440 ie a4 = 440Hz
    'c': 16.35,
    'C': 17.32,
    'd': 18.35,
    'D': 19.45,
    'e': 20.6,
    'f': 21.83,
    'F': 23.12,
    'g': 24.5,
    'G': 25.96,
    'a': 27.5,
    'A': 29.14,
    'b': 30.87
}


def playnote(note, note_style):

    octave = int(note[1])
    frequency = scale_notes[note[0]] * (2**(octave + 1))

    p = pyaudio.PyAudio()  # initialize pyaudio

    # sampling rate
    sample_rate = 22050

    LENGTH = 1  # seconds to play sound

    frames = int(sample_rate * LENGTH)

    wavedata = ''

    # generating waves
    stream = p.open(
        format=p.get_format_from_width(1),
        channels=1,
        rate=sample_rate,
        output=True)
    for x in range(frames):
        wave = math.sin(x / ((sample_rate / frequency) / math.pi)) * 127 + 128

        if note_style == 'bytwos':
            for i in range(3):
                wave += math.sin((2 + 2**i) * x /
                                 ((sample_rate / frequency) / math.pi)) * 127 + 128
            wavedata = (chr(int(wave / 4)
                            ))

        elif note_style == 'even':
            for i in range(3):
                wave += math.sin((2 * (i + 1)) * x /
                                 ((sample_rate / frequency) / math.pi)) * 127 + 128
            wavedata = (chr(int(wave / 4)
                            ))

        elif note_style == 'odd':
            for i in range(3):
                wave += math.sin(((2 * i) + 1) * x /
                                 ((sample_rate / frequency) / math.pi)) * 127 + 128
            wavedata = (chr(int(wave / 4)
                            ))

        elif note_style == 'trem':
            wave = wave * (1 + 0.5 * math.sin((1 / 10)
                                              * x * math.pi / 180)) / 2
            wavedata = (chr(int(wave)))

        else:
            wavedata = (chr(int(wave))
                        )

        stream.write(wavedata)

    stream.stop_stream()
    stream.close()
    p.terminate()


song = []
while True:
    song_composing = True
    note = ''
    while note != 'p':
        note = str(input(
            '''Enter note (a-G) (capital for sharp) and an octave (0-8) or any other key to play: '''))
        if note[0] in scale_notes:
            note_style = str(
                input('''Enter style (bytwos, even, odd, trem): '''))
            song.append((note, note_style))
            playnote(note, note_style)

    for notes in song:
        playnote(notes[0], notes[1])
    break

Once you experiment with different sounds, you can start looking into how these go together to make real instrument sounds. For instance, guitar or piano decay differently, but not as different as they'd be to woodwinds. Drums generally lack much harmonic structure on purpose, a violin is designed to highlight very pleasant harmonic overtones. There is a good music stackexchange question on the characteristics of instruments.

One thing I would suggest is using a buffer instead of the one-off approach to calculating the next value. Being able to generate a good sound (and apply more complicated algorithms) will be impeded by the ability of your process to complete before the next audio sample is due. I think that's outside the scope of this particular question, but it would also probably be good to use the call back method from pyaudio for this application: https://people.csail.mit.edu/hubert/pyaudio/docs/#example-callback-mode-audio-i-o

0

As an alternative, you and your son may be more interested in how sound waves are actually crafted and then written to a file. While my other answer focused on music, the code shown below is just about the generation of sound. It supports sine, square, triangle, and saw-tooth sound waves and includes the ability to adjust frequency, amplitude, mixing, and interpolation of sounds. Tests are included to generate wave files that can be played back using another program.

#! /usr/bin/env python3
import math
import wave

################################################################################

class Waves:

    BUFF = 1 << 20
    MAX = 127
    MID = 128

    def __init__(self, fps):
        self.__fps = fps
        self.__data = []

    @staticmethod
    def __sin(ratio):
        return math.sin(ratio * math.pi * 2)

    @staticmethod
    def __squ(ratio):
        return 1 - ratio // 0.5 * 2

    @staticmethod
    def __tri(ratio):
        if ratio < 0.25:
            return ratio / 0.25
        elif ratio < 0.75:
            return 1 - 4 * (ratio - 0.25)
        else:
            return (ratio - 0.75) * 4 - 1

    @staticmethod
    def __saw(ratio):
        return ratio / 0.5 - ratio // 0.5 * 2

    def add_sine(self, freq, amp):
        self.__add(freq, amp, self.__sin)

    def add_square(self, freq, amp):
        self.__add(freq, amp, self.__squ)

    def add_triangle(self, freq, amp):
        self.__add(freq, amp, self.__tri)

    def add_sawtooth(self, freq, amp):
        self.__add(freq, amp, self.__saw)

    def __add(self, freq, amp, func):
        rate = int(self.__fps / freq)
        self.__data.extend(func(pos / rate) * amp for pos in range(rate))

    def interpolate_sine(self, freq_a, freq_b, amp_a, amp_b, seconds):
        self.__lerp(freq_a, freq_b, amp_a, amp_b, seconds, self.add_sine)

    def interpolate_square(self, freq_a, freq_b, amp_a, amp_b, seconds):
        self.__lerp(freq_a, freq_b, amp_a, amp_b, seconds, self.add_square)

    def interpolate_triangle(self, freq_a, freq_b, amp_a, amp_b, seconds):
        self.__lerp(freq_a, freq_b, amp_a, amp_b, seconds, self.add_triangle)

    def interpolate_sawtooth(self, freq_a, freq_b, amp_a, amp_b, seconds):
        self.__lerp(freq_a, freq_b, amp_a, amp_b, seconds, self.add_sawtooth)

    def __lerp(self, freq_a, freq_b, amp_a, amp_b, seconds, func):
        samples = int(seconds * (freq_a + freq_b) / 2)
        for position in range(samples):
            b = position / samples
            a = 1 - b
            func(freq_a * a + freq_b * b, amp_a * a + amp_b * b)

    def write(self, name):
        file = wave.open(name, 'wb')
        file.setnchannels(1)
        file.setsampwidth(1)
        file.setframerate(self.__fps)
        self.__writeframes(file)
        file.close()

    def __writeframes(self, file):
        parts = len(self.__data) // self.BUFF
        parts += bool(len(self.__data) % self.BUFF)
        for part in range(parts):
            index = part * self.BUFF
            buff = self.__data[index:index+self.BUFF]
            byte = self.__transform(buff)
            file.writeframes(byte)

    @classmethod
    def __transform(cls, buff):
        return bytes(int(pos * cls.MAX) + cls.MID for pos in buff)

    @classmethod
    def add(cls, *waves):
        sounds = len(waves)
        assert sounds > 1, 'Must have two or more waves to add!'
        fps = waves[0].__fps
        for wave_instance in waves[1:]:
            assert wave_instance.__fps == fps, 'Framerate is not the same!'
        result = cls(fps)
        package = map(lambda wave_instance: wave_instance.__data, waves)
        result.__data = [sum(sound) / sounds for sound in zip(*package)]
        return result

    def __add__(self, other):
        return Waves.add(self, other)

    def __mul__(self, other):
        result = Waves(self.__fps)
        result.__data = [value * other for value in self.__data]
        return result

    def __imul__(self, other):
        self.__data = [value * other for value in self.__data]
        return self

    def append(self, other):
        assert self.__fps == other.__fps, 'Framerate is not the same!'
        self.__data.extend(other.__data)

    def average_amp(self):
        total = count = 0
        for value in self.__data:
            total += abs(value)
            count += 1
        return total / count

    def adjust_amp(self, value):
        self *= value / self.average_amp()

################################################################################

def test_1():
    test = Waves(96000)
    # Standard Test
    test.interpolate_sine(440, 440, 0.1, 0.1, 1)
    test.interpolate_square(440, 440, 0.1, 0.1, 1)
    test.interpolate_triangle(440, 440, 0.1, 0.1, 1)
    # Frequency Test
    test.interpolate_sine(440, 880, 0.1, 0.1, 1)
    test.interpolate_square(440, 880, 0.1, 0.1, 1)
    test.interpolate_triangle(440, 880, 0.1, 0.1, 1)
    # Amplitude Test
    test.interpolate_sine(440, 440, 0.1, 0.5, 1)
    test.interpolate_square(440, 440, 0.1, 0.5, 1)
    test.interpolate_triangle(440, 440, 0.1, 0.5, 1)
    # Frequency & Amplitude Test
    test.interpolate_sine(440, 880, 0.1, 0.5, 1)
    test.interpolate_square(440, 880, 0.1, 0.5, 1)
    test.interpolate_triangle(440, 880, 0.1, 0.5, 1)
    # Finish Test
    test.write('test_1.wav')

def test_2():
    # Addition, Multiplication, & Append Test
    test = Waves(96000)
    # Sine -> Square
    a = Waves(96000)
    a.interpolate_sine(440, 440, 0.5, 0.0, 5)
    a = a * (0.2 / a.average_amp())
    b = Waves(96000)
    b.interpolate_square(440, 440, 0.0, 0.5, 5)
    b = b * (0.2 / b.average_amp())
    c = a + b
    test.append(c)
    # Square -> Triangle
    a = Waves(96000)
    a.interpolate_square(440, 440, 0.5, 0.0, 5)
    a = a * (0.2 / a.average_amp())
    b = Waves(96000)
    b.interpolate_triangle(440, 440, 0.0, 0.5, 5)
    b = b * (0.2 / b.average_amp())
    c = a + b
    test.append(c)
    # Triangle -> Sawtooth
    a = Waves(96000)
    a.interpolate_triangle(440, 440, 0.5, 0.0, 5)
    a = a * (0.2 / a.average_amp())
    b = Waves(96000)
    b.interpolate_sawtooth(440, 440, 0.0, 0.5, 5)
    b = b * (0.2 / b.average_amp())
    c = a + b
    test.append(c)
    # Sawtooth -> Sine
    a = Waves(96000)
    a.interpolate_sawtooth(440, 440, 0.5, 0.0, 5)
    a = a * (0.2 / a.average_amp())
    b = Waves(96000)
    b.interpolate_sine(440, 440, 0.0, 0.5, 5)
    b = b * (0.2 / b.average_amp())
    c = a + b
    test.append(c)
    # Finish Test
    test.write('test_2.wav')

def test_3():
    # Test Sample Mixing
    sound = Waves(96000)
    sample_1 = Waves(96000)
    sample_1.interpolate_sine(220, 440, 0.5, 0.5, 10)
    sample_2 = Waves(96000)
    sample_2.interpolate_sine(330, 660, 0.2, 0.2, 10)
    sample_3 = Waves(96000)
    sample_3.interpolate_sine(440, 880, 0.2, 0.2, 10)
    sound.append(sample_1)
    sound.append(sample_1 + sample_2)
    sound.append(sample_1 + sample_2 + sample_3)
    sound.write('test_3.wav')

def test_4():
    # Test Sound of Waveforms
    sound = Waves(96000)
    # Sine
    sample = Waves(96000)
    sample.interpolate_sine(440, 440, 0.1, 0.1, 2)
    sample.adjust_amp(0.2)
    sound.append(sample)
    # Square
    sample = Waves(96000)
    sample.interpolate_square(440, 440, 0.1, 0.1, 2)
    sample.adjust_amp(0.2)
    sound.append(sample)
    # Triangle
    sample = Waves(96000)
    sample.interpolate_triangle(440, 440, 0.1, 0.1, 2)
    sample.adjust_amp(0.2)
    sound.append(sample)
    # Sawtooth
    sample = Waves(96000)
    sample.interpolate_sawtooth(440, 440, 0.1, 0.1, 2)
    sample.adjust_amp(0.2)
    sound.append(sample)
    # Finish Test
    sound.write('test_4.wav')

################################################################################

if __name__ == '__main__':
    test_1()
    test_2()
    test_3()
    test_4()

If you combine concepts from both answers, you could create a program that takes music you have created or encoded and generate music files other people can play on their devices.

Noctis Skytower
  • 21,433
  • 16
  • 79
  • 117
0

A fairly simple way to do this type of thing is the winsound module. Here's a tutorial that I've excerpted a small section of to illustrate.

from winsound import Beep

# Beep(frequency, duration) Frequency in Hertz, duration in ms

Beep(480,200)
Beep(1568,200)
Beep(1568,200)
Beep(1568,200)
Beep(740,200)
Beep(784,200)
Beep(784,200)
Beep(784,200)
Beep(370,200)
Beep(392,200)
Beep(370,200)
Beep(392,200)
Beep(392,400)
Beep(196,400)

And here's a website with the frequency of notes. It lets you choose from a few different tuning standards.