Advanced Interactive Media final

For my final project in interactive media, I composed “Underground Lake” with LMMS. Then, I challenged myself to visualize the music as a pattern of moving ripples in a dark pool of water.

The purpose of this project was to overcome a barrier in After Effects, the lack of a feature that seems fairly simple: A way to transform the composited image data in the Echo effect. For instance, creating a trail for a stationary object.

It would be so simple, if only you could:

  1. reference the image of this layer “current” as it appeared 1 frame ago, through the virtual layer “last”
  2. apply the transform effect to “last”
  3. composite “last” on top of “current”

The Echo effect is sealed shut, and doesn’t allow that kind of tinkering. This project is a hack, using MotionTile to animate layer trails.

My final submission also included a diagram of the entire project. However, I think that my diagram could be refined before I post it here. In the meantime, here’s the video:

Assignment: My name in 3D

Made using Cinema4D on September 9th, 2016.

Our teacher asked us to make a video revealing our names from left to right, with a moving spotlight and a moving camera, in front of a checkered floor and a starry sky, with swishes and squiggles behind the text, that was exactly 90 frames long.

I felt it was so regulated as to not allow much creativity. But then I got over it.

We also collectively bargained with him to increase the time limit, and he agreed. It wouldn’t have worked if 3 or 4 of us didn’t already have projects that were too long.

Operation: Catapult

One day in the spring of 2016, I wasn’t able to make my internship, so I found myself in my actual Computer Programming II “cover class” that was displayed on my schedule. That day I was introduced to Python. I didn’t have the time to finish the customary Mandelbrot generator to put the new language through its paces. I didn’t know much about python by the end of the day either.

That summer, I had the great privilege of visiting Rose-Hulman for 2 1/2 weeks. I immediately joined computer programming, because I wanted to learn Python for real.

I was surprised to learn that python is MAGIC. This xkcd put it well:

python

During my too-short stay in paradise, I finally learned how to work in a group. We used Dropbox instead of Git, for some reason.

I had originally planned to work alone and create a simple lossy image format (which I’m still interested in doing) until our professor convinced me to work in a group instead. Something about not having enough time to complete such a large project going solo. At the time all I could hear was him asking me to aim lower, but I’m thankful that I chose the group project now. It probably presented greater challenges and taught me more anyway – I’m already onto lossy compression techniques. Game design? Other people? Conforming to someone else’s master plan? Not so much yet.

I’m only going to release the code I contributed at this time.

 

file stickmanFigure3.py:

#06.24.16

import pygame
from stickmanSprite6 import *
from pose import *
import animationLib3 as animLib
import animationUtility as animUtil
import time

class StickmanFigure:

    def __init__(self, figureName = "Tom", expression = "Angry", scale = [1, 1], hasTail = True, drawMan = True, currentAnimName = "IDLE", mssg = ""):
        self.figureName = figureName
        self.message = mssg
        self.currentAnimName = currentAnimName
        self.contentSprite = StickmanSprite(figureName, expression, hasTail) #the main sprite
        self.animFrame = 0 #the current frame of the animation
        self.frame = 0 #the total number of frames ever
        self.baseDuration = 12.0 # the number of frames which pass between each keyframe
        self.currentAnim = animLib.getAnim(currentAnimName)
        self.currentAnimLength = animUtil.frameCount(self.currentAnim, self.baseDuration)
        self.drawMan = drawMan
        self.tempDrawMan = True
        self.scale = scale
        
    def doAnimation(self, animationName):
        self.animFrame = 0
        self.currentAnimName = animationName
        self.currentAnim = animLib.getAnim(self.currentAnimName)

    def draw(self, target, coords, scale):
        tailDirection = scale[0] / abs(scale[0])
        scale = (abs(scale[0]), scale[1])
        scale = self.scale
        if self.drawMan:
            if self.animFrame >= self.currentAnimLength:
                self.doAnimation(self.currentAnimName)
            if not self.contentSprite.editing_isEditing:
                self.contentSprite.setPose(self.getPose())
                self.animFrame += 1
            self.frame += 1
        return self.contentSprite.draw(target, coords, scale, tailDirection)

    def getPose(self):
        #print("StickmanFigure: StickmanFigure: getPose: calculating based on anim " + str(self.currentAnim))
        return  animUtil.terpAnim(self.currentAnim, self.baseDuration, self.animFrame)
        #print("StickmanFigure: StickmanFigure: getPose: and now the animation is  " + str(self.currentAnim))
        #print("StickmanFigure: StickmanFigure: getPose: returning " + str(result))
        

