Bells - Ring the changes

The game screen appears here if your browser supports the Canvas API.

Use the corresponding number or numpad keys 1 through 8 to ring the bell.

Alternatively, just click the bell you want to ring.

In effect, it is like a slow piano using number or numpad keys or handbells.

Attribution

Mike's Pi Bakery: Ring the Changes, pages 40 - 45, by Mike Cook.

Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported.

Original Python code


#!/usr/bin/env python3
# Pi Ring the changes - bells_play
# By Mike Cook - December 2017

import pygame, time, os, copy, random
from tkinter import filedialog
from tkinter import *

pygame.init()                   # initialise graphics interface
pygame.mixer.quit()
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)

os.environ['SDL_VIDEO_WINDOW_POS'] = 'center'
pygame.display.set_caption("Bells - Ring the changes")
pygame.event.set_allowed(None)
pygame.event.set_allowed([pygame.KEYDOWN,pygame.QUIT,
                          pygame.MOUSEBUTTONDOWN])
screenWidth = 1260 ; screenHight = 482
screen = pygame.display.set_mode([screenWidth,screenHight],0,32)
textHeight=26 ; hangY = 30
font = pygame.font.Font(None, textHeight)
swingSpeed = 0.01 # animation rate
bellX = [60,180,320,460,620,790,973,1160]
backCol = (0,255,255) # background colour
trails = [(255,0,0),(255,255,0),(0,255,0),(0,0,255),
          (0,0,0),(255,128,0), (255,255,255), (32,120,0)]
speed = 0.4 ; running = False ; automatic = False
random.seed() ; ringLength = 8 ; filePlay = False

def main():
   global lastSequence, swapFrom, running, bellSequence
   drawLables()
   resetSequence()
   loadResources()
   print("Ring in the new - press R to ring")
   print("S to stop - F to play a file - C to ring the changes")
   while True:
      checkForEvent()
      if filePlay :
         if running:
            drawControls()
            lastSequence = fSeq[0]
            i=-1
            while i < int(len(fSeq))-1 and running:
               i += 1
               if int(len(fSeq[i])) > 0 :
                  if int(fSeq[i] !=0):
                    bellSequence = fSeq[i]
                    playPeal()
                    drawSequence()
                    lastSequence = copy.deepcopy(bellSequence[:])
            running = False
      else:
         if running:
            playPeal()
            lastSequence = copy.deepcopy(bellSequence[:])
            if swapFrom != -1: # if we need to swap
               bellSequence[swapFrom],bellSequence[swapFrom+1]=bellSequence[swapFrom+1],bellSequence[swapFrom]
            swapFrom = -1 # remove swap call
            drawControls()
            drawSequence()

def playPeal():
   global swapFrom, speed
   for ring in range(0,ringLength):
      showRing(ring)
      swing(bellSequence[ring])
      if ring ==2 and automatic and not(filePlay): # random swap
         swapFrom = random.randint(0,ringLength-2)
         drawControls()
         pygame.display.update()
      checkForEvent()
      time.sleep(speed)

def setMode(mode):
   global filePlay
   filePlay = mode
   if filePlay:
      root = Tk()
      root.filename = filedialog.askopenfilename(initialdir = "/home/pi",
           title = "Select bell method",filetypes = (("txt files","*.txt"),
           ("all files","*.*")))
      loadFile(root.filename)
      root.withdraw()
   else :
      pygame.display.set_caption("Bells - Ring the changes")
      resetSequence()

def loadFile(fileName):
   global fSeq, ringLength
   nameF = open(fileName,"r")
   pygame.display.set_caption("Playing - "+fileName)
   sequenceFile = nameF.readlines()
   ringLength = int(len(sequenceFile[0]) / 2)
   fSeq = [] ; k=-1
   for i in sequenceFile:
      k +=1
      ns = []
      for j in range(0,int(len(sequenceFile[k])),2):
         if i[j:j+1] != '-' and i[j:j+1] != '\n':
            n = int(i[j:j+1])-1 # to get bells 0 to 7
            ns.append(n)
      fSeq.append(ns)
   fSeq.append(ns) # extra line at end
   nameF.close()

def showRing(n): # indicate the current ring point
   pygame.draw.rect(screen,backCol,(524,248,185,16),0)
   drawWords("^",530+n*24,248,(0,0,0),backCol)
   pygame.display.update()

def drawControls(): # draw swap radio buttons
   pygame.draw.rect(screen,backCol,(0,160,screenWidth,20),0)
   if filePlay:
      return
   for n in range(0,ringLength-1):
      if n == swapFrom:
         pygame.draw.rect(screen,(128,32,32),(bellX[n]+10,160,bellX[n+1]-bellX[n]-20,20),0)
         drawWords("<-- Swap -->",bellX[n]+10+n*6,160,(0,0,0),(128,32,32))
      else:
         drawWords("<-- Swap -->",bellX[n]+10+n*6,160,(0,0,0),backCol)
         pygame.draw.rect(screen,(0,0,0),(bellX[n]+10,160,bellX[n+1]-bellX[n]-20,20),1)

def drawSequence(): # display bell sequence
   screen.set_clip(0,260,screenWidth,screenHight-260)
   screen.scroll(-30,0)
   screen.set_clip(0,0,screenWidth,screenHight)
   for n in range(0,ringLength):
      t = -1 ; j = 0
      while t == -1:
         if bellSequence[j] == lastSequence[n]:
            t = j
         j +=1
      pygame.draw.line(screen,trails[lastSequence[n]],(screenWidth-50,screenHight-16-n*24),(screenWidth-30,screenHight-16-t*24),4)
   pygame.draw.rect(screen,backCol,(530,227,179,20),0)
   pygame.draw.rect(screen,backCol,(screenWidth-30,screenHight-200,16,191),0)
   for n in range(0,ringLength):
      drawWords(str(bellSequence[n]+1),530+n*24,227,(0,0,0),backCol) # horizontally
      drawWords(str(bellSequence[n]+1),screenWidth-30,screenHight-(n+1)*24,(0,0,0),backCol) # vertically
   pygame.display.update()

