Sometimes I think it would be useful to have a metronome when practicing music, so I decided to create one for my computer.
The Basic Metronome
In order to get the sound component to work, you will need a collection of WAV files: tick1.wav, tick2.wav, tick3.wav, ding1.wav, ding2.wav, and ding3.wav. Any short WAV files will work. The ones in this zip file are all from my installation of Microsoft Windows and renamed for this application. Had I a microphone, I would have recorded my own sounds.
First, create the GUI any way you'd like, as long as you have the following components:
A SpinCtrl called 'bpm'
- A button called 'go'
- A button called 'stop' (you can label these buttons anything you want)
Here's what mine looks like:
Here is the code for metronome0.py
1 import wx 2 mspm = 60*1000 # milliseconds per minute 3 class MFrame(wx.Frame): 4 def __init__(self,): 5 wx.Frame.__init__(self,None) 6 #define the components 7 self.go = wx.Button(self,wx.ID_ANY,"Start") 8 self.stop = wx.Button(self,wx.ID_ANY,"Stop") 9 self.bpm = wx.SpinCtrl(self,wx.ID_ANY,value="120",min=0,max=240) 10 #do the layout 11 sizer = wx.BoxSizer(wx.VERTICAL) 12 sizer.Add(wx.StaticText(self,wx.ID_ANY,"Set the beats per minute")) 13 sizer.Add(self.bpm) 14 sizer.Add(self.go) 15 sizer.Add(self.stop) 16 self.SetSizerAndFit(sizer) 17 self.Layout() 18 #Bind events 19 self.Bind(wx.EVT_BUTTON,self.Start,self.go) 20 self.Bind(wx.EVT_BUTTON,self.Stop,self.stop) 21 self.Bind(wx.EVT_TIMER,self.Tick) 22 def Start(self,evt): 23 self.timer=wx.Timer(self) 24 self.timer.Start(mspm/float(self.bpm.GetValue())) 25 print "go!" 26 self.go.Enable(False) 27 self.stop.Enable(True) 28 def Stop(self,evt): 29 self.timer.Stop() 30 print "stop!" 31 del self.timer 32 self.go.Enable(True) 33 self.stop.Enable(False) 34 def Tick(self,evt): 35 print "tick",self.timer.GetInterval(),wx.GetElapsedTime() 36 sound = wx.Sound('tick2.wav') 37 sound.Play(wx.SOUND_ASYNC) 38 wx.YieldIfNeeded() 39 class TestApp(wx.App): 40 def OnInit(self): 41 mainframe = MFrame() 42 self.SetTopWindow(mainframe) 43 mainframe.Show() 44 return 1 45 app = TestApp(0) 46 app.MainLoop()
This code is quick and dirty and it does the trick. Sadly, it's dull, and there's a lot more to learn. Right now the drawbacks are obvious: You cannot change the sound of the tick, and there's no downbeat ding to help keep you on track. If you change the number of beats per minute, you have to stop and start the metronome again to hear the change. Let's fix those faults.
If we want to highlight the downbeats, then we need a separate sound, and we need to keep track of when to play that sound. A quick and dirty fix follows. Change the Start and Tick methods:
1 def Start(self,evt): 2 self.timer=wx.Timer(self) 3 self.count = 0 4 self.timer.Start(mspm/float(self.bpm.GetValue())) 5 self.go.Enable(False) 6 self.stop.Enable(True) 7 def Tick(self,evt): 8 if bool(self.count): 9 sound = wx.Sound('tick2.wav') 10 else: 11 sound = wx.Sound('ding.wav') 12 sound.Play(wx.SOUND_ASYNC) 13 self.count = (self.count + 1) % 4 14 wx.YieldIfNeeded()
That's not satisfying because this only dings once every four beats. So we need to add a control to let us determine the number of beats. I'm using another spin ctrl:
1 class MFrame(wx.Frame): (-) 2 def __init__(self,): 3 wx.Frame.__init__(self,None) 4 #define the components 5 ... 6 self.downbeat = wx.SpinCtrl(self,wx.ID_ANY,value="4",min = 2, max=12) 7 ... 8 sizer.Add(self.stop) 9 s2 = wx.BoxSizer(wx.HORIZONTAL) 10 s2.Add(wx.StaticText(self,wx.ID_ANY,"Beats per measure")) 11 s2.Add(self.downbeat) 12 sizer.Add(s2) 13 def Tick(self,evt): 14 ... 15 self.count = (self.count + 1) % int(self.downbeat.GetValue())
This allows us to change the number of beats in each measure. Notice that it is interactive. It checks with every single tick, so changing that number alters the way the program runs. But it would be nice if this was still optional. There are two choices to get this to work: have the spin control go down to zero (and if zero, never play the ding), or have a check box. I'll use a check box.
Here's the next iteration of this file: metronome1.py and it looks like this:
Adding the flash
The most important thing is to get a large flashing panel on my screen. I plan on watching this thing as well, in case I can't hear it because I'm listening to myself practice. Lets make the following changes:
1 '''metronome2 2 Now try to get a visual component flashing with each beat 3 ''' 4 import wx 5 mspm = 60*1000 # milliseconds per minute 6 7 class MFrame(wx.Frame): 8 def __init__(self,): 9 wx.Frame.__init__(self,None) 10 #define the components 11 ... 12 self.flashcheck = wx.CheckBox(self,wx.ID_ANY,"Flash") 13 self.flash = wx.Panel(self,wx.ID_ANY,size=(150,150)) 14 self.flashing = True 15 self.flashcheck.SetValue(True) 16 self.SetFlash() 17 self.flash.SetBackgroundColour('White') 18 19 ... 20 #do the layout 21 ... 22 sizer.Add(self.flashcheck,0,wx.ALIGN_CENTRE) 23 sizer.Add(self.flash,1,wx.ALL|wx.EXPAND|wx.ALIGN_CENTRE,10) 24 self.SetSizerAndFit(sizer) 25 self.Layout() 26 27 #Bind events 28 ... (nothing changes in this section) 29 def SetFlash(self): 30 if self.flashcheck.IsChecked(): 31 if self.flashing: 32 self.flash.SetBackgroundColour('White') 33 else: 34 self.flash.SetBackgroundColour('Black') 35 self.flash.Refresh() 36 self.flashing = not self.flashing 37 38 ... 39 def Tick(self,evt): 40 ... 41 sound.Play(wx.SOUND_ASYNC) # This line doesn't change 42 self.SetFlash() # This line is new 43 ...
Nothing else changes. Here is metronome2.py and it looks like this:
Changing the tempo
The wx.Timer class sets the number of milliseconds to fire once and only once. That's why the timer has to be stopped (and in the code deleted) and restarted (by creating a fresh one) to change the tempo. wxPython has another class called CallLater that can be restarted with a different delay. The code doesn't change that much, but CallLater behaves differently than Timer. First, it takes the method to call as an argument, so the application isn't responding to repeated wx.TimerEvents. So we can delete the self.Bind(wx.EVT_TIMER,self.Tick) line and change the Start and Tick methods:
1 def Start(self,evt): 2 when = mspm/float(self.bpm.GetValue()) 3 self.timer=wx.CallLater(when,self.Tick) 4 self.count = 0 5 self.go.Enable(False) 6 self.stop.Enable(True) 7 def Tick(self): 8 if bool(self.count): 9 sound = wx.Sound('tick2.wav') 10 else: 11 if self.dingcheck.IsChecked(): 12 sound = wx.Sound('ding.wav') 13 else: 14 sound = wx.Sound('tick2.wav') 15 sound.Play(wx.SOUND_ASYNC) 16 self.SetFlash() 17 self.count = (self.count + 1) % int(self.downbeat.GetValue()) 18 when = mspm/float(self.bpm.GetValue()) 19 self.timer.Restart(when) 20 wx.YieldIfNeeded()
The program doesn't look different, but it behaves differently. After each tick it determines how long to wait before the next tick. Try it. Start the metronome and edit the bpm field to 60. It slows down. Change it to 240 and it speeds up. All without restarting the thing.
Where to go from here
I'll be the first to admit that the metronome panel is pretty ugly, and that the ticking sound is really annoying after a while, so here are some suggestions:
- Make it look nice (I have done this with XRCed)
- Provide an option for turning off the sound
- Provide an option for changing the sound (the zip file has three dings and three ticks)
- Control everything from menus
- Replace the panel with a button than that starts and stops the metronome
- Have the beat count appear in the flash panel/button
Let me know what you think.