description:

The StickmanFigure class is responsible for running requested animations and manipulating a StickmanSprite object. It is the highest level class I wrote, and can be implemented directly into any project that uses pygame.

 

file stickmanSprite6:

#06.24.16

import pygame
import os
from pose import *
import math

class StickmanSprite:

    face = pygame.Surface((32,32))

    def __init__(self, identity = "Tom", expression = "Angry", hasTail = True):

        #identity ----------------------------------------------------------------
        self.identity = identity #used in getting face image from resources
        self.expression = expression #used in getting face image from resources

        #pose data ---------------------------------------------------------------
        self.basePose = [[35,100], [65,100], [50,50], [50,30], [35,60], [65,60]] #the default locations of all parts. poses are added to this.
        self.currentPose = [[0,0], [0,0], [0,0], [0,0], [0,0], [0,0]] #the current pose, can be used to access the pose which is in use.
        self.drawPose =  [[0,0], [0,0], [0,0], [0,0], [0,0], [0,0]] #the pose which is editable and drawable (basePose + currentPose = drawPose)
        self.drawPose = Pose.add(self.basePose, self.currentPose) #initialize drawPose

        #configuration -----------------------------------------------------------
        self.headResponse = 0.6 #effect of pose on head angle
        self.hasTail =hasTail #enable/disable tail feature
        self.tailWavelength = 32 #number of frames for a full cycle of tail wiggle amplitude
        self.tailLength = 0.5 #shorten tail to the specified length without changing the wave
        self.tailAmplitude = 0.01 #multiplier for tail wiggle amplitude
        self.tailHeights = [0,10,18,24,26,24,18,10,0,-10,-18,-24,-26,-24,-18,-10] #approximation of a sine wave
        self.foreground = [0,0,0] #foreground color
        self.background = [220,220,220] #background color
        #self.aa = True #enable/disable antialiasing. this is no longer available.
        self.lineWidth = 3

        #other --------------------------------------------------------------------
        self.editing_isEditing = False #when True, the stickman can be editied by clicking and dragging parts, to get pose information
        self.editing_clicked_left = False #whether the stickman is being clicked during editing
        self.editing_clicked_right = False
        self.timesDrawn = 0 #number of frames the stickman has been drawn in. used in tail calculations!
        self.framePhase = 0 #cached tail wiggle amplitude for this frame (instead of recalculating for each pixel.) this is calculated at the start of draw()
        self.face = pygame.image.load(self.identity + "_" + self.expression + ".png") #his face
        #self.face = self.face.convert()
        #self.face.set_alpha()
        #self.face.set_colorkey([255,255,255])

        #constants ----------------------------------------------------------------
        self.PI = 3.141592653589793238462643383279502884197169399375
        self.TAU = self.PI * 2



    def setPose(self, newPose): #set the pose of the stickman
        self.currentPose = newPose
        self.drawPose = Pose.add(self.basePose, self.currentPose)

    def draw(self, target, coords, scale, tailDirection = 1):
        if self.editing_isEditing:
            self.edit(coords, scale)  #edit while drawing
        self.timesDrawn += 1
        #self.framePhase = float(math.sin((self.timesDrawn / self.tailWavelength) * math.pi * 2)) * 100000000000000000.0
        self.framePhase = self.timesDrawn%self.tailWavelength - (self.tailWavelength/2)
        #self.framePhase = self.timesDrawn%self.tailWavelength
        #self.thisSurface.fill(self.background) #clear the main surface
        pygame.draw.line(target, self.foreground, [self.drawPose[0][0]*scale[0]+coords[0],self.drawPose[0][1]*scale[1]+coords[1]], [self.drawPose[2][0]*scale[0]+coords[0],self.drawPose[2][1]*scale[1]+coords[1]], self.lineWidth) #draw left leg
        pygame.draw.line(target, self.foreground, [self.drawPose[1][0]*scale[0]+coords[0],self.drawPose[1][1]*scale[1]+coords[1]], [self.drawPose[2][0]*scale[0]+coords[0],self.drawPose[2][1]*scale[1]+coords[1]], self.lineWidth) #draw right leg
        pygame.draw.line(target, self.foreground, [self.drawPose[2][0]*scale[0]+coords[0],self.drawPose[2][1]*scale[1]+coords[1]], [self.drawPose[3][0]*scale[0]+coords[0],self.drawPose[3][1]*scale[1]+coords[1]], self.lineWidth) #draw torso
        pygame.draw.line(target, self.foreground, [self.drawPose[4][0]*scale[0]+coords[0],self.drawPose[4][1]*scale[1]+coords[1]], [self.drawPose[3][0]*scale[0]+coords[0],self.drawPose[3][1]*scale[1]+coords[1]], self.lineWidth) #draw left arm
        pygame.draw.line(target, self.foreground, [self.drawPose[5][0]*scale[0]+coords[0],self.drawPose[5][1]*scale[1]+coords[1]], [self.drawPose[3][0]*scale[0]+coords[0],self.drawPose[3][1]*scale[1]+coords[1]], self.lineWidth) #draw right arm
        if self.hasTail:
            self.drawTail(target, coords, scale, tailDirection) #draw tail
        if scale == [.5,.5]:
            stickmanHead = pygame.transform.rotate(self.face, Pose.getHeadRot(self.drawPose) * -self.headResponse)
            stickmanHead = pygame.transform.scale(self.face, (int(64*scale[0]),int(128*scale[1])))
            posx = 64*scale[0] - 145*scale[0]
            posy = 64*scale[1] - 150*scale[1]
            target.blit(stickmanHead, ((int(self.drawPose[3][0]+posx)+coords[0]),int((self.drawPose[3][1]+posy)+coords[1])))  #special_flags=pygame.BLEND_RGB_MULT
        else:
            stickmanHead = pygame.transform.rotate(self.face, Pose.getHeadRot(self.drawPose) * -self.headResponse)
            stickmanHead = pygame.transform.scale(self.face, (int(64*scale[0]),int(128*scale[1])))
            target.blit(stickmanHead, ((self.drawPose[3][0]-32)*scale[0]+coords[0],(self.drawPose[3][1]-60)*scale[1]+coords[1]))#target.blit(self.thisSurface, (0,0))
        return stickmanHead


    def drawTail(self, drawOnto, coords, scale, direction):
        tailSpacing = self.tailLength * direction * 50 / len(self.tailHeights)
        #print(str(tailSpacing))
        for index in range(len(self.tailHeights)-1):
            #print(str(index))
            pygame.draw.line(drawOnto, self.foreground,
                               [(tailSpacing*(index+0) + self.drawPose[2][0])*scale[0] + coords[0], (self.drawPose[2][1] + self.tailHeights[index+0]*self.tailAmplitude*self.tailHeights[int(self.framePhase%len(self.tailHeights))])*scale[1] + coords[1]],
                               [(tailSpacing*(index+1) + self.drawPose[2][0])*scale[0] + coords[0], (self.drawPose[2][1] + self.tailHeights[index+1]*self.tailAmplitude*self.tailHeights[int(self.framePhase%len(self.tailHeights))])*scale[1] + coords[1]],
                               self.lineWidth)

    def toggleEdit(self):
        self.editing_isEditing = not self.editing_isEditing
        if self.editing_isEditing:
            print("left click to adjust nearest part, middle click to print pose")

    def edit(self, coords, scale):
        #print("editing stickman...")
        relativePos = pygame.mouse.get_pos()
        relativePos = [(relativePos[0]-coords[0])/scale[0],(relativePos[1]-coords[1])/scale[1]]
        editingPoint = self.getClosest(relativePos)
        if self.editing_clicked_left and (not pygame.mouse.get_pressed()[0]): #whenever the mouse button is released
            self.reversePose() #determine the actual pose, which is the difference between the current pose and the base pose
        if self.editing_clicked_right and (not pygame.mouse.get_pressed()[1]):
            print(str(self.currentPose)) #print the current pose for use in an animation
        self.editing_clicked_left = pygame.mouse.get_pressed()[0]
        self.editing_clicked_right = pygame.mouse.get_pressed()[1]
        if self.editing_clicked_left:
            self.drawPose[editingPoint] = relativePos
            #print("    Moving a point... clicked==" + str(clicked))

    def getClosest(self, testPosition):
        distance, index = min((dist, ind) for (ind, dist) in enumerate(Pose.getDistance(testPosition, test) for test in self.drawPose)) #I do not understand this statement, but it works
        return index

    def reversePose(self):
        #print("reversing pose...")
        #print("drawPose: " + str(self.drawPose))
        #print("basePose: " + str(self.basePose))
        #print("changing current pose from " + str(self.currentPose))
        self.currentPose = Pose.add(self.drawPose, Pose.neg(self.basePose))
        #print("to " + str(self.currentPose))

