Symbian developer community

 
wiki

Graphics and Multimedia (Python on Symbian)

From Symbian Developer Community

Jump to: navigation, search

Original Author: Bogdan Galiceanu

This chapter illustrates how to access a device's multimedia capabilities using Python.

Contents

Introduction

Python supports operations for drawing basic primitives and text, capturing, displaying and editing images, and for recording and playing sound. This chapter demonstrates each of these operations through basic examples, and even provides worked examples of credible camera and music player applications.

With mobile devices becoming increasingly capable of advanced multimedia operations, the creative possibilities are almost endless.

Drawing on Canvas

The Canvas is the most low-level UI control available in PyS60. It consists of a drawable area on the screen that other UI components can be displayed on - including basic drawing primitives, text, images, and even camera viewfinder videos.

The canvas can intercept key events from the 5-way keypad present on many Symbian devices - this is a keypad that can signal left, right, up, down, and has a central "select" button. The canvas bind method can be used to associate one of your functions with each key event.

 
canvas.bind(EKeyUpArrow, your_up_function)
canvas.bind(EKeyDownArrow, your_down_function)
canvas.bind(EKeyLeftArrow, your_left_function)
canvas.bind(EKeyRightArrow, your_right_function
canvas.bind(EKeySelect, your_select_function)
 

Some devices, such as those with touch screens, do not have physical softkeys or arrow keys. In this case you can use the canvas to display a virtual directional pad (if the application is in full screen mode, the directional pad is accompanied by two virtual softkeys, as shown in Figure 7.1). The pad can be enabled or disabled by setting appuifw.app.directional_pad to True or False, respectively.

    Figure 7.1 The virtual directional pad

    Applications can draw to the Canvas using basic "graphics primitives", or shapes. The available shapes include line, rectangle, polygon, ellipse, arc, pieslice and point. The methods used to draw shapes and their arguments are explained in detail in the PyS60 documentation.

    The example below shows how to draw a few of these shapes, and is followed by an image of the resulting screen.

     
    import e32, appuifw
     
     
    app_lock = e32.Ao_lock()
    def quit():
    app_lock.signal()
    appuifw.app.exit_key_handler = quit
     
    appuifw.app.screen = 'full'
     
    canvas = appuifw.Canvas()
    appuifw.app.body = canvas
     
    canvas.line((30,45,160,15), 0)
     
    canvas.rectangle((30,45,190,130), fill=0xCC55AA)
     
    canvas.ellipse((22,250,78,280), fill=0x337700)
     
    canvas.point((200,180), (243,46,113), width=6)
     
    app_lock.wait()
     
    Figure 7.2 A few of the shapes that can be drawn


    The Canvas can also be used to draw text; this feature is often used to display text with customized font, size or colour, at arbitrary positions on the screen. The Canvas provides the text method to set the text (font and colour) to be drawn, and measure_text to determine the size and position that would be occupied by some given text if drawn in a specified font.

    The example below shows how to draw the text "Symbian" in a specified font, position and colour, and is followed by a screenshot.

     
    import appuifw, e32
     
     
    app_lock = e32.Ao_lock()
    def quit():
    app_lock.signal()
    appuifw.app.exit_key_handler = quit
     
    canvas = appuifw.Canvas()
    appuifw.app.body = canvas
     
    canvas.text((40,60), u"Symbian", 0x007F00, font=(u'Nokia Hindi S60',45,appuifw.STYLE_BOLD))
     
    app_lock.wait()
     
    Figure 7.3 Customized text written on the canvas

    Displaying and Handling Image Objects

    The Image class, as defined in the graphics module, is used to create, edit, and save images.

    Image objects are created with the new method, specifying the desired size:

     
    import graphics
     
     
    img = graphics.Image.new((360, 640))
     

    An image can be edited in various ways. Text can be added to it, shapes can be drawn on it, and it can be transposed, rotated and resized. The following example shows how all these operations can be performed:

     
    import graphics, appuifw, e32
     
     
    app_lock = e32.Ao_lock()
    def quit():
    app_lock.signal()
    appuifw.app.exit_key_handler = quit
     
    #Open an image
    img = graphics.Image.open("C:\\Data\\my_image.jpg")
     
    #Draw a rectangle on it
    img.rectangle((30,45,110,100), fill=0xCC55AA)
     
    #Rotate it by 90 degrees and resize it to a quarter of the original size
    img = img.transpose(graphics.ROTATE_90)
    img = img.resize((img.size[0]/2, img.size[1]/2))
     
    #Display text on it
    img.text((20, 45), u"Image editing", font="title")
     
    #Save the image
    img.save("C:\\Data\\my_edited_image.jpg", quality=100)
     
    def handle_redraw(rect):
    canvas.blit(img)
     
    #Show the result on the screen
    canvas = appuifw.Canvas(redraw_callback=handle_redraw)
    appuifw.app.body = canvas
     
    app_lock.wait()
     

      Figure 7.4 A "Before and after" illustration of editing an image

      Using the Camera

      PyS60 offers access to the device's camera through the camera module and provides functions to capture and save photos and videos. The viewfinder can be used to display what is being captured. Once the camera is no longer needed, it must be released so that other applications may use it.

      Using the Viewfinder

      The following code snippet demonstrates how to use the viewfinder. Basically, we define a function that places the information received from the camera on the application's body (which is an instance of Canvas) and pass it as an argument to the start_finder function.

       
      import camera, appuifw, e32
       
       
      app_lock = e32.Ao_lock()
      def quit():
      #Close the viewfinder
      camera.stop_finder()
      #Release the camera so that other programs can use it
      camera.release()
      app_lock.signal()
      appuifw.app.exit_key_handler = quit
       
      #Define a for displaying the viewfinder; basically it just displays an image on a Canvas
      def vf(im):
      appuifw.app.body.blit(im)
       
      #Set the application's body to Canvas
      appuifw.app.body = appuifw.Canvas()
       
      #Start the viewfinder and keep the backlight on
      camera.start_finder(vf, backlight_on=1)
       
      app_lock.wait()
       

      Capturing Photos

      Photos are captured with the take_photo function. After capturing a photo, you can save it just as you would an ordinary image.

      take_photo can has a number of (optional) arguments that you can specify to photo settings. The full form of the function is take_photo([mode, size, zoom, flash, exposure, white_balance, position]); the meaning of each argument is described in the documentation.

       
      import camera, appuifw, e32
       
       
      app_lock = e32.Ao_lock()
      def quit():
      #Release the camera so that other programs can use it
      camera.release()
      app_lock.signal()
      appuifw.app.exit_key_handler = quit
       
      #Take the photo
      photo = camera.take_photo('RGB', (1024, 768))
       
      #Save it at maximum quality
      photo.save("C:\\Data\\my_photo", quality=100)
       
      app_lock.wait()
       

      In some newer Symbian devices, in order to be able to capture images at the maximum resolution supported by the camera, one must:

      1. Switch to the landscape mode.
      2. Import the camera module.
      3. Take the picture in the 'JPEG_Exif' format.
      4. Save it by writing its information in a file, not as an Image object.

      This example illustrates how to capture a 5 megapixel picture with a device such as the Nokia N95. The photo can be taken by selecting "Take photo" from the Options menu.

       
      import appuifw, e32
       
       
      #Switch to landscape mode
      appuifw.app.orientation = "landscape"
       
      import camera
       
      app_lock = e32.Ao_lock()
      def quit():
      #Release the camera so that other programs can use it
      camera.release()
      app_lock.signal()
      appuifw.app.exit_key_handler = quit
       
      #Function for taking the picture
      def take_picture():
      #Take the photo
      photo = camera.take_photo('JPEG_Exif', (2592, 1944))
      #Save it
      f = open(savepath, "w")
      f.write(photo)
      f.close()
       
      savepath = u"C:\\photo.jpg"
      #This is the path and name for storing the photo
       
      appuifw.app.body = appuifw.Canvas()
       
      appuifw.app.menu=[(u"Take photo", take_picture)]
       
      app_lock.wait()
       

      Capturing Videos

      Capturing videos is done through the start_record function of the camera module.

      The following code snippet uses a timer to record video for 10 seconds, after which recording stops.

       
      import camera, appuifw, e32
       
       
      app_lock = e32.Ao_lock()
      def quit():
      #Cancel the timer when the user exits, if it has not expired
      timer.cancel()
      #Close the viewfinder
      camera.stop_finder()
      #Release the camera so that other programs can use it
      camera.release()
      app_lock.signal()
      appuifw.app.exit_key_handler = quit
       
      #Function for displaying the viewfinder
      def vf(im):
      appuifw.app.body.blit(im)
       
      #Function that will be passed as an argument to start_record; it is used for
      # handling possible errors and monitoring the current status
      def video_callback(err, current_state):
      global control_light
      if current_state == camera.EPrepareComplete:
      control_light=1
      else:
      pass
       
      #The path where the video file will be saved
      savepath = u"C:\\video.mp4"
       
      appuifw.app.body = appuifw.Canvas()
       
      #Start the viewfinder
      camera.start_finder(vf)
       
      #Start recording
      video = camera.start_record(video_savepath, video_callback)
       
      #Create a timer
      timer = e32.Ao_timer()
       
      #Record for 10 seconds, then stop
      timer.after(10, lambda:camera.stop_record())
       
      app_lock.wait()
       

      PySymCamera - a tabs interface camera example

      Here is an example of an application that can take pictures and record video, similar to the one present in the Nokia 6630.

       
      import camera, appuifw, e32, os, graphics, sysinfo
      from key_codes import *
       
       
      app_lock = e32.Ao_lock()
      def quit():
      appuifw.app.set_tabs([], None)
      camera.stop_finder()
      camera.release()
      app_lock.signal()
      appuifw.app.exit_key_handler = quit
       
      #The "recording" variable keeps track of whether a video is currently being recorded
      recording = False
       
      def number_of_files():
      #The photos and videos will be stored in C:\Data, and the current number is added at the end of each file name
      global number_of_photos, number_of_videos
      number_of_photos = 0
      number_of_videos = 0
      for f in os.listdir("C:\\Data"):
      if os.path.isfile("C:\\Data\\" + f):
      if f.startswith("my_photo") and f.endswith(".jpg"):
      number_of_photos += 1
      if f.startswith("my_video") and f.endswith(".mp4"):
      number_of_videos += 1
       
      #Define the function to be called when the select key is pressed, for taking photos or videos
      def take():
      global recording
       
      if mode == "photocamera":
      number_of_files()
      photo = camera.take_photo('RGB', camera.image_sizes()[0], zoom, flash)
      photo.save("C:\\Data\\my_photo" + str(number_of_photos + 1) + ".jpg")
      photocamera()
      elif mode == "videocamera":
      if not recording:
      number_of_files()
      camera.start_record("C:\\Data\\my_video" + str(number_of_videos + 1) + ".mp4", video_callback)
      recording = True
      elif recording:
      camera.stop_record()
      recording = False
       
      #The functions for increasing and decreasing the zoom level
      def zoom_up():
      global zoom
       
      if mode == "photocamera":
      if zoom + camera.max_zoom() / 2 < camera.max_zoom():
      zoom += camera.max_zoom() / 2
      photo = camera.take_photo('RGB', camera.image_sizes()[-1], zoom, flash='none')
      camera.stop_finder()
      photocamera()
       
      def zoom_down():
      global zoom
       
      if mode == "photocamera":
      if zoom > 0:
      zoom -= camera.max_zoom() / 2
      photo = camera.take_photo('RGB', camera.image_sizes()[-1], zoom, flash='none')
      camera.stop_finder()
      photocamera()
       
      def handle_tabs(index):
      global recording, zoom
      if index == 0:
      if recording:
      camera.stop_record()
      recording = False
      camera.stop_finder()
      zoom = 0
      photocamera()
      if index == 1:
      camera.stop_finder()
      videocamera()
      appuifw.app.set_tabs([u"Photo", u"Video"], handle_tabs)
       
      #Create a new, blank image to be used for double buffering
      img = graphics.Image.new(sysinfo.display_pixels())
       
      #Define a function for displaying the viewfinder
      def vf(im):
      img.blit(im)
      handle_redraw(())
       
      def handle_redraw(rect):
      global canvas
      canvas.blit(img)
       
      canvas = appuifw.Canvas(redraw_callback=handle_redraw)
      appuifw.app.body = canvas
       
      canvas.bind(EKeyUpArrow, zoom_up)
      canvas.bind(EKeyDownArrow, zoom_down)
      canvas.bind(EKeySelect, take)
       
      #Define a callback for video capturing
      def video_callback(err, current_state):
      global control_light
      if current_state == camera.EPrepareComplete:
      control_light=1
      else:
      pass
       
      #Zoom is 0 initially
      zoom = 0
      #Flash is disabled by default
      flash = "none"
      flash_enabled = " "
      flash_disabled = "x "
       
      #A function for enabling and disabling the flash
      def set_flash(option):
      global flash, flash_enabled, flash_disabled
      if option == True:
      flash = "forced"
      flash_enabled = "x "
      flash_disabled = " "
      if option == False:
      flash = "none"
      flash_enabled = " "
      flash_disabled = "x "
      appuifw.app.menu = [(u"Flash", ((flash_enabled + u"Enabled", lambda:set_flash(True)), (flash_disabled + u"Disabled", lambda:set_flash(False))))]
       
      #Define the 2 main functions, one for photo mode and one for video mode
      def photocamera():
      #Define a variable that stores the current mode, used to take action when a key is pressed
      global mode
      mode = "photocamera"
       
      camera.start_finder(vf, backlight_on=1)
       
      appuifw.app.menu = [(u"Flash", ((flash_enabled + u"Enabled", lambda:set_flash(True)), (flash_disabled + u"Disabled", lambda:set_flash(False))))]
       
      def videocamera():
      #Define a variable that stores the current mode, used to take action when a key is pressed
      global mode
      mode = "videocamera"
       
      camera.start_finder(vf, backlight_on=1)
       
      appuifw.app.menu = []
       
      photocamera()
       
      app_lock.wait()
       

      Now let's analyze every part of the code to see what it does and how it works:

       
      app_lock = e32.Ao_lock()
      def quit():
      appuifw.app.set_tabs([], None)
      camera.stop_finder()
      camera.release()
      app_lock.signal()
      appuifw.app.exit_key_handler = quit
       

      First, we handle what happens when closing the application. We create an active object based synchronization service (app_lock, define the function to be called when the application is closed (the function has to get rid of the tabs - see the Tabs section in Chapter 4, stop the viewfinder, release the camera, and call the service's signal method, telling the script to end). Then we assign the function to the right softkey.

      recording = False

      This is how we keep track of whether or not video is currently being recorded. We need this in order to know what to do when the selection key is pressed in video mode (see the part about the take function below).

       
      def number_of_files():
      #The photos and videos will be stored in C:\Data, and the current number is added at the end of each file name
      global number_of_photos, number_of_videos
      number_of_photos = 0
      number_of_videos = 0
      for f in os.listdir("C:\\Data"):
      if os.path.isfile("C:\\Data\\" + f):
      if f.startswith("my_photo") and f.endswith(".jpg"):
      number_of_photos += 1
      if f.startswith("my_video") and f.endswith(".mp4"):
      number_of_videos += 1
       

      This is the function that counts how many photos and videos there are in the location where we save the photos and videos we take. If the name of a file in that location matches a certain format (in this case, if it starts with "my_photo"/"my video" and has the ".jpg"/".mp4" extension), a counter variable is increased. The value of that counter variable (number_of_photos and number_of_videos) will be increased by 1 and appended to the name of every photo and video that is taken and saved.

       
      def take():
      global recording
       
      if mode == "photocamera":
      number_of_files()
      photo = camera.take_photo('RGB', camera.image_sizes()[0], zoom, flash)
      photo.save("C:\\Data\\my_photo" + str(number_of_photos + 1) + ".jpg")
      photocamera()
      elif mode == "videocamera":
      if not recording:
      number_of_files()
      camera.start_record("C:\\Data\\my_video" + str(number_of_videos + 1) + ".mp4", video_callback)
      recording = True
      elif recording:
      camera.stop_record()
      recording = False
       

      This is the most important function in the entire program. It's called when the selection key is pressed, and acts differently depending on whether it was called in "photocamera" mode or "videocamera" mode. In the first case it calculates the numer of photos inside the save directory, takes a photo, saves it and restarts the camera. In the second case, it first checks if video is being recorded. If it isn't, it calculates the number of videos inside the save directory and starts recording. If video is being recorded, it simply stops recording.

       
      def zoom_up():
      global zoom
       
      if mode == "photocamera":
      if zoom + camera.max_zoom() / 2 < camera.max_zoom():
      zoom += camera.max_zoom() / 2
      photo = camera.take_photo('RGB', camera.image_sizes()[-1], zoom, flash='none')
      camera.stop_finder()
      photocamera()
       
      def zoom_down():
      global zoom
       
      if mode == "photocamera":
      if zoom > 0:
      zoom -= camera.max_zoom() / 2
      photo = camera.take_photo('RGB', camera.image_sizes()[-1], zoom, flash='none')
      camera.stop_finder()
      photocamera()
       

      These two functions complement each other. They are used for zooming in and out, respectively. Since setting a zoom level other than 0 only works for taking pictures, we first check if the application is running in "photocamera" mode. If it is, we check if the zoom level can be modified beyond the current level. If it can, the current level is modified by half of the maximum zoom level. After that, a dummy photo is taken and the camera is restarted in order to make the viewfinder show things at the new zoom level.

       
      def handle_tabs(index):
      global recording, zoom
      if index == 0:
      if recording:
      camera.stop_record()
      recording = False
      camera.stop_finder()
      zoom = 0
      photocamera()
      if index == 1:
      camera.stop_finder()
      videocamera()
      appuifw.app.set_tabs([u"Photo", u"Video"], handle_tabs)
       

      This piece of code handles tabs. The function is called whenever the user switches between the two tabs the application uses (one for "photocamera" mode and one for "videocamera" mode). If the photo tab is selected, any ongoing recording is stopped and the camera is restarted in photo mode. If the video tab is selected, since there is no recording to stop, the application simply switches to that tab by restarting in video mode. The last line creates the two tabs.

      img = graphics.Image.new(sysinfo.display_pixels())

      When displaying the viewfinder, the high rate at which data is sent from the camera to the screen can cause flickering. This is solved by applying the concept of double buffering. Instead of displaying the viewfinder data directly on the screen we first place it on img and then display that.

       
      def vf(im):
      img.blit(im)
      handle_redraw(())
       

      Here we define a function that takes the data from the camera in the form of an image and draws it on another image. This is the first phase of double buffering, as explained above. Then it calls the canvas' redraw callback which puts the image on the canvas, thus displaying it on the screen. This is the second phase of double buffering.

       
      canvas.bind(EKeyUpArrow, zoom_up)
      canvas.bind(EKeyDownArrow, zoom_down)
      canvas.bind(EKeySelect, take)
       

      Now we associate certain keys with certain actions. The bind method of the Canvas instance binds a function to a keycode. For example, when the up arrow button is pressed the take function is called.

       
      #Define a callback for video capturing
      def video_callback(err, current_state):
      global control_light
      if current_state == camera.EPrepareComplete:
      control_light=1
      else:
      pass
       

      This callback function will be called with an error code and status information as parameters. It has to be present in order to record video but it's not essential to understand exactly how it works. A list of possible states is available in the PyS60 documentation.

       
      #Zoom is 0 initially
      zoom = 0
      #Flash is disabled by default
      flash = "none"
      flash_enabled = " "
      flash_disabled = "x "
       
      #A function for enabling and disabling the flash
      def set_flash(option):
      global flash, flash_enabled, flash_disabled
      if option == True:
      flash = "forced"
      flash_enabled = "x "
      flash_disabled = " "
      if option == False:
      flash = "none"
      flash_enabled = " "
      flash_disabled = "x "
      appuifw.app.menu = [(u"Flash", ((flash_enabled + u"Enabled", lambda:set_flash(True)), (flash_disabled + u"Disabled", lambda:set_flash(False))))]
       

      Here we specify that the zoom level is initially 0 and that flash is disabled at first. Then we define the set_flash(option) function that enables or disables the flash. The only important lines in the entire function are flash = "forced" and flash = "none" (remember, "forced" and "none" are flash modes) because they set the flash on or off, depending on the Boolean value passed as an argument. The rest is just so it will look good in the menu.

       
      def photocamera():
      #Define a variable that stores the current mode, used to take action when a key is pressed
      global mode
      mode = "photocamera"
       
      camera.start_finder(vf, backlight_on=1)
       
      appuifw.app.menu = [(u"Flash", ((flash_enabled + u"Enabled", lambda:set_flash(True)), (flash_disabled + u"Disabled", lambda:set_flash(False))))]
       

      This is the function that is called when the camera is switched into photo mode. It sets the current mode to "photocamera", starts the viewfinder and sets the menu accordingly.

       
      def videocamera():
      #Define a variable that stores the current mode, used to take action when a key is pressed
      global mode
      mode = "videocamera"
       
      camera.start_finder(vf, backlight_on=1)
       
      appuifw.app.menu = []
       

      This function does the same thing as photocamera, only for video mode.

       
      photocamera()
       
      app_lock.wait()
       

      We simply start the camera application in photo mode and tell the script not to end its execution until the right softkey is pressed (and thus the quit function is called).

      Recording and Playing Sound Files

      Audio can be recorded and played using the audio module. The most commonly used methods of sound objects are open, record, play, stop and close. The supported sound file formats may vary from one device to another. The following code snippet demonstrates how to record a sound file and then play it.

       
      import appuifw, e32, audio
       
       
      app_lock = e32.Ao_lock()
       
      def quit():
      #Close the Sound object
      s.close()
      app_lock.signal()
      appuifw.app.exit_key_handler = quit
       
      file_path = u"C:\\Data\\my_sound_file.mp3"
       
      #Open the sound file
      s = audio.Sound.open(file_path)
       
      #Record for 10 seconds
      s.record()
      e32.ao_sleep(10)
      s.stop()
       
      #Play it indefinitely
      s.play(audio.KMdaRepeatForever)
       
      app_lock.wait()
       

      PyMPlayer - a music player example

      The following sample application shows what a basic music player might look like. Its features include play/pause functionality, increasing and decreasing the volume, and displaying information about the status of the playback. The most interesting things to note are the way we implement pause and resume, since there are no standard methods for them, and how we display the file information in the center of the screen. Every part of the code is explained after the example.

       
      import appuifw, e32, audio, graphics, os, sysinfo
      from key_codes import *
       
       
      app_lock = e32.Ao_lock()
      def quit():
      app_lock.signal()
      appuifw.app.exit_key_handler = quit
       
      class PyMPlayer:
      def __init__(self, path):
      #The path where the player will look for sound files
      self.path = path
      #A variable that keeps track of the current state of the player;
      #None means no song is open, False means playing is paused, and True means a song is playing
      self.playing = None
      #We will use this when coming out of pause in order to know from what point the song should play
      self.pickup_time = 0
      #Create an image the size of the screen
      self.img = graphics.Image.new(sysinfo.display_pixels())
      #Instantiate a Canvas and set it as the application's body, and bind the keys that control playback options
      self.canvas = appuifw.Canvas(redraw_callback=self.handle_redraw)
      self.canvas.bind(EKeyUpArrow, self.volume_up)
      self.canvas.bind(EKeyDownArrow, self.volume_down)
      self.canvas.bind(EKeySelect, self.play_pause)
      appuifw.app.body = self.canvas
      appuifw.app.menu = [(u"Pick song", self.select_song)]
      #Instantiate a timer that will be used for updating the info displayed on the screen
      self.timer = e32.Ao_timer()
       
      def handle_redraw(self, rect):
      self.canvas.blit(self.img)
       
      #This function finds all the sound files in a certain directory and displays them as a list for the user to choose from
      def select_song(self):
      self.list_of_songs = []
      for self.i in os.listdir(self.path):
      #Check if the extension is that of a sound file and if it is, add the name to the list
      if self.i[-4:] in [".mp3", ".wav", ".wma"]:
      self.list_of_songs.append(unicode(self.i))
      #Ask the user which file they want to play
      self.file_name = self.list_of_songs[appuifw.selection_list(self.list_of_songs)]
       
      def volume_up(self):
      try:
      self.s.set_volume(self.s.current_volume() + 1)
      except:
      pass
       
      def volume_down(self):
      try:
      self.s.set_volume(self.s.current_volume() - 1)
      except:
      pass
       
      #Here we handle opening a song for the first time and playing/pausing it
      def play_pause(self):
      if self.playing == False:
      self.s.set_position(self.pickup_time)
      self.playing = True
      self.s.play()
      self.show_info()
      elif self.playing == True:
      self.pickup_time = self.s.current_position()
      self.playing = False
      self.s.stop()
      self.timer.cancel()
      if self.playing == None:
      self.s = audio.Sound.open(self.path + "\\" + self.file_name)
      appuifw.app.menu = [(u"Stop", self.stop)]
      self.playing = True
      self.s.play()
      self.show_info()
       
      #This function takes care of stopping playback
      def stop(self):
      appuifw.app.menu = [(u"Pick song", self.select_song)]
      self.s.stop()
      self.s.close()
      self.playing = None
      self.timer.cancel()
       
      #A function that establishes and displays elapsed time and the duration of the song as well as the name of the file
      def show_info(self):
      #Calculate the values
      self.min1, self.sec1 = divmod((self.s.current_position() / 1000000), 60)
      self.min2, self.sec2 = divmod((self.s.duration() / 1000000), 60)
      #Give the values the standard format mm:ss
      if self.min1<10:
      self.info = u"0" + unicode(self.min1)
      else:
      self.info = unicode(self.min1)
      self.info += u":"
      if self.sec1<10:
      self.info += u"0" + unicode(self.sec1)
      else:
      self.info += unicode(self.sec1)
      self.info += u" - "
      if self.min2<10:
      self.info += u"0" + unicode(self.min2)
      else:
      self.info += unicode(self.min2)
      self.info += u":"
      if self.sec2<10:
      self.info += u"0" + unicode(self.sec2)
      else:
      self.info += unicode(self.sec2)
      #Clear the image so things don't overlap
      self.img.clear()
      #Calculate where to display the info so that it's in the center of the screen
      self.text_size = self.img.measure_text(self.file_name, font='title')[0]
      self.text_width = self.text_size[2] - self.text_size[0]
      self.text_height = self.text_size[3] - self.text_size[1]
      self.text_x = (sysinfo.display_pixels()[0] / 2) - (self.text_width / 2)
      self.text_y = (sysinfo.display_pixels()[1] / 2) - self.text_height
      self.img.text((self.text_x, self.text_y), self.file_name, font='title')
      self.text_size = self.img.measure_text(self.info, font='title')[0]
      self.text_width = self.text_size[2] - self.text_size[0]
      self.text_height = self.text_size[3] - self.text_size[1]
      self.text_x = (sysinfo.display_pixels()[0] / 2) - (self.text_width / 2)
      self.text_y = (sysinfo.display_pixels()[1] / 2) - self.text_height
      self.img.text((self.text_x, self.text_y + self.text_height), self.info, font='title')
      self.handle_redraw(())
      #Update the info every second
      self.timer.after(1, self.show_info)
       
      #The class' destructor; here we close the currently open sound file, if any
      def __del__(self):
      try:
      self.s.stop()
      self.s.close()
      except:
      pass
       
      #Create an instance of the player, passing the path where the sound files are as an argument
      player = PyMPlayer(u"C:\\Data")
       
      app_lock.wait()
       

      Upon taking a close look at the code examble above, the first thing to notice is that the music player is implemented as a class, not as a series of functions (classes are discussed at length in Chapter 2). This approach allows the player to be used in other applications by simply importing it like any other class from a module.

       
      def __init__(self, path):
      #The path where the player will look for sound files
      self.path = path
      #A variable that keeps track of the current state of the player; None means no song is open, False means playing is paused, and True means a song is playing
      self.playing = None
      #We will use this when coming out of pause in order to know from what point the song should play
      self.pickup_time = 0
      #Create an image the size of the screen
      self.img = graphics.Image.new(sysinfo.display_pixels())
      #Instantiate a Canvas and set it as the application's body, and bind the keys that control playback options
      self.canvas = appuifw.Canvas(redraw_callback=self.handle_redraw)
      self.canvas.bind(EKeyUpArrow, self.volume_up)
      self.canvas.bind(EKeyDownArrow, self.volume_down)
      self.canvas.bind(EKeySelect, self.play_pause)
      appuifw.app.body = self.canvas
      appuifw.app.menu = [(u"Pick song", self.select_song)]
      #Instantiate a timer that will be used for updating the info displayed on the screen
      self.timer = e32.Ao_timer()
       

      This is the constructor. The data that is needed right after the player starts is defined here.

       
      #This function finds all the sound files in a certain directory and displays them as a list for the user to choose from
      def select_song(self):
      self.list_of_songs = []
      for self.i in os.listdir(self.path):
      #Check if the extension is that of a sound file and if it is, add the name to the list
      if self.i[-4:] in [".mp3", ".wav", ".wma"]:
      self.list_of_songs.append(unicode(self.i))
      #Ask the user which file they want to play
      self.file_name = self.list_of_songs[appuifw.selection_list(self.list_of_songs)]
       

      We use this function to select a song to be played. It searches the directory at the specified path for files whose extension is that of a sound file and when it finds one, it adds it to the list. The list is then displayed using a selection list, the user chooses a file, and thus the file name of the song to be played is established.

       
      def volume_up(self):
      try:
      self.s.set_volume(self.s.current_volume() + 1)
      except:
      pass
       
      def volume_down(self):
      try:
      self.s.set_volume(self.s.current_volume() - 1)
      except:
      pass
       

      The two functions handle modifying the volume. They are called when the up or down arrow keys are pressed. The volume is increased/decreased by 1 until it reaches maximum/0.

       
      def play_pause(self):
      if self.playing == False:
      self.s.set_position(self.pickup_time)
      self.playing = True
      self.s.play()
      self.show_info()
      elif self.playing == True:
      self.pickup_time = self.s.current_position()
      self.playing = False
      self.s.stop()
      self.timer.cancel()
      if self.playing == None:
      self.s = audio.Sound.open(self.path + "\\" + self.file_name)
      appuifw.app.menu = [(u"Stop", self.stop)]
      self.playing = True
      self.s.play()
      self.show_info()
       

      The player can be in one of three states at any given time:

      • No sound file is currently open (self.playing == None). This means that either the player has just been loaded or that playback has been stopped.
      • Playback is paused (self.playing == False).
      • A sound file is playing (self.playing == True).

      The play_pause function handles switching between these states. When pausing the playback, the number of microseconds that have already been played is accessed with the current_position method and stored in self.pickup_time, and the playback is stopped. In order to resume, we specify from where the file should start playing using the set_position method, and we start playing the file. Things are done this way because there is no predefined way of pausing and resuming playback.

       
      def stop(self):
      appuifw.app.menu = [(u"Pick song", self.select_song)]
      self.s.stop()
      self.s.close()
      self.playing = None
      self.timer.cancel()
       

      As the name suggests, this function is used to stop the playback. The sound file is closed, the menu is set to allow the user to pick a different song, and the timer that is used to update the information on the screen is canceled (updating the info when no song is playing would be pointless).

       
      def show_info(self):
      #Calculate the values
      self.min1, self.sec1 = divmod((self.s.current_position() / 1000000), 60)
      self.min2, self.sec2 = divmod((self.s.duration() / 1000000), 60)
      #Give the values the standard format mm:ss
      if self.min1<10:
      self.info = u"0" + unicode(self.min1)
      else:
      self.info = unicode(self.min1)
      self.info += u":"
      if self.sec1<10:
      self.info += u"0" + unicode(self.sec1)
      else:
      self.info += unicode(self.sec1)
      self.info += u" - "
      if self.min2<10:
      self.info += u"0" + unicode(self.min2)
      else:
      self.info += unicode(self.min2)
      self.info += u":"
      if self.sec2<10:
      self.info += u"0" + unicode(self.sec2)
      else:
      self.info += unicode(self.sec2)
      #Clear the image so things don't overlap
      self.img.clear()
      #Calculate where to display the info so that it's in the center of the screen
      self.text_size = self.img.measure_text(self.file_name, font='title')[0]
      self.text_width = self.text_size[2] - self.text_size[0]
      self.text_height = self.text_size[3] - self.text_size[1]
      self.text_x = (sysinfo.display_pixels()[0] / 2) - (self.text_width / 2)
      self.text_y = (sysinfo.display_pixels()[1] / 2) - self.text_height
      self.img.text((self.text_x, self.text_y), self.file_name, font='title')
      self.text_size = self.img.measure_text(self.info, font='title')[0]
      self.text_width = self.text_size[2] - self.text_size[0]
      self.text_height = self.text_size[3] - self.text_size[1]
      self.text_x = (sysinfo.display_pixels()[0] / 2) - (self.text_width / 2)
      self.text_y = (sysinfo.display_pixels()[1] / 2) - self.text_height
      self.img.text((self.text_x, self.text_y + self.text_height), self.info, font='title')
      self.handle_redraw(())
      #Update the info every second
      self.timer.after(1, self.show_info)
       

      Here we simply calculate the duration of the song and how far into it we are, and display this information along with the name of the file. The function is called every second with the help of the timer's after method.

      One thing worth discussing is the measure_text method. It is used to determine how much space (in pixels) a certain text would take up if written in a certain font. The returned result contains a 4-element tuple with the coordinates of the top-left and bottom-right corners of the rectangle that would contain the text. We use the information to determine where to draw the text so that it appears in the middle of the screen. A complete description of the measure_text method is available in the PyS60 documentation.

       
      def __del__(self):
      try:
      self.s.stop()
      self.s.close()
      except:
      pass
       

      An object's __del__ method is called when that object is about to be destroyed. Here we close any open sound file.

      Conclusion

      This chapter has explained how to create Python applications that focus on graphics and multimedia.

      Comments

      Hamishwillee said…

      Hi Bogdan

      Nicely written chapter with some excellent examples.

      I've made a number of changes to the text. Please check these are ok

      • minor modifications to introduction - main point was to add that we can draw text and primitives and play/record video.
      • I've removed the "Example code:" title before code fragments. There were use inconsistently, and IMO are unnecessary in a document like this
      • Improved explanation of the directional pad - adding brief comment on what the pad is and how to binding to keys before jumping into the virtual pad.
      • Lots of small grammatical changes


      Questions/Suggestions

      • Some of the code goes very wide. While we do have the ability to scroll right on screen, this makes it more difficult to read, and in any case won't work in printing. Can we make some of the very long code example lines go over multiple lines?
      • In the drawing section you say "The methods used to draw shapes and their arguments are explained in detail in the PyS60 documentation.". However for me non of the arguments are displayed, which makes the documentation unusable: - e.g. line( coordseq[, tex2html_wrap_inline$<$optionstex2html_wrap_inline$>$])
        • Irrespective, IMO we should at least provide a list of some of the basic graphics primitives and their arguments before sending people off to the further documentation.
      • This and the next example in the graphics section are undocumented - which is inconsistent. I know they are trivial, but I think its useful as you have done in the other sections to add comments. This is particularly important if you're not showing the method arguments, because it gives you an opportunity to explain what the arguments are in the example.
      • The "draw text" example needs to be documented.
        • IMO should also have a link to the specific documentation for fonts on the garage.
        • Ideally also some more about the different font parameters, although I consider that "optional".
        • There was some caveat about what fonts you can use and specifying filenames. It might be nice to have a note/tip or warning about this.
      • In "Displaying and Handling Image Objects" - should we have link to further information in the official documentation at the end. I'm not sure - but would like to have somewhere obvious the other parameters for transposing, rotating etc - could we perhaps instead add the other arguments within the example - e.g. add to the sentence "#Rotate it by 90 degrees and resize it to a quarter of the original size" with "Other rotation options include X, Y, Z ...
      • "Using the viewfinder. " Good section. Implication of code is that we have no control over the size and position of the viewfinder - is this correct? If so, perhaps we should say so.
      • We say "The full form of the function is take_photo([mode, size, zoom, flash, exposure, white_balance, position])" then refer to the documentation. I guess thats fair enough, but perhaps another paragraph describing these a high level wouldn't hurt?
        • YOu might also say in the example that this example uses RGB which is a 12 bit image format and size xxx
        • In summary, I just think we jumped a little fast over this, and the example seems a little isolated from description.
      • Capturing videos
        • It would be good to cover a little more explanation - specifically that video requires you to prepare the camera first - explain the fact you can ignore the other states, and when you couldn't
      • PySymCamera - a tabs interface camera example
        • Can we find a bit more up to date phone than the 6630?
        • The introduction is pretty thin for such an impressive application. Perhaps a little bit more description? Ie that its a tabbed example that takes photo and video from different tabs and is clever enough to work out which tab you're in .... Alternatively "This example pulls together the image capture and video examples discussed in the previous section into a fully functional example"?
      • def number_of_files(): - this will work only if no one deletes a numbered file somewhere in the middle of the set. If someone does this you'll try to save over a higher number (potentially, if the higher number exists). I have no idea what would happen with the save function. I'd consider either fixing this to select the highest number of all the files in the folder based on name OR just add a note to the text explaining that you're aware of the issue. Optional - after all, its clearly and example - but it does look better to identify your own potential bugs.
      • def zoom_up(): and zoom_down look a little buggy. I tihnk that zoom up may never reach the maximum value, it should set to max if thats what the user wants to do. zoom_down can go negative if the max value is high and the current zoom is low - not sure if thats a problem.
      • We say ""When displaying the viewfinder, the high rate at which data is sent from the camera to the screen can cause flickering. " - I'd add "because .." and a brief explanation to the end of that sentence (I think its because the image from the camera may only be partially updated before blitting - double buffering ensures you're always blitting a complete image)
      • Recording and Playing Sound Files
        • I like the use of a class here - should we reference this example from the chapter where we discuss classes?
        • The use of exceptions is similarly handy
        • def volume_up(self): increments in an exception handling loop. I assume this will throw an exception if you reach the maximum and not change it? Its not clear to me that self.s is defined here - is it, and if not, will and exception be thrown for that too (what will happen?). I think its worth noting here that you're using exceptions to cap the max/min and implications of doing so - ie why not just check whether you've reached the max?
        • It was odd to me that self.s wasn't declared until later. May be worth noting this just after where you say " The data that is needed right after the player starts is defined here."

      I'm sure that I got a bit sloppier towards the end, but plenty for you to go on with :-)

      Great job H



      --Hamishwillee 07:09, 9 February 2010 (UTC)

      Sign in to comment…