def drawLables():
   global textHeight
   textHeight = 26
   pygame.draw.rect(screen,backCol,(0,0,screenWidth,screenHight),0)
   for n in range(0,8):
      drawWords(str(n+1),bellX[n]-4,0,(0,0,0),backCol)
   textHeight = 36
   drawWords("<---- Sequence ---->",532,207,(0,0,0),backCol)

def swing(bellNumber): # animated bell swing
   global bellState
   if bellState[bellNumber] :
      for pos in range(1,11): # swing one direction
         showBell(bellNumber,pos,pos-1)
         time.sleep(swingSpeed)
      bellState[bellNumber] = 0
   else:
      for pos in range(9,-1,-1): # swing the other direction
         showBell(bellNumber,pos,pos+1)
         time.sleep(swingSpeed)
      bellState[bellNumber] = 1
   samples[bellNumber].play()  # make sound

def showBell(bellNumber,seqNumber,lastBell): # show one frame of the bell
   cRect = bells[bellNumber][lastBell].get_rect()
   cRect.move_ip((bellX[bellNumber]-plotPoints[bellNumber][lastBell][0],
                  hangY-plotPoints[bellNumber][lastBell][1]) )
   pygame.draw.rect(screen,backCol,cRect,0) # clear last bell image
   screen.blit(bells[bellNumber][seqNumber],(bellX[bellNumber]
         -plotPoints[bellNumber][seqNumber][0],
          hangY-plotPoints[bellNumber][seqNumber][1]))
   pygame.display.update()

def drawWords(words,x,y,col,backCol) :
        textSurface = pygame.Surface((14,textHeight))
        textRect = textSurface.get_rect()
        textRect.left = x
        textRect.top = y
        textSurface = font.render(words, True, col, backCol)
        screen.blit(textSurface, textRect)

def loadResources():
   global bells, plotPoints, bellState, samples, swapIcon
   bellState = [1,1,1,1,1,1,1,1]
   scale = [12.0,11.0,10.15,9.42,8.8,8.25,7.76,7.33] # size of bell
   point = [(676, 63),(646, 73),(606, 73),(532, 75),(452, 71),
            (380,67),(290, 71),(214, 61),(154, 57),(118, 77),(114, 75) ]
   plotPoints = []
   bells = []
   for scaledBell in range(0,8):# get images of bells and scale them
      plotPoint = []
      bell = [ pygame.transform.smoothscale(pygame.image.load(
        "swing/b"+str(b)+".png").convert_alpha(),(int(792.0/scale[scaledBell]),
        int(792.0/scale[scaledBell]))) for b in range(0,11)]
      for p in range(0,11):
         p1 = int(point[p][0] / scale[scaledBell])
         p2 = int(point[p][1] / scale[scaledBell])
         plotPoint.append((p1,p2))
      bells.append(bell)
      plotPoints.append(plotPoint)
      showBell(scaledBell,0,0)
   samples = [pygame.mixer.Sound("sounds/"+str(pitch)+".wav")
               for pitch in range(0,8)]

def resetSequence():
   global bellSequence, swapFrom,lastSequence
   bellSequence = [0,1,2,3,4,5,6,7]
   lastSequence = [0,1,2,3,4,5,6,7]
   swapFrom = -1
   pygame.draw.rect(screen,backCol,(0,227,screenWidth,253),0)
   drawControls()
   drawSequence()

def handleMouse(pos): # look at click for swap positions
   global swapFrom
   if filePlay :
      return
   update = False
   if pos[1] > 160 and pos[1] < 180: # swap click
      for b in range(0,ringLength-1):
         if pos[0] > bellX[b]+10 and pos[0] < bellX[b+1]+10 :
            swapFrom = b
            update = True
   if update :
      drawControls()
      pygame.display.update()

def terminate(): # close down the program
    pygame.mixer.quit()
    pygame.quit() # close pygame
    os._exit(1)

def checkForEvent(): # see if we need to quit
    global speed, running,ringLength, automatic
    event = pygame.event.poll()
    if event.type == pygame.QUIT :
         terminate()
    if event.type == pygame.KEYDOWN :
       if event.key == pygame.K_ESCAPE :
          terminate()
       if event.key == pygame.K_RETURN and not filePlay: # reset sequence
          resetSequence()
       if event.key > pygame.K_3 and event.key < pygame.K_9 and not filePlay:
          ringLength = event.key & 0x0f # number of bells
          drawControls()
          drawSequence()
       if event.key == pygame.K_a : # automatic swap
          automatic = not(automatic)
       if event.key == pygame.K_r : # run bell
          running = True
       if event.key == pygame.K_s : # stop bells
          running = False
       if event.key == pygame.K_EQUALS : # reduce speed
          speed -= 0.04
          if speed < .08:
             speed = .08
       if event.key == pygame.K_MINUS : # increase speed
          speed += 0.04
       if event.key == pygame.K_c : # ring changes
          setMode(False)
       if event.key == pygame.K_f : # play a file
          setMode(True)
    if event.type == pygame.MOUSEBUTTONDOWN :
        handleMouse(pygame.mouse.get_pos())

# Main program logic:
if __name__ == '__main__':
    main()