description:

The StickmanSprite class is a system for drawing a stick man based on an adjustable Pose. It also functions as the tool for creating animations – if the game is launched while isEditing = True, you can drag around the stick man’s limbs to set his pose. When you middle-click the stick man, the current pose will be printed in the console. Do this once for each keyframe, then copy all the lines from the console into the animation library and give the animation a name. It’s a fast system.

The sprite also handles facial expressions by loading images from a directory based on the character’s name and specified emotion. The head tilts to match the body at all times based on simple mathematical rules. It also has a tail, so that we could use it as a monkey when necessary. The tail, like the head, is not directly controlled by the pose.

I chose “Tom” as the name of the starter character as a reference to Tom’s Diner. Although in testing my own audio codec, I went my own way and decided on “Don’t Stop” by Foster The People – The Fat Rat’s remix.

 

file pose.py:

class Pose:

    LFOOT = 0
    RFOOT = 1
    WAIST = 2
    NECK = 3
    LHAND = 4
    RHAND = 5


    def __init__(self):
        pass   #it is possible, but pointless, to instantiate this empty class


    @staticmethod
    def neg(in1):
        result = [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]] #empty ffwnhh pose
        for i in range(len(in1)): #for all in the first dimension
            for ii in range(2): #for all in the second dimension
                result[i][ii] = -in1[i][ii] #set to the opposite of the input
        return result[:]


    @staticmethod
    def add(in1, in2):
        result = [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]] #empty ffwnhh pose
        for i in range(len(in1)): #for all in the first dimension
            for ii in range(2): #for all in the second dimension
                result[i][ii] = in1[i][ii] + in2[i][ii] #add them to the answer
        return result[:]


    @staticmethod
    def multiply(in1, in2):
        result = [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]] #empty ffwnhh pose
        for i in range(len(in1)): #for all in the first dimension
            for ii in range(2): #for all in the second dimension
                result[i][ii] = in1[i][ii] * in2 #multiply them into the answer
        return result[:]


    @staticmethod
    def lerpPoints(point1, point2, distance):
        displacement = [point2[0]-point1[0], point2[1]-point1[1]]
        progress = [displacement[0]*distance,displacement[1]*distance]
        result = [point2[0]-progress[0],point2[1]-progress[1]]
        return result
        

    @staticmethod
    def lerp(pose1, pose2, distance):
        #print("pose: Pose: lerp: ----------------------------------------------------------------------")
        #print("pose: Pose: lerp: given a distance of " + str(distance))
        #print("pose: Pose: lerp: given a pose1 of    " + str(pose1))
        #print("pose: Pose: lerp: given a pose2 of    " + str(pose2))
        displacement = Pose.add(pose2, Pose.neg(pose1))
        #print("pose: Pose: lerp: resulting in pose1  " + str(pose1))
        #print("pose: Pose: lerp: resulting in pose1  " + str(pose2))
        #print("pose: Pose: lerp: displacement is now " + str(displacement))
        progress = Pose.multiply(displacement, distance)
        #print("pose: Pose: lerp: resulting in pose1  " + str(pose1))
        #print("pose: Pose: lerp: resulting in pose1  " + str(pose2))
        #print("pose: Pose: lerp: progress is now " + str(progress))
        result = Pose.add(pose2, Pose.neg(progress))
        #print("pose: Pose: lerp: resulting in pose1  " + str(pose1))
        #print("pose: Pose: lerp: resulting in pose1  " + str(pose2))
        #print("pose: Pose: lerp: result is now " + str(result))
        return result[:]

    
    @staticmethod
    def getHeadRot(pose1): #thrice the head's X minus the sum of the X's of the feet and waist. a decentish approximation w/o trig.
        return (3 * pose1[3][0]) - pose1[0][0] - pose1[1][0] - pose1[2][0]


    @staticmethod            
    def getTailEnd(pose1): #a point which is exactly getTailLength(pose) to the left of getMidpoint(pose)
        midpt = Pose.getMidpoint(pose1)
        return [midpt[0] - Pose.getTailLength(pose1), midpt[1]]

    @staticmethod
    def getTailLength(pose1): #the average leg length
        return Pose.getMean([Pose.getDistance(pose1[0],pose1[2]), Pose.getDistance(pose1[1],pose1[2])])


    @staticmethod
    def getMean(inputList): #takes any lengthy list
        result = 0
        for item in inputList:
            result += item
        result /= len(inputList)
        return result


    @staticmethod
    def getMidpoint(pose1): #get the average of all points in a pose
        list1 = [0,0,0,0,0,0]
        list2 = [0,0,0,0,0,0]
        for point in range(len(pose1)):
            list1[point] = pose1[point][0]
            list2[point] = pose1[point][1]
        return [Pose.getMean(list1), Pose.getMean(list2)]


    @staticmethod
    def getDistance(in1, in2): #basic planar distance function: square root of ((x1-y1)^2 + (x2-y2)^2)
        return ((in1[0] - in2[0])**2 + (in1[1] - in2[1])**2)**0.5

