CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Event-Based Animations in Tkinter
Optional, Part 4: More Events, Images, Sound, Modes, etc
- These notes are optional this semester, but you may find these topics very useful for your term projects.
- These notes require that you are using cmu_112_graphics version 0.9.0 or later.
- Installing required modules (PIL/Pillow and Requests)
- Keyboard Shortcuts
- Events
- Input and Output Methods
- Image Methods
- loadImage and scaleImage (using url)
- loadImage and scaleImage (using local file)
- Using image.size
- Using transpose to flip an image
- Using getpixel and putpixel
- Making new images and using ImageDraw
- getSnapshot and saveSnapshot
- Spritesheets using Pillow/PIL methods (such as image.crop)
- Caching PhotoImages for increased speed
- Using Modes (aka Screens)
- Example: Sidescrollers 1-3
- Playing Sounds
- To run these examples, first download cmu_112_graphics.py and be sure it is in the same folder as the file you are running.
- That file has version numbers. As we release updates, be sure you are using the most-recent version!
- This is a new animation framework. Which means it may have some bugs that we will fix as we go. Also, it is similar to previous semesters, but different in important ways. Be aware of this if you are reviewing previous semesters' materials!
- As with Tkinter graphics, the examples here will not run using Brython in your browser.
- Installing required modules (PIL/Pillow and Requests)
To use cmu_112_graphics.py, you need to have some modules installed. If they are not installed, you will see a message like this (or a similar one for "requests" instead of "PIL"):********************************************************** ** Cannot import PIL -- it seems you need to install pillow ** This may result in limited functionality or even a runtime error. **********************************************************
You can try to use 'pip' to install the missing modules, but it can be complicated making sure you are installing these modules for the same version of Python that you are running. Here are some more-reliable steps that should work for you:- For Windows:
- Run this Python code block in your main Python file (it will print the commands you need to paste into your command prompt):
import sys print(f'"{sys.executable}" -m pip install pillow') print(f'"{sys.executable}" -m pip install requests')
- Open Command Prompt as an administrator user (right click - run as administrator)
- Copy-paste each of the two commands printed in step 1 into the command prompt you opened in step 2.
- Close the command prompt and close Python.
- Re-open Python, and you're set (hopefully)!
- Run this Python code block in your main Python file (it will print the commands you need to paste into your command prompt):
- For Mac or Linux:
- Run this Python code block in your main Python file (it will print the commands you need to paste into your command prompt):
import sys print(f'sudo "{sys.executable}" -m pip install pillow') print(f'sudo "{sys.executable}" -m pip install requests')
- Open Terminal
- Copy-paste each of the two commands printed in step 1 into the
command prompt you opened in step 2.
- If you see a lock and a password is requested, type in the same password that you use to log into your computer.
- Close the terminal and close Python.
- Re-open Python, and you're set (hopefully)!
- Run this Python code block in your main Python file (it will print the commands you need to paste into your command prompt):
If these steps do not work for you, please go to OH and we will be happy to assist. - For Windows:
- Keyboard Shortcuts
from cmu_112_graphics import * def appStarted(app): app.counter = 0 def timerFired(app): app.counter += 1 def redrawAll(app, canvas): canvas.create_text(200, 50, text='Keyboard Shortcut Demo') canvas.create_text(200, 100, text='Press control-p to pause/unpause') canvas.create_text(200, 150, text='Press control-s to save a snapshot') canvas.create_text(200, 200, text='Press control-q to quit') canvas.create_text(200, 250, text='Press control-x to hard exit') canvas.create_text(200, 300, text=f'{app.counter}') runApp(width=400, height=400) # quit still runs next one, exit does not runApp(width=600, height=600)
- Events
from cmu_112_graphics import * def appStarted(app): app.messages = ['appStarted'] def appStopped(app): app.messages.append('appStopped') print('appStopped!') def keyPressed(app, event): app.messages.append('keyPressed: ' + event.key) def keyReleased(app, event): app.messages.append('keyReleased: ' + event.key) def mousePressed(app, event): app.messages.append(f'mousePressed at {(event.x, event.y)}') def mouseReleased(app, event): app.messages.append(f'mouseReleased at {(event.x, event.y)}') def mouseMoved(app, event): app.messages.append(f'mouseMoved at {(event.x, event.y)}') def mouseDragged(app, event): app.messages.append(f'mouseDragged at {(event.x, event.y)}') def sizeChanged(app): app.messages.append(f'sizeChanged to {(app.width, app.height)}') def redrawAll(app, canvas): font = 'Arial 20 bold' canvas.create_text(app.width/2, 30, text='Events Demo', font=font) n = min(10, len(app.messages)) i0 = len(app.messages)-n for i in range(i0, len(app.messages)): canvas.create_text(app.width/2, 100+50*(i-i0), text=f'#{i}: {app.messages[i]}', font=font) runApp(width=600, height=600)
- Input and Output Methods
# This demos app.getUserInput(prompt) and app.showMessage(message) from cmu_112_graphics import * def appStarted(app): app.message = 'Click the mouse to enter your name!' def mousePressed(app, event): name = app.getUserInput('What is your name?') if (name == None): app.message = 'You canceled!' else: app.showMessage('You entered: ' + name) app.message = f'Hi, {name}!' def redrawAll(app, canvas): font = 'Arial 24 bold' canvas.create_text(app.width/2, app.height/2, text=app.message, font=font) runApp(width=500, height=300)
- Image Methods
- loadImage and scaleImage (using url)
# This demos loadImage and scaleImage from a url from cmu_112_graphics import * def appStarted(app): url = 'https://tinyurl.com/great-pitch-gif' app.image1 = app.loadImage(url) app.image2 = app.scaleImage(app.image1, 2/3) def redrawAll(app, canvas): canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1)) canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2)) runApp(width=700, height=600)
- loadImage and scaleImage (using local file)
Let's do that again, only this time using an image stored locally. To run this version, you must first download this image (testImage2.gif) and save it in the same folder as your Python code:# This demos loadImage and scaleImage from a local file from cmu_112_graphics import * def appStarted(app): app.image1 = app.loadImage('testImage2.gif') app.image2 = app.scaleImage(app.image1, 2/3) def redrawAll(app, canvas): canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1)) canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2)) runApp(width=700, height=600) - Using image.size
# This demos using image.size from cmu_112_graphics import * def appStarted(app): url = 'https://tinyurl.com/great-pitch-gif' app.image1 = app.loadImage(url) app.image2 = app.scaleImage(app.image1, 2/3) def drawImageWithSizeBelowIt(app, canvas, image, cx, cy): canvas.create_image(cx, cy, image=ImageTk.PhotoImage(image)) imageWidth, imageHeight = image.size msg = f'Image size: {imageWidth} x {imageHeight}' canvas.create_text(cx, cy + imageHeight/2 + 20, text=msg, font='Arial 20 bold') def redrawAll(app, canvas): drawImageWithSizeBelowIt(app, canvas, app.image1, 200, 300) drawImageWithSizeBelowIt(app, canvas, app.image2, 500, 300) runApp(width=700, height=600)
- Using transpose to flip an image
# This demos using transpose to flip an image from cmu_112_graphics import * def appStarted(app): url = 'https://tinyurl.com/great-pitch-gif' app.image1 = app.loadImage(url) app.image2 = app.image1.transpose(Image.FLIP_LEFT_RIGHT) def redrawAll(app, canvas): canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1)) canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2)) runApp(width=700, height=600)
- Using getpixel and putpixel
# This demos using getpixel and putpixel from cmu_112_graphics import * def appStarted(app): url = 'https://tinyurl.com/great-pitch-gif' app.image1 = app.loadImage(url) # now let's make a copy that only uses the red part of each rgb pixel: app.image1 = app.image1.convert('RGB') app.image2 = Image.new(mode='RGB', size=app.image1.size) for x in range(app.image2.width): for y in range(app.image2.height): r,g,b = app.image1.getpixel((x,y)) app.image2.putpixel((x,y),(r,0,0)) def redrawAll(app, canvas): canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1)) canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2)) runApp(width=700, height=600)
- Making new images and using ImageDraw
Now let's look see how to create a new image (rather than load an image), and to use the ImageDraw module to draw (lines, etc) on an image:# This demos creating a new blank image and using PIL ImageDraw from cmu_112_graphics import * def appStarted(app): imageWidth, imageHeight = app.width//3, app.height//2 bgColor = (0, 255, 255) # cyan app.image1 = Image.new('RGB', (imageWidth, imageHeight), bgColor) # Now that we created the image, let's use ImageDraw to draw in it # See https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html draw = ImageDraw.Draw(app.image1) draw.line((0, 0, imageWidth, imageHeight), width=10, fill=(255, 0, 0)) draw.line((0, imageHeight, imageWidth, 0), width=10, fill=(0, 0, 255)) # And now we will create a scaled copy to show this is a normal image app.image2 = app.scaleImage(app.image1, 2/3) def redrawAll(app, canvas): canvas.create_image(200, 300, image=ImageTk.PhotoImage(app.image1)) canvas.create_image(500, 300, image=ImageTk.PhotoImage(app.image2)) runApp(width=700, height=600) - getSnapshot and saveSnapshot
Now let's look at getSnapshot and saveSnapshot: Note: this feature does not work especially well on some platforms.# This demos getSnapshot and saveSnapshot from cmu_112_graphics import * def appStarted(app): app.image = None def keyPressed(app, event): if (event.key == 'g'): snapshotImage = app.getSnapshot() app.image = app.scaleImage(snapshotImage, 0.4) elif (event.key == 's'): app.saveSnapshot() def redrawAll(app, canvas): canvas.create_text(350, 20, text='Press g to getSnapshot') canvas.create_text(350, 40, text='Press s to saveSnapshot') canvas.create_rectangle(50, 100, 250, 500, fill='cyan') if (app.image != None): canvas.create_image(525, 300, image=ImageTk.PhotoImage(app.image)) runApp(width=700, height=600) - Spritesheets using Pillow/PIL methods (such as image.crop)
We can use Pillow/PIL methods such as image.crop() (among many others!), which we use here to use a spritestrip (a kind of spritesheet) by cropping each sub-image from this single image:
# This demos sprites using Pillow/PIL images # See here for more details: # https://pillow.readthedocs.io/en/stable/reference/Image.html # This uses a spritestrip from this tutorial: # https://www.codeandweb.com/texturepacker/tutorials/how-to-create-a-sprite-sheet from cmu_112_graphics import * def appStarted(app): url = 'http://www.cs.cmu.edu/~112/notes/sample-spritestrip.png' spritestrip = app.loadImage(url) app.sprites = [ ] for i in range(6): sprite = spritestrip.crop((30+260*i, 30, 230+260*i, 250)) app.sprites.append(sprite) app.spriteCounter = 0 def timerFired(app): app.spriteCounter = (1 + app.spriteCounter) % len(app.sprites) def redrawAll(app, canvas): sprite = app.sprites[app.spriteCounter] canvas.create_image(200, 200, image=ImageTk.PhotoImage(sprite)) runApp(width=400, height=400) - Caching PhotoImages for increased speed
If you are using a lot of images, then the call toImageTk.PhotoImage(image)
can slow things down, so we can cache the results of that, like so:# This demos caching PhotoImages for increased speed # when using a LOT of images (2500 here) from cmu_112_graphics import * import time def make2dList(rows, cols): return [ ([0] * cols) for row in range(rows) ] def appStarted(app): url = 'https://tinyurl.com/great-pitch-gif' app.image1 = app.loadImage(url) app.margin = 20 app.rows = app.cols = 50 app.images = make2dList(app.rows, app.cols) for row in range(app.rows): for col in range(app.cols): app.images[row][col] = app.scaleImage(app.image1, 0.1) app.counter = 0 app.timerDelay = 1 app.timerResult = 'Counting to 10...' app.useCachedImages = False resetTimer(app) def resetTimer(app): app.time0 = time.time() app.counter = 0 def timerFired(app): app.counter += 1 if (app.counter == 10): duration = time.time() - app.time0 app.timerResult = f'Last time to 10: {round(duration,1)}s' app.useCachedImages = not app.useCachedImages resetTimer(app) # from www.cs.cmu.edu/~112/notes/notes-animations-part1.html#exampleGrids def getCellBounds(app, row, col): # aka "modelToView" # returns (x0, y0, x1, y1) corners/bounding box of given cell in grid gridWidth = app.width - 2*app.margin gridHeight = app.height - 2*app.margin columnWidth = gridWidth / app.cols rowHeight = gridHeight / app.rows x0 = app.margin + col * columnWidth x1 = app.margin + (col+1) * columnWidth y0 = app.margin + row * rowHeight y1 = app.margin + (row+1) * rowHeight return (x0, y0, x1, y1) def getCachedPhotoImage(app, image): # stores a cached version of the PhotoImage in the PIL/Pillow image if ('cachedPhotoImage' not in image.__dict__): image.cachedPhotoImage = ImageTk.PhotoImage(image) return image.cachedPhotoImage def redrawAll(app, canvas): for row in range(app.rows): for col in range(app.cols): (x0, y0, x1, y1) = getCellBounds(app, row, col) cx, cy = (x0 + x1)/2, (y0 + y1)/2 image = app.images[row][col] if (app.useCachedImages): photoImage = getCachedPhotoImage(app, image) else: photoImage = ImageTk.PhotoImage(image) canvas.create_image(cx, cy, image=photoImage) canvas.create_rectangle(app.width/2-250, app.height/2-100, app.width/2+250, app.height/2+100, fill='lightYellow') canvas.create_text(app.width/2, app.height/2-50, text=f'Using cached images = {app.useCachedImages}', font='Arial 30 bold') canvas.create_text(app.width/2, app.height/2, text=app.timerResult, font='Arial 30 bold') canvas.create_text(app.width/2, app.height/2+50, text=str(app.counter), font='Arial 30 bold') runApp(width=700, height=600)
- loadImage and scaleImage (using url)
- Using Modes (aka Screens)
# This demos using modes (aka screens). from cmu_112_graphics import * import random ########################################## # Splash Screen Mode ########################################## def splashScreenMode_redrawAll(app, canvas): font = 'Arial 26 bold' canvas.create_text(app.width/2, 150, text='This demos a ModalApp!', font=font) canvas.create_text(app.width/2, 200, text='This is a modal splash screen!', font=font) canvas.create_text(app.width/2, 250, text='Press any key for the game!', font=font) def splashScreenMode_keyPressed(app, event): app.mode = 'gameMode' ########################################## # Game Mode ########################################## def gameMode_redrawAll(app, canvas): font = 'Arial 26 bold' canvas.create_text(app.width/2, 20, text=f'Score: {app.score}', font=font) canvas.create_text(app.width/2, 60, text='Click on the dot!', font=font) canvas.create_text(app.width/2, 100, text='Press h for help screen!', font=font) canvas.create_text(app.width/2, 140, text='Press v for an MVC Violation!', font=font) canvas.create_oval(app.x-app.r, app.y-app.r, app.x+app.r, app.y+app.r, fill=app.color) if app.makeAnMVCViolation: app.ohNo = 'This is an MVC Violation!' def gameMode_timerFired(app): moveDot(app) def gameMode_mousePressed(app, event): d = ((app.x - event.x)**2 + (app.y - event.y)**2)**0.5 if (d <= app.r): app.score += 1 randomizeDot(app) elif (app.score > 0): app.score -= 1 def gameMode_keyPressed(app, event): if (event.key == 'h'): app.mode = 'helpMode' elif (event.key == 'v'): app.makeAnMVCViolation = True ########################################## # Help Mode ########################################## def helpMode_redrawAll(app, canvas): font = 'Arial 26 bold' canvas.create_text(app.width/2, 150, text='This is the help screen!', font=font) canvas.create_text(app.width/2, 250, text='(Insert helpful message here)', font=font) canvas.create_text(app.width/2, 350, text='Press any key to return to the game!', font=font) def helpMode_keyPressed(app, event): app.mode = 'gameMode' ########################################## # Main App ########################################## def appStarted(app): app.mode = 'splashScreenMode' app.score = 0 app.timerDelay = 50 app.makeAnMVCViolation = False randomizeDot(app) def randomizeDot(app): app.x = random.randint(20, app.width-20) app.y = random.randint(20, app.height-20) app.r = random.randint(10, 20) app.color = random.choice(['red', 'orange', 'yellow', 'green', 'blue']) app.dx = random.choice([+1,-1])*random.randint(3,6) app.dy = random.choice([+1,-1])*random.randint(3,6) def moveDot(app): app.x += app.dx if (app.x < 0) or (app.x > app.width): app.dx = -app.dx app.y += app.dy if (app.y < 0) or (app.y > app.height): app.dy = -app.dy runApp(width=600, height=500)
- Example: Sidescrollers 1-3
- SideScroller1
# SideScroller1: from cmu_112_graphics import * import random def appStarted(app): app.scrollX = 0 app.dots = [(random.randrange(app.width), random.randrange(60, app.height)) for _ in range(50)] def keyPressed(app, event): if (event.key == "Left"): app.scrollX -= 5 elif (event.key == "Right"): app.scrollX += 5 def redrawAll(app, canvas): # draw the player fixed to the center of the scrolled canvas cx, cy, r = app.width/2, app.height/2, 10 canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='cyan') # draw the dots, shifted by the scrollX offset for (cx, cy) in app.dots: cx -= app.scrollX # <-- This is where we scroll each dot!!! canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='lightGreen') # draw the x and y axes x = app.width/2 - app.scrollX # <-- This is where we scroll the axis! y = app.height/2 canvas.create_line(x, 0, x, app.height) canvas.create_line(0, y, app.width, y) # draw the instructions and the current scrollX x = app.width/2 canvas.create_text(x, 20, text='Use arrows to move left or right') canvas.create_text(x, 40, text=f'app.scrollX = {app.scrollX}') runApp(width=300, height=300)
- SideScroller2
# SideScroller2: # Now with a scroll margin, so player does not stay fixed # at the center of the scrolled canvas, and we only scroll # if the player's center (in this case) gets closer than the # margin to the left or right edge of the canvas. from cmu_112_graphics import * import random def appStarted(app): app.scrollX = 0 app.scrollMargin = 50 app.playerX = app.width//2 # player's center app.dots = [(random.randrange(app.width), random.randrange(60, app.height)) for _ in range(50)] def makePlayerVisible(app): # scroll to make player visible as needed if (app.playerX < app.scrollX + app.scrollMargin): app.scrollX = app.playerX - app.scrollMargin if (app.playerX > app.scrollX + app.width - app.scrollMargin): app.scrollX = app.playerX - app.width + app.scrollMargin def movePlayer(app, dx, dy): app.playerX += dx makePlayerVisible(app) def keyPressed(app, event): if (event.key == "Left"): movePlayer(app, -5, 0) elif (event.key == "Right"): movePlayer(app, +5, 0) def redrawAll(app, canvas): # draw the player, shifted by the scrollX offset cx, cy, r = app.playerX, app.height/2, 10 cx -= app.scrollX # <-- This is where we scroll the player!!! canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='cyan') # draw the dots, shifted by the scrollX offset for (cx, cy) in app.dots: cx -= app.scrollX # <-- This is where we scroll each dot!!! canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='lightGreen') # draw the x and y axes x = app.width/2 - app.scrollX # <-- This is where we scroll the axis! y = app.height/2 canvas.create_line(x, 0, x, app.height) canvas.create_line(0, y, app.width, y) # draw the instructions and the current scrollX x = app.width/2 canvas.create_text(x, 20, text='Use arrows to move left or right') canvas.create_text(x, 40, text=f'app.scrollX = {app.scrollX}') runApp(width=300, height=300)
- SideScroller3
# SideScroller3: # Now with walls that track when you run into them (but # ignore while you are still crossing them). from cmu_112_graphics import * def appStarted(app): app.scrollX = 0 app.scrollMargin = 50 app.playerX = app.scrollMargin app.playerY = 0 app.playerWidth = 10 app.playerHeight = 20 app.walls = 5 app.wallPoints = [0]*app.walls app.wallWidth = 20 app.wallHeight = 40 app.wallSpacing = 90 # wall left edges are at 90, 180, 270,... app.currentWallHit = -1 # start out not hitting a wall def getPlayerBounds(app): # returns absolute bounds, not taking scrollX into account (x0, y1) = (app.playerX, app.height/2 - app.playerY) (x1, y0) = (x0 + app.playerWidth, y1 - app.playerHeight) return (x0, y0, x1, y1) def getWallBounds(app, wall): # returns absolute bounds, not taking scrollX into account (x0, y1) = ((1+wall) * app.wallSpacing, app.height/2) (x1, y0) = (x0 + app.wallWidth, y1 - app.wallHeight) return (x0, y0, x1, y1) def getWallHit(app): # return wall that player is currently hitting # note: this should be optimized to only check the walls that are visible # or even just directly compute the wall without a loop playerBounds = getPlayerBounds(app) for wall in range(app.walls): wallBounds = getWallBounds(app, wall) if (boundsIntersect(app, playerBounds, wallBounds) == True): return wall return -1 def boundsIntersect(app, boundsA, boundsB): # return l2<=r1 and t2<=b1 and l1<=r2 and t1<=b2 (ax0, ay0, ax1, ay1) = boundsA (bx0, by0, bx1, by1) = boundsB return ((ax1 >= bx0) and (bx1 >= ax0) and (ay1 >= by0) and (by1 >= ay0)) def checkForNewWallHit(app): # check if we are hitting a new wall for the first time wall = getWallHit(app) if (wall != app.currentWallHit): app.currentWallHit = wall if (wall >= 0): app.wallPoints[wall] += 1 def makePlayerVisible(app): # scroll to make player visible as needed if (app.playerX < app.scrollX + app.scrollMargin): app.scrollX = app.playerX - app.scrollMargin if (app.playerX > app.scrollX + app.width - app.scrollMargin): app.scrollX = app.playerX - app.width + app.scrollMargin def movePlayer(app, dx, dy): app.playerX += dx app.playerY += dy makePlayerVisible(app) checkForNewWallHit(app) def sizeChanged(app): makePlayerVisible(app) def mousePressed(app, event): app.playerX = event.x + app.scrollX checkForNewWallHit(app) def keyPressed(app, event): if (event.key == "Left"): movePlayer(app, -5, 0) elif (event.key == "Right"): movePlayer(app, +5, 0) elif (event.key == "Up"): movePlayer(app, 0, +5) elif (event.key == "Down"): movePlayer(app, 0, -5) def redrawAll(app, canvas): # draw the base line lineY = app.height/2 lineHeight = 5 canvas.create_rectangle(0, lineY, app.width, lineY+lineHeight,fill="black") # draw the walls # (Note: should optimize to only consider walls that can be visible now!) sx = app.scrollX for wall in range(app.walls): (x0, y0, x1, y1) = getWallBounds(app, wall) fill = "orange" if (wall == app.currentWallHit) else "pink" canvas.create_rectangle(x0-sx, y0, x1-sx, y1, fill=fill) (cx, cy) = ((x0+x1)/2 - sx, (y0 + y1)/2) canvas.create_text(cx, cy, text=str(app.wallPoints[wall])) cy = lineY + 5 canvas.create_text(cx, cy, text=str(wall), anchor=N) # draw the player (x0, y0, x1, y1) = getPlayerBounds(app) canvas.create_oval(x0 - sx, y0, x1 - sx, y1, fill="cyan") # draw the instructions msg = "Use arrows to move, hit walls to score" canvas.create_text(app.width/2, 20, text=msg) runApp(width=300, height=300)
- SideScroller1
- Playing Sounds
We will wrap this up with two ways to play sounds. There are more sophisticated ways to play sounds. This just meant to be a quick way to quickly get some sounds in your app.
In order to run these examples, first download button.mp3- Playing Sounds with Pygame
Note that you may not use Pygame in general before MVP, but you may use Pygame even before then if it is strictly used for sounds in this way:# This demos playing sounds using Pygame: from cmu_112_graphics import * import pygame class Sound(object): def __init__(self, path): self.path = path self.loops = 1 pygame.mixer.music.load(path) # Returns True if the sound is currently playing def isPlaying(self): return bool(pygame.mixer.music.get_busy()) # Loops = number of times to loop the sound. # If loops = 1 or 1, play it once. # If loops > 1, play it loops + 1 times. # If loops = -1, loop forever. def start(self, loops=1): self.loops = loops pygame.mixer.music.play(loops=loops) # Stops the current sound from playing def stop(self): pygame.mixer.music.stop() def appStarted(app): pygame.mixer.init() app.sound = Sound("button.mp3") def appStopped(app): app.sound.stop() def keyPressed(app, event): if (event.key == 's'): if app.sound.isPlaying(): app.sound.stop() else: app.sound.start() elif (event.key == 'l'): app.sound.start(loops=-1) elif event.key.isdigit(): app.sound.start(loops=int(event.key)) def timerFired(app): pass def redrawAll(app, canvas): canvas.create_text(app.width/2, app.height/2-60, text=f'{app.sound.path} (loops = {app.sound.loops})', font='Arial 30 bold') canvas.create_text(app.width/2, app.height/2-20, text=f'sound is playing = {app.sound.isPlaying()}', font='Arial 30 bold') canvas.create_text(app.width/2, app.height/2+20, text='Press s to start/stop sound', font='Arial 30 bold') canvas.create_text(app.width/2, app.height/2+60, text='Press l to loop sound', font='Arial 30 bold') runApp(width=600, height=200) - Playing Sounds with afplay (Mac only)
If Pygame does not work for you, here is an alternative that only runs on Macs and is a bit slower, but runs without Pygame:# This demos playing sounds using afplay (Mac only) import subprocess, threading, time from cmu_112_graphics import * class Sound(object): def __init__(self, path): self.path = path self.process = None self.loop = False def isPlaying(self): return (self.process is not None) def checkProcess(self): # This method is run inside a separate thread # so the main thread does not hang while this runs. while self.process is not None: if (self.process.poll() is not None): self.process = None else: time.sleep(0.2) if self.loop: self.start(loop=True) def start(self, loop=False): self.stop() self.loop = loop self.process = subprocess.Popen(['afplay', self.path]) threading.Thread(target=self.checkProcess).start() def stop(self): process = self.process self.loop = False self.process = None if (process is not None): try: process.kill() except: pass def appStarted(app): app.sound = Sound('button.mp3') def appStopped(app): app.sound.stop() def keyPressed(app, event): if (event.key == 's'): if app.sound.isPlaying(): app.sound.stop() else: app.sound.start() elif (event.key == 'l'): app.sound.start(loop=True) def redrawAll(app, canvas): canvas.create_text(app.width/2, app.height/2-60, text=f'{app.sound.path} (loop = {app.sound.loop})', font='Arial 30 bold') canvas.create_text(app.width/2, app.height/2-20, text=f'sound is playing = {app.sound.isPlaying()}', font='Arial 30 bold') canvas.create_text(app.width/2, app.height/2+20, text='Press s to start/stop sound', font='Arial 30 bold') canvas.create_text(app.width/2, app.height/2+60, text='Press l to loop sound', font='Arial 30 bold') runApp(width=600, height=200)
- Playing Sounds with Pygame
Notes: