Saving beat patterns

In the preceding iteration, we added the capability to define multiple beat patterns. 

However, the beat patterns can be played only on a single script run. When the program is closed and restarted, all previous pattern data is lost.

We need a way to persist or store the beat patterns beyond a single program run. We need the ability to store values in some form of file storage and reload, play, and even edit the patterns. We need some form of object persistence.

Python provides several modules for object persistence. The module that we will use for persistence is called the pickle module. Pickle is a standard library of Python.

An object represented as a string of bytes is called a pickle in Python. Pickling, also known as object serialization, let's us convert our object into a string of bytes. The process of reconstructing the object from the string of bytes is called unpickling or deserialization.

More information about the pickle module is available at http://docs.python.org/3/library/pickle.html.

Let's illustrate it with a simple example:

import pickle
party_menu= ['Bread', 'Salad', 'Bordelaise', 'Wine', 'Truffles']
pickle.dump(party_menu, open("my_menu", "wb"))

First, we serialize or pickle our list, party_menu, using pickle.dump, and save it in an external file, my_menu.

We later retrieve the object using pickle.load:

import pickle
menu= pickle.load( open( "my_menu", "rb" ) )
print(menu) # prints ['Bread', 'Salad', 'Bordelaise', 'Wine', 'Truffles']

Coming back to our drum machine—if we need to store and reuse the beat patterns, we only need to pickle our data structure list, named self.all_patterns. Having saved the object, we can later easily unpickle the file to reconstruct our beat patterns.

We first need to add three top-menu items to our program, as shown in the following screenshot:

The three top-menu items are:

  • File | Load Project
  • File | Save Project
  • File | Exit

While we are creating our menu items, let's also add an About menu item. 

Here, we are particularly interested in saving the project (pickling), and loading the project back (unpickling). The code for menu items is defined in a separate method called create_top_menu, as shown in the following code (see code 3.09.py):

def create_top_menu(self):
self.menu_bar = Menu(self.root)
self.file_menu = Menu(self.menu_bar, tearoff=0)
self.file_menu.add_command(
label="Load Project", command=self.load_project)
self.file_menu.add_command(
label="Save Project", command=self.save_project)
self.file_menu.add_separator()
self.file_menu.add_command(label="Exit", command=self.exit_app)
self.menu_bar.add_cascade(label="File", menu=self.file_menu)
self.about_menu = Menu(self.menu_bar, tearoff=0)
self.about_menu.add_command(label="About",command=self.show_about)
self.menu_bar.add_cascade(label="About", menu=self.about_menu)
self.root.config(menu=self.menu_bar)

The code is self-explanatory. We have created similar menu items in our last two projects. Finally, to display this menu, we call this method from our init_gui() method.

To pickle our object, we first import the pickle module into the current namespace, as follows (3.09.py):

import pickle 

The Save Project menu has a command callback attached to self.save_project, which is where we define the pickling process:

def save_project(self):
saveas_file_name = filedialog.asksaveasfilename
(filetypes = [('Explosion Beat File','*.ebt')],
title="Save project as...")
if saveas_file_name is None: return
pickle.dump( self.all_patterns, open(saveas_file_name, "wb"))
self.root.title(os.path.basename(saveas_file_name) +PROGRAM_NAME)

The description of the code is as follows:

  • The save_project method is called when the user clicks on the Save Project menu; hence, we need to give the user an option to save the project in a file. 
  • We have chosen to define a new file extension (.ebt) to keep track of our beat patterns. This is a completely arbitrary choice of extension name.
  • When the user specifies the filename, it is saved with a .ebt extension. The file contains the serialized list self.all_patterns, which is dumped into the file using pickle.dump.
  • Lastly, the title of the Toplevel window is changed to reflect the filename. 

We are done pickling the object. Let's now code the unpickling process. The unpickling process is handled by a method, load_project, which is called from the Load Project menu, as follows:

  def load_project(self):
file_path = filedialog.askopenfilename(
filetypes=[('Explosion Beat File', '*.ebt')], title='Load Project')
if not file_path:
return
pickled_file_object = open(file_path, "rb")
try:
self.all_patterns = pickle.load(pickled_file_object)
except EOFError:
messagebox.showerror("Error", "Explosion Beat file seems corrupted or invalid !")
pickled_file_object.close()
try:
self.change_pattern()
self.root.title(os.path.basename(file_path) + PROGRAM_NAME)
except:
messagebox.showerror("Error",
"An unexpected error occurred trying to process the beat file")

The description of the code is as follows:

  • When a user clicks on the Load Project menu, it triggers a command callback connected to this load_project method.
  • The first line of the method prompts the user with an Open File window. When the user specifies a previously pickled file with a .ebt extension, the filename is stored in a variable called pickled_file_object.
  • If the filename returned is None because the user cancels the Open File dialog, nothing is done. The file is then opened in read mode, and the contents of the file are read into self.all_patterns using pickle.load.
  • self.all_patterns now contains the list of beat patterns defined in the previous pickle.
  • The file is closed and the first pattern of self.all_patterns is reconstructed by calling our previously defined change_pattern() method.

This should load the first pattern on our drum machine. Try playing any of the patterns, and you should be able to replay the pattern exactly as it was defined at the time of pickling.

Note, however, that the pickled .ebt files are not portable from one computer to another. This is because we have just pickled the file path for our drum files. We have not pickled the actual audio files. So if you try to run the .ebt file on another machine or if the file path to the audio files has changed since the pickling, our code will not be able to load the audio files and will report an error.

The process of pickling uncompressed audio files like those in .wav files, .ogg files, or PCM data is the same as the preceding process. After all, these uncompressed audio files are nothing but lists of numbers.

However, trying to pickle audio files here would require us to deviate a lot from our current topic. Therefore, we have not implemented it here.

Pickling, though great for serialization, is vulnerable to malicious or erroneous data. You may want to pickle only if the data is from a trusted source, or if proper validation mechanisms are in place.
You may also find the json module useful for serializing objects in JSON. Also, the ElementTree and xml.minidom libraries are relevant for parsing XML data.

To end this section, let's complete coding the response to clicking on the About menu item:

def show_about(self):
messagebox.showinfo(PROGRAM_NAME,
"Tkinter GUI Application Development Blueprints")

This is self-explanatory. We have done similar coding in our previous project.

To summarize this iteration, we used Python's built-in pickle module to pickle and unpickle the beat patterns defined by the user.

This now lets us save our beat patterns. We can later load, replay, and edit these saved patterns in our drum machine (see code 3.09.py).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.79.84