description:

The Pose class isn’t a type that’s used to hold poses – it’s a collection of methods for acting on poses, which are just arrays of 6 tuples (coordinate pairs for feet, waist, neck, hands). The Pose class can add, multiply, and most importantly Lerp two poses together. Lerp is called to find every frame of an animation based on stored keyframes.

 

file animationUtility.py:

 from pose import *

def terpAnim(inputAnim, baseDuration, frameIndex):
    keyPair = int(frameIndex//baseDuration)
    #print("animationUtility: terpAnim: (keyPair=" + str(keyPair) + ",frameIndex=" + str(frameIndex) + ")")
    #print("animationUtility: terpAnim: inputAnim[" + str(keyPair) + "] is " + str(inputAnim[keyPair]))
    #print("animationUtility: terpAnim: inputAnim[" + str(keyPair+1) + "] is " + str(inputAnim[keyPair+1]))
    return lerpFrames(inputAnim[keyPair], inputAnim[(keyPair+1)%len(inputAnim)], baseDuration, frameIndex % baseDuration)

def lerpFrames(inputPose1, inputPose2, baseDuration, frameIndex):
    #print("animationUtility: lerpFrames: (baseDuration=" + str(baseDuration) + ",frameIndex=" + str(frameIndex) + ")")
    #print("animationUtility: lerpFrames: working with pose1: " + str(inputPose1))
    #print("animationUtility: lerpFrames: working with pose2: " + str(inputPose2))
    result = Pose.lerp(inputPose1, inputPose2, float(frameIndex) / baseDuration)
    #print("animationUtility: lerpFrames: resulting in pose1: " + str(inputPose1))
    #print("animationUtility: lerpFrames: resulting in pose2: " + str(inputPose2))
    return result

def frameCount(inputAnim, baseDuration):
    return (keyframeCount(inputAnim) - 1) * baseDuration

def keyframeCount(inputAnim):
    return len(inputAnim)

description:

The animation utility holds a few methods for playing animations, and is responsible for lerping the two appropriate keyframes to get any requested frame.

 

file animationLib3.py:

#06.24.16

anims = dict([
    ('IDLE',
         [
             [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
             [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
             [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]]
         ]
    ),
    ('JITTERTEST',
         [
             [[5,8],[-10,-14],[0,0],[0,0],[4,4],[20,8]],
             [[3,11],[7,4],[0,0],[0,0],[-5,0],[2,-11]],
             [[-4,-2],[-2,11],[0,0],[0,0],[-8,8],[-4,-5]]
         ]
    ),
    ('LIEDOWN',
         [
             [[-14.0, -65.0], [-74.0, -55.0], [-26.0, -2.0], [10.0, 12.0], [56.0, -11.0], [17.0, 0.0]],
             [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]]
         ]
     ),
     ('WALK',
         [
            [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]],
            [[32.0, -2.0], [-31.0, -4.0], [0, 0], [0, 0], [40.0, -4.0], [-36.0, -5.0]]
         ]
     ),
     ('JUMP',
         [
            [[32.0, -23.0], [-29.0, -23.0], [0, 0], [0, 0], [37.0, -4.0], [-34.0, -9.0]],
            [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]
         ]
     )
])

