Build a bell ringing simulator
By Russell Barnes. Posted
Advertisement
Christmas is coming – keep Santa on the right track with our Raspberry Pi gift guide!
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.
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.
Alternatively, these lists of sequences can be shown horizontally, known as a roller, Figure 3,
Or even circularly as a ring, Figure 4.
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.
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()
Russell runs Raspberry Pi Press, which includes The MagPi, Hello World, HackSpace magazine, and book projects. He’s a massive sci-fi bore.
Subscribe to Raspberry Pi Official Magazine
Save up to 37% off the cover price and get a FREE Raspberry Pi Pico 2 W with a subscription to Raspberry Pi Official Magazine.
More articles
Christmas Gift Guide in Raspberry Pi Official Magazine 160
There’s a ton of great stuff in issue 160, including the incredible motion scanner, inspired by the film Aliens. It’s beautiful, it uses Raspberry Pi technology and makes a satisfying ping sound, and the best thing about it is that it actually works, thanks to a DreamHAT+ radar board. Yes, you too can join the […]
Read more →
Win a Raspberry Pi 500+ and Raspberry Pi Monitor!
Fancy getting the most powerful Raspberry Pi desktop setup? Raspberry Pi 500+ and Raspberry Pi monitor pair perfectly together for a portable – and fixed – desktop computer, powered by Raspberry Pi. We have a set to give away, and you can enter below. A Raspberry Pi 500+ & Raspberry Pi Monitor
Read more →
All right all right!! Artificial Intelligence, Hollywood style
When we get AI right, odds on it’ll be thanks to small firms, motivated individuals, and Raspberry Pi
Read more →
Sign up to the newsletter
Get every issue delivered directly to your inbox and keep up to date with the latest news, offers, events, and more.




