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.

metronomewavfiles.zip

First, create the GUI any way you'd like, as long as you have the following components:

Here's what mine looks like:

mnome0.png

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.

Downbeats

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:

mnome2.png

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:

Let me know what you think.

Metronome (last edited 2008-03-11 10:50:29 by localhost)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.