def getAnim(keyword):
    return anims[keyword.upper()]

description:

These are the basic animations that made it into our final project. The speed couldn’t be set for each animation, and the movement of the character around the screen was handled by a different class – something I wasn’t aware of until after the game was finished. I would’ve incorporated that motion into these animations, but it’s probably for the best; this simple collection can’t be broken be resizing the window.

file frameRate.py:

"""
============== USAGE ==============
import using:
    from frameRate import FrameRate
create a FrameRate object to handle your frames:
    frames = FrameRate(number of frames per second)
at the end of every frame, do:
    frames.finishFrame()
this will hold the frame rate as near to the rate you specified as possible.
"""


import pygame.time
import time

class FrameRate:
    
    def __init__(self, rate):
        self.bias = Biaser()
        self.t1 = pygame.time.get_ticks()
        self.targetWait = 1000/rate

    def finishFrame(self):
        self.t2 = pygame.time.get_ticks()
        self.bias.addInput(self.t2 - self.t1)
        duration = max(0, self.targetWait - self.bias.getValue())
        time.sleep(duration/1000)
        self.t1 = pygame.time.get_ticks()


class Biaser:
    def __init__(self):
        self.value = 1.0/15.0
        self.last = 1.0/15.0
        self.shortCutoff = 1.0/120.0
        self.longCutoff = 1.0/12.0
        self.feedback = 0.5
        self.wetness = 0.7

    def getValue(self):
        result = (self.value*self.wetness) + (self.last*(1-self.wetness))
        return result

    def addInput(self, in1):
        self.value = (self.value*self.feedback) + (max(min(in1, self.longCutoff), self.shortCutoff)*(1-self.feedback))
        self.last = in1

