Build a bell ringing simulator

By Russell Barnes. Posted

Once, when writing a Raspberry Pi book, your author used a section heading of ‘Ringing the changes’, to signify that the section was going to look at variations on what went before. He was astonished when his American editor had no idea what that term meant.

In fact, the question ‘what is the term for a bell ringer?’, if asked on the popular TV quiz QI, would provoke the klaxon if you gave the answer ‘campanologist’, as that is what the study of bells is called. The correct answer would be ‘bell ringer’.

Bell ringing has a lot more to do with mathematics than you might first think and, despite its ancient origin, it is an ideal topic to computerise.

This feature was written by Mike Cook and first appeared in The MagPi 65. Click here to download a free digital PDF edition of the magazine.

Ways of bell ringing

While there are many different methods of ringing a set of bells, the two basic ones are change ringing and method ringing. In short, all the bells are rung in turn; this is known as a round. With change ringing, two of the bells in a round swap places for the next round. These bells must be adjacent in the current sequence, because of the very high mass of the bells which results in a limited ability to delay or advance the ringing position in a sequence. Method ringing is similar, but more than one pair of bells can change between any one round. In both systems they start and end with a round going from the highest bell, called the Treble, to the lowest, the Tenor. The bells are numbered, starting with 1 for the Treble. Note that this is the reverse of many systems in music, where the lowest number is reserved for the lowest note.

Normally, there are anything between four and twelve bells, with eight being popular. If tuned, they are usually in the key of C. There are many hundreds of different methods, but the basic rule is that the round starts with the sequence 1 to the highest bell number and ends on that sequence as well, but no other sequence is permitted to repeat. Ideally, all possible sequences must be used; this is called an ‘extent’.

But, for twelve bells there will be 12! (12×11×10×9×8×7×6×5×4×3×2×1) combinations, and that would take over 35 years to ring. The record currently stands at 21 216 changes on the twelve bells of South Petherton Church, near Yeovil, which took 14 hours 26 minutes to complete.

It might come as a surprise, but the dedicated bell ringer is not interested so much in how it sounds, but in learning how to ring a specific pattern. In fact, a lot of the sequences are musically unremarkable and sound a bit like random ringing, even to the trained ear. The real appeal is in the physicality and discipline in getting it right. However, our curiosity got the better of us and we wanted to hear what it sounded like, so we wrote this simulator/player. It simulates change ringing, in that you can direct which bells to swap, but it will also play preprogrammed sequences where ‘one man and his mouse’ would be hard put to keep up any live determination of sequences. These sequences delight in names like Plain Bob Major, Bristol S Maximus, and Grandsire Cinques, to name but three.

Documenting a ring

These rings are documented by writing each successive sequence of bells, with lines connecting the bell numbers so you can see how they change. However, normally there is only one line for one bell to follow, and not all bells are numbered, as shown in Figure 1.

 Figure 1 Normally there is only one line for one bell to follow, and not all bells are numbered

This is understandable, because it is meant for one bell/player, and they just need to know if they have to keep their ringing position the same, or move up or down in the sequence. This shorthand, however, often makes it difficult for a beginner to follow. The full diagrams are normally shown as a vertical list; a full list, resembling braid, is shown in Figure 2.

 Figure 2 A full ringing diagram resembles an intricate braid

Alternatively, these lists of sequences can be shown horizontally, known as a roller, Figure 3,

 Figure 3 Sequences can be shown horizontally, known as a roller

Or even circularly as a ring, Figure 4.

 Figure 4 Sequences can also be shown as a ring

All these pictures were generated by the free-to-use Change Ringing Toolkit and reproduced here by kind permission of the author Steve Scanlon.

Preparing the resources

The first thing we did was to prepare the graphics. We found a royalty-free image of a bell on the internet and rotated it through 90° in eleven stages. At each stage, we used a photo editing package to move the bell’s clapper; the results are shown in Figure 5.

 Figure 5 An array of frames of the bells in motion

The thing to note here is that we want the bell to swing about the pivot point at the top, in order for it to look like a realistic swing. So we have to plot each bell, in the animation, at a different position in the x direction, so that the pivot point ends up in the same spot.

These images were named b0.png to b10.png and put in a folder called swing. The software would then scale this set of images so that each bell had its own sized animation sequence.

Then the sound of eight bells were put in a folder called sounds and named 0.wav to 7.wav. We started with bells recorded from a MIDI sound generator, but eventually replaced these with live recordings, done by a friend, of the bells in St Matthias Church, Leeds.

Finally, we prepared some method files based on classic methods. These are simple text files and consist of the sequence of each round, with a row of ‘-’ signs being used as a comment or blank line to break things up and make it easer to see what is going on. The two methods we have encoded like this are ‘Plain Bob Minor’ and ‘New Year Delight Minor’ and can be found along with the software in the GitHub repository.

The software

The program, bells_play.py, uses the Pygame framework (you can find the code at the end of this article). Most of the parameters – like colour, speed, and the control variables – are defined at the start of the code, just before the main function. The loadResources function does the scaling of each animation sequence and, as this takes some time, when each bell has been processed it is displayed on the screen, to prevent having a long time where nothing seems to happen. It is important to the visual effect that the bell goes through an animated sequence and doesn’t just flip from a bell on one side to a bell on the other, even though each image spends very little time on the screen. The handleMouse function sees if any of the ‘swap icons’ has been clicked and the checkForEvent function is where most of the other control takes place in response to keyboard presses. The drawSequence function displays the current order of the bells, and the showRing function points to the bell currently being rung.

Using the software

The software starts up in the stopped mode; pressing the R key will start it ringing, with the S key stopping the ringing at the end of the current round. It can use four to eight bells, selected by simply pressing the number keys on the keyboard. The + and - keys control the speed of the ringing and the F file key brings up a dialogue box to allow you to load in a specific ring. The A key will turn on and off the automatic swap mode; this is where the swap position is generated at random. When the bells are running, clicking on one of the Swap boxes between two bells will swap then at the end of that round. All the time, the map or documentation of the sequence history is displayed scrolling along the bottom of the window. We liked to turn on the automatic swap mode for a time, then turn it off and manually swap bells to get the sequence back to the start.

Taking it further

For a start, the bell sounds are all mono – it would be interesting to space these out in a stereo field. Also, we have not implemented the ‘calling’ of the bells; that is, calling out the two that need swapping in a round with change ringing. Calling is done in two ways: calling up and calling down. The latter is the simplest, a call of ‘Six to Seven’ will swap bell numbers six and seven; the only complication is that the highest and lowest bells are called ‘Treble’ and ‘Tenor’. Such a list could be taken to any tower and called. Finally, we urge you to have a good look at the toolkit from Steve’s website. If that piques your interest, why not see if there is a bell ringing group in your area and try the real thing?

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()

 

From The MagPi store

Subscribe

Subscribe to the newsletter

Get every issue delivered directly to your inbox and keep up to date with the latest news, offers, events, and more.