diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0f1098 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg diff --git a/InteractiveProgramming.pdf b/InteractiveProgramming.pdf new file mode 100644 index 0000000..ed2909b Binary files /dev/null and b/InteractiveProgramming.pdf differ diff --git a/KeyListener.py b/KeyListener.py new file mode 100644 index 0000000..096c284 --- /dev/null +++ b/KeyListener.py @@ -0,0 +1,47 @@ +"""A listener class that turns on and off keys in the Synthesis class""" +from pygame.key import * +import pygame +import time +import sys + +#holds onto keys we're looking for +keys = [pygame.K_0, pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4, pygame.K_5, pygame.K_6, pygame.K_7, pygame.K_8, pygame.K_9, pygame.K_PLUS, pygame.K_MINUS, + pygame.K_a, pygame.K_b, pygame.K_c, pygame.K_d, pygame.K_e, pygame.K_f, pygame.K_g, pygame.K_h, pygame.K_i, pygame.K_j, pygame.K_k, pygame.K_l, pygame.K_m, + pygame.K_n, pygame.K_o, pygame.K_p, pygame.K_q, pygame.K_r, pygame.K_s, pygame.K_t, pygame.K_u, pygame.K_v, pygame.K_w, pygame.K_x, pygame.K_y, pygame.K_z] + +class KeyListener(object): + """ + This class implements the Controller in our MVC. It uses a separate thread to + loop through the event queue, looking for keyboard events and passing the + corresponding sounds into the Model. + """ + + def __init__(self, synth=None, filename='samplelist.txt'): + """This method initializes the KeyListener and reads in the sound sample + names from a text file. + """ + self.synth = synth + f = open(filename) + samplenames = f.readlines() + self.soundmap = {keys[i]:samplenames[i][:-5] for i in range(len(keys))} + + def main(self): + """This method loops through the pygame event queue checking for key + presses or for a quit event. + """ + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN: + self.synth.loop[self.synth.count].append(self.soundmap.get(event.key, 'Zoom')) + # print self.soundmap.get(event.key, 'Zoom') + if event.type == pygame.QUIT: + self.synth.playq.put(('exit',)) + sys.exit() + +if __name__ == '__main__': + pygame.init() + _display_surf = pygame.display.set_mode((300, 300), pygame.HWSURFACE | pygame.DOUBLEBUF) + _running = True + k = KeyListener() + while True: + k.main() + time.sleep(.01) diff --git a/README.md b/README.md index 61ec120..c816131 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ # InteractiveProgramming -This is the base repo for the interactive programming project for Software Design, Spring 2016 at Olin College. +SampleScape2k17 is an Interactive Programming project for Olin College Spring 2016 SoftDes. + +To run, simply download the repository, and run synthesis.py + +``` +$ python synthesis.py +``` + +SampleScape2k17 requires PyGame and TKinter. + +To install these packages: + +``` +$ sudo apt-get install python-pygame python-tk +``` diff --git a/Samples/2012.wav b/Samples/2012.wav new file mode 100644 index 0000000..06f1b03 Binary files /dev/null and b/Samples/2012.wav differ diff --git a/Samples/808.wav b/Samples/808.wav new file mode 100644 index 0000000..5c35148 Binary files /dev/null and b/Samples/808.wav differ diff --git a/Samples/BANG.wav b/Samples/BANG.wav new file mode 100644 index 0000000..4014d70 Binary files /dev/null and b/Samples/BANG.wav differ diff --git a/Samples/BassSaw.wav b/Samples/BassSaw.wav new file mode 100644 index 0000000..4c04e2c Binary files /dev/null and b/Samples/BassSaw.wav differ diff --git a/Samples/Beep.wav b/Samples/Beep.wav new file mode 100644 index 0000000..0fbf66e Binary files /dev/null and b/Samples/Beep.wav differ diff --git a/Samples/Boats.wav b/Samples/Boats.wav new file mode 100644 index 0000000..42a28a7 Binary files /dev/null and b/Samples/Boats.wav differ diff --git a/Samples/Cats.wav b/Samples/Cats.wav new file mode 100644 index 0000000..38fae17 Binary files /dev/null and b/Samples/Cats.wav differ diff --git a/Samples/Chord.wav b/Samples/Chord.wav new file mode 100644 index 0000000..ae49ce4 Binary files /dev/null and b/Samples/Chord.wav differ diff --git a/Samples/Clank.wav b/Samples/Clank.wav new file mode 100644 index 0000000..e82c6da Binary files /dev/null and b/Samples/Clank.wav differ diff --git a/Samples/Clap.wav b/Samples/Clap.wav new file mode 100644 index 0000000..543fe4f Binary files /dev/null and b/Samples/Clap.wav differ diff --git a/Samples/Crash.wav b/Samples/Crash.wav new file mode 100644 index 0000000..f8c329b Binary files /dev/null and b/Samples/Crash.wav differ diff --git a/Samples/Done.wav b/Samples/Done.wav new file mode 100644 index 0000000..00e2659 Binary files /dev/null and b/Samples/Done.wav differ diff --git a/Samples/Kick.wav b/Samples/Kick.wav new file mode 100644 index 0000000..79beab7 Binary files /dev/null and b/Samples/Kick.wav differ diff --git a/Samples/MVC.wav b/Samples/MVC.wav new file mode 100644 index 0000000..377a25f Binary files /dev/null and b/Samples/MVC.wav differ diff --git a/Samples/Meow1.wav b/Samples/Meow1.wav new file mode 100644 index 0000000..2bdaa27 Binary files /dev/null and b/Samples/Meow1.wav differ diff --git a/Samples/Meow2.wav b/Samples/Meow2.wav new file mode 100644 index 0000000..6566f3a Binary files /dev/null and b/Samples/Meow2.wav differ diff --git a/Samples/Pruves.wav b/Samples/Pruves.wav new file mode 100644 index 0000000..d8cd649 Binary files /dev/null and b/Samples/Pruves.wav differ diff --git a/Samples/RealClap.wav b/Samples/RealClap.wav new file mode 100644 index 0000000..b568913 Binary files /dev/null and b/Samples/RealClap.wav differ diff --git a/Samples/Ring.wav b/Samples/Ring.wav new file mode 100644 index 0000000..fe80528 Binary files /dev/null and b/Samples/Ring.wav differ diff --git a/Samples/SampleNames.txt~ b/Samples/SampleNames.txt~ new file mode 100644 index 0000000..e69de29 diff --git a/Samples/Shake.wav b/Samples/Shake.wav new file mode 100644 index 0000000..d1b60c9 Binary files /dev/null and b/Samples/Shake.wav differ diff --git a/Samples/Tap.wav b/Samples/Tap.wav new file mode 100644 index 0000000..771c70e Binary files /dev/null and b/Samples/Tap.wav differ diff --git a/Samples/Tss.wav b/Samples/Tss.wav new file mode 100644 index 0000000..39c699b Binary files /dev/null and b/Samples/Tss.wav differ diff --git a/Samples/YEAHYEAHYEAH.wav b/Samples/YEAHYEAHYEAH.wav new file mode 100644 index 0000000..8f45334 Binary files /dev/null and b/Samples/YEAHYEAHYEAH.wav differ diff --git a/Samples/Yeah.wav b/Samples/Yeah.wav new file mode 100644 index 0000000..78f5edf Binary files /dev/null and b/Samples/Yeah.wav differ diff --git a/Samples/Zoom.wav b/Samples/Zoom.wav new file mode 100644 index 0000000..9d82e89 Binary files /dev/null and b/Samples/Zoom.wav differ diff --git a/Samples/a1.wav b/Samples/a1.wav new file mode 100644 index 0000000..1c1ce14 Binary files /dev/null and b/Samples/a1.wav differ diff --git a/Samples/a1s.wav b/Samples/a1s.wav new file mode 100644 index 0000000..d6d087d Binary files /dev/null and b/Samples/a1s.wav differ diff --git a/Samples/b1.wav b/Samples/b1.wav new file mode 100644 index 0000000..05dcfea Binary files /dev/null and b/Samples/b1.wav differ diff --git a/Samples/c1.wav b/Samples/c1.wav new file mode 100644 index 0000000..1370d67 Binary files /dev/null and b/Samples/c1.wav differ diff --git a/Samples/c1s.wav b/Samples/c1s.wav new file mode 100644 index 0000000..7e916ab Binary files /dev/null and b/Samples/c1s.wav differ diff --git a/Samples/c2.wav b/Samples/c2.wav new file mode 100644 index 0000000..9a1957f Binary files /dev/null and b/Samples/c2.wav differ diff --git a/Samples/d1.wav b/Samples/d1.wav new file mode 100644 index 0000000..f82c97c Binary files /dev/null and b/Samples/d1.wav differ diff --git a/Samples/d1s.wav b/Samples/d1s.wav new file mode 100644 index 0000000..0110649 Binary files /dev/null and b/Samples/d1s.wav differ diff --git a/Samples/e1.wav b/Samples/e1.wav new file mode 100644 index 0000000..41d2572 Binary files /dev/null and b/Samples/e1.wav differ diff --git a/Samples/f1.wav b/Samples/f1.wav new file mode 100644 index 0000000..5b77cd0 Binary files /dev/null and b/Samples/f1.wav differ diff --git a/Samples/f1s.wav b/Samples/f1s.wav new file mode 100644 index 0000000..7fe23c8 Binary files /dev/null and b/Samples/f1s.wav differ diff --git a/Samples/g1.wav b/Samples/g1.wav new file mode 100644 index 0000000..963a6a7 Binary files /dev/null and b/Samples/g1.wav differ diff --git a/Samples/g1s.wav b/Samples/g1s.wav new file mode 100644 index 0000000..04cad57 Binary files /dev/null and b/Samples/g1s.wav differ diff --git a/bass1.wav b/bass1.wav new file mode 100644 index 0000000..141bbd6 Binary files /dev/null and b/bass1.wav differ diff --git a/entrytest.py b/entrytest.py new file mode 100644 index 0000000..f7ae358 --- /dev/null +++ b/entrytest.py @@ -0,0 +1,26 @@ +import tkSimpleDialog +from Tkinter import * + +import tkSimpleDialog + +class MyDialog(tkSimpleDialog.Dialog): + + def body(self, other): + Label(other, text="BPM:").grid(row=0) + Label(other, text="Bars:").grid(row=1) + self.e1 = Entry(other) + self.e2 = Entry(other) + self.e1.grid(row=0, column=1) + self.e2.grid(row=1, column=1) + return self.e1 # initial focus + + def apply(self): + try: + first = int(self.e1.get()) + second = int(self.e2.get()) + except: + first, second = 120, 8 + self.result = first, second + + def values(self): + return self.result diff --git a/lead1.wav b/lead1.wav new file mode 100644 index 0000000..935fd21 Binary files /dev/null and b/lead1.wav differ diff --git a/pygame_play_test.py b/pygame_play_test.py new file mode 100644 index 0000000..3ec6ae0 --- /dev/null +++ b/pygame_play_test.py @@ -0,0 +1,23 @@ +import pygame +import sys + +pygame.mixer.init() + +bass1 = pygame.mixer.Sound('bass1.wav') +lead1 = pygame.mixer.Sound('lead1.wav') + +bass1.play() + +pygame.init() +_display_surf = pygame.display.set_mode((300, 300), pygame.HWSURFACE | pygame.DOUBLEBUF) +_running = True + +while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + sys.exit() + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_1: + bass1.play() + if event.key == pygame.K_2: + lead1.play() diff --git a/samplelist.txt b/samplelist.txt new file mode 100644 index 0000000..768b741 --- /dev/null +++ b/samplelist.txt @@ -0,0 +1,38 @@ +c1.wav +c1s.wav +d1.wav +d1s.wav +e1.wav +f1.wav +f1s.wav +g1.wav +g1s.wav +a1.wav +a1s.wav +b1.wav +c2.wav +Clank.wav +Clap.wav +Kick.wav +Ring.wav +Zoom.wav +Beep.wav +BassSaw.wav +Crash.wav +Chord.wav +Pruves.wav +BANG.wav +Yeah.wav +YEAHYEAHYEAH.wav +Shake.wav +MVC.wav +Boats.wav +2012.wav +808.wav +Tss.wav +Meow1.wav +Meow2.wav +Cats.wav +RealClap.wav +Tap.wav +Done.wav diff --git a/speaker.bmp b/speaker.bmp new file mode 100644 index 0000000..3583cc6 Binary files /dev/null and b/speaker.bmp differ diff --git a/synthesis.py b/synthesis.py new file mode 100644 index 0000000..1a5a660 --- /dev/null +++ b/synthesis.py @@ -0,0 +1,162 @@ +import pygame +from pygame.mixer import * +from pygame.locals import * +import math +import numpy as np +from Queue import Queue +import threading +from threading import Thread +import time +from KeyListener import KeyListener +import sys +from Tkinter import Tk +import entrytest + +SUBDIVISIONS = 8 + +class Synth(object): + """This class holds onto the state of our synthesizer. It is the M in our MVC. + This class has a list of "16th notes", which are lists of sounds to be played. + As it loops through the list, it adds any key presses to the list and places + any sounds already in the list onto the playing queue. + + attributes: bpm, bars, click, sleep_time + """ + + def __init__(self, bpm=120, bars=8, click=True): + """This method initializes the synthesizer's bpm, the length of the loop, + its sleep time, and sets up the empty sample loop. + """ + self.bpm = bpm + self.bars = bars + self.click = click + self.sleep_time = 60.0/(bpm/4) / SUBDIVISIONS + self.count = 0 + self.loop = [[] for sub in range(bars * SUBDIVISIONS)] + self.loop = [self.loop[beat] + ['Beep'] if beat % 4 == 0 and click else self.loop[beat] for beat in range(len(self.loop))] + self.playq = Queue() + + # def frequencyMap(self, index): + # return 2**(index/12.0) * 440 + + + def main(self): + """This method keeps track of our position in the sample loop and puts + any samples it finds in the current subdivision into the sound playing + queue. + """ + for e in self.loop[self.count]: + # Note that queues will take tuples, but will break up strings. + self.playq.put((e,)) + self.count += 1 + self.count = self.count % (self.bars * SUBDIVISIONS) + + +class Viewer(object): + """This class is the View part of our MVC model. It runs in its own separate + thread, waiting for sounds to appear in its queue. It then maps those sounds + to pygame Sound objects and plays them. + """ + def __init__(self, synth=None, filename='samplelist.txt'): + """This method handles initializing this class's synth, and also prepares + the name-sound mapping for playing sounds. + """ + if synth == None: + synth = Synth() + self.synth = synth + + pygame.mixer.init() + + f = open(filename) + + #preparing the sound map + self.soundmap = {line.strip('\n')[:-4]:Sound('Samples/' + line.strip('\n')) for line in f.readlines()} + print self.soundmap + + def main(self): + """This method interprets the play queue, and plays sounds unless it + receives a special exit tag. + """ + for sound in self.synth.playq.get(): + if sound == ('exit'): + break + self.soundmap[sound].play() + +if __name__ == '__main__': + """This main function is where a lot of learning happened. We went through + many iterations to get the threading to work properly. First, everything is + initialized, and then three threads are started, one on each of the classes' + main methods. Conditionals in these loops serve to clean up when the + controller receives an exit event. + """ + + #pygame initialization + pygame.init() + _display_surf = pygame.display.set_mode((46,45), pygame.HWSURFACE | pygame.DOUBLEBUF) + _running = True + img = pygame.image.load('speaker.bmp') + _display_surf.blit(img, pygame.Surface.get_rect(img)) + + #synthesizer initialization + d = entrytest.MyDialog(Tk()) + a = Synth(*d.values()) + b = KeyListener(a) + c = Viewer(a, 'samplelist.txt') + + #thread target functions + def _synthstart(): + print 'running SYNTH' + global _running + next_loop = time.time() + print a.sleep_time + while _running: + #handles processing time and drift + next_loop += a.sleep_time + a.main() + try: + time.sleep(next_loop - time.time()) + except: + pass + + print 'finished SYNTH' + + def _keylistenerstart(): + 'running KEYLISTENER' + try: + global _running + while _running: + b.main() + except SystemExit: + _running = False + pygame.quit() + print 'finished KEYLISTENER' + + def _viewerstart(): + print 'running VIEWER' + global _running + while _running: + c.main() + print 'finished VIEWER' + + # def _exit(): + # print 'running EXIT' + # while True: + # for event in pygame.event.get(): + # if event.type == pygame.QUIT: + # _running = False + # pygame.quit() + # sys.exit() + k = Thread(target=_keylistenerstart, name='KEYLISTENER') + v = Thread(target=_viewerstart, name='VIEWER') + s = Thread(target=_synthstart, name='SYNTH') + # exit = Thread(target=_exit, name='EXIT') + + # exit.start() + s.start() + k.start() + v.start() + + s.join() + v.join() + k.join() + # exit.join() diff --git a/timingtest.py b/timingtest.py new file mode 100644 index 0000000..2d949fd --- /dev/null +++ b/timingtest.py @@ -0,0 +1,8 @@ +import time + +next_loop = time.time() + +while True: + next_loop += 1 + print 'hi' + time.sleep(next_loop - time.time()) diff --git a/tkSimpleDialog.py b/tkSimpleDialog.py new file mode 100644 index 0000000..56fa30d --- /dev/null +++ b/tkSimpleDialog.py @@ -0,0 +1,73 @@ +"""Default dialog box object. Courtesy of Fredrik Lundh, but public domain""" + +from Tkinter import * +import os + +class Dialog(Toplevel): + + def __init__(self, parent, title = None): + Toplevel.__init__(self, parent) + self.transient(parent) + if title: + self.title(title) + self.parent = parent + self.result = None + body = Frame(self) + self.initial_focus = self.body(body) + body.pack(padx=5, pady=5) + self.buttonbox() + self.grab_set() + if not self.initial_focus: + self.initial_focus = self + self.protocol("WM_DELETE_WINDOW", self.cancel) + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, + parent.winfo_rooty()+50)) + self.initial_focus.focus_set() + self.wait_window(self) + # + # construction hooks + + def body(self, master): + # create dialog body. return widget that should have + # initial focus. this method should be overridden + pass + + def buttonbox(self): + # add standard button box. override if you don't want the + # standard buttons + box = Frame(self) + w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE) + w.pack(side=LEFT, padx=5, pady=5) + w = Button(box, text="Cancel", width=10, command=self.cancel) + w.pack(side=LEFT, padx=5, pady=5) + self.bind("", self.ok) + self.bind("", self.cancel) + box.pack() + + # + # standard button semantics + + def ok(self, event=None): + + if not self.validate(): + self.initial_focus.focus_set() # put focus back + return + self.withdraw() + self.update_idletasks() + self.apply() + self.cancel() + + def cancel(self, event=None): + + # put focus back to the parent window + self.parent.focus_set() + self.destroy() + + # + # command hooks + + def validate(self): + return 1 # override + + def apply(self): + pass # override