description:

The first class I wrote that wasn’t part of my StickFigure. This object dynamically adjusts the wait period at the end of each frame to prioritize a stable framerate over a high one. It has cutoff values so that it can’t be majorly affected by crazy frames that take longer than 1/12 second (if the game is paused) or shorter than 1/120 second (supposing we accidentally called draw multiple times in a row). Thus it never needs to be turned off to preserve its vision of the perfect framerate.

And you’ll notice I’m getting better at commenting my code as time passes.

 

file soundEngine.py:

import pygame
import pygame.mixer
import time
import os


"""
 ============ USAGE ===========
 -import using:
     from soundEngine import *
     
 -initialize a SoundEngine:
     self.yourMusicThing = SoundEngine()
     
 -load music and effect before you need to use them:
     self.yourMusicThing.loadMusic()
     self.yourMusicThing.loadEffect()
     
 -play sounds from \sound\effect\ using:
     self.yourMusicThing.playEffect('sound ID')
     (when adding new sounds to \sound\effect\
     make sure to add them to the dictionary as well)
     
 -switch to new music from \sound\music\ using:
     self.yourMusicThing.setMusic("song ID")
     (songs are:
         - 'TITLE'
         - 'AMBIENT'
         - 'BATTLE'
         - 'BOSS'
         - 'WIN'
         - 'LOSE')
     switching to new music will stop the old music.

"""

class SoundEngine:

    pygame.mixer.init(24000, 16, 2, 4096)


    def __init__(self):
        print("sound engine: initializing...")
        self.soundSubdir = "\\sound"
        self.musicSubdir = "\\music\\"
        self.effectSubdir = "\\effect\\"
        self.container = ".ogg"
        self.currentMusic = 'AMBIENT'
        self.fadeDuration = 250
        print("sound engine: initialized")

    
    def loadMusic(self):
        print("sound engine: loading music...")
        print("     \"BlockMan\", \"Cyborg Ninja\", \"Danger Storm\", \"Pixelland\", \"Show Your Moves\"\n     Kevin MacLeod (incompetech.com)\n     Licensed under Creative Commons: By Attribution 3.0\n     http://creativecommons.org/licenses/by/3.0/\"")
        self.music = dict([
            #('TITLE', self.getSound(self.musicSubdir, "incompetech-spazzmatic_polka")),
            ('AMBIENT', self.getSound(self.musicSubdir, "incompetech-pixelland")),
            ('BATTLE', self.getSound(self.musicSubdir, "incompetech-cyborg_ninja")),
            ('BOSS', self.getSound(self.musicSubdir, "incompetech-blockman")),
            ('LOSE', self.getSound(self.musicSubdir, "incompetech-danger_storm")),
            ('WIN', self.getSound(self.musicSubdir, "incompetech-show_your_moves"))
        ])
        print("sound engine: music loaded")


    def loadEffect(self):
        print("sound engine: loading effect...")
        self.effect = dict([
            ('TEST', self.getSound(self.effectSubdir, "pocketTestTone"))
        ])
        print("sound engine: effect loaded")

        
    def getSound(self, subdir, soundName):
        return pygame.mixer.Sound(self.getFilepath(subdir, soundName))

     
    def getFilepath(self, subdir, soundName):
        result = os.path.realpath(os.getcwd()) + self.soundSubdir + subdir + soundName + self.container
        return result


    def setMusic(self, musicName):
        print("sound engine: switching music to " + musicName)
        self.stopMusic()
        self.currentMusic = musicName
        self.startMusic()


    def playEffect(self, effectName):
        self.effect[effectName].play(0,3000,0)


    def stopMusic(self):
        self.music[self.currentMusic].fadeout(self.fadeDuration)


    def startMusic(self):
        self.music[self.currentMusic].play(-1, 0, self.fadeDuration)

description:

This sound engine simplifies the loading of songs and sound effects. It allows us to change songs at random, and without worry of having two playing at once (remember MC-35856? that’s still a thing)

When the game starts, it credits Kevin MacLeod for his amazing music used under a Creative Commons Attribution license v3.0.

When I put the soundtrack together I was able to squeeze everything down to 32kbps or so with Ogg Vorbis, which became my new favorite audio codec. So the game takes only a second to launch.

 

During my stay at Rose, I improved my ability to work in a group with other programmers. And rediscovered that it’s fun. I gained a fascination for python. There is no language but python. All other languages are trash. I would delete them from my mind if python were faster by a factor of just a few hundred. (no, just kidding. I never delete anything)

At the end, we were awarded 5th place for best project, out of more than 40, spanning many fields of engineering.