×
Namespaces

Variants
Actions

Archived:Python on Symbian/07. Graphics and Multimedia

From Nokia Developer Wiki
Jump to: navigation, search

Archived.pngAquivado: Este artigo foi arquivado, pois o conteúdo não é mais considerado relevante para se criar soluções comerciais atuais. Se você achar que este artigo ainda é importante, inclua o template {{ForArchiveReview|escreva a sua justificativa}}.

All PySymbian articles have been archived. PySymbian is no longer maintained by Nokia and is not guaranteed to work on more recent Symbian devices. It is not possible to submit apps to Nokia Store.

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. With mobile devices becoming increasingly capable of advanced multimedia operations, the creative possibilities are almost endless.

This chapter demonstrates each of these operations through basic examples, and also provides worked examples of credible camera and music player applications.

Drawing on Canvas

The Canvas is the most low-level UI control available in PySymbian. It consists of a drawable area on the screen that other UI components can be displayed upon, 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 (it's a 5-way keypad because it can signal left, right, up, down, and has a central 'select' button). The Canvas has a bind() method that can be used to associate a function with a 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. For these, 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 have the general form method(coordseq[, <options>]) and are explained in detail in the PySymbian documentation.

The following example shows how to draw some shapes, and the resulting screen is shown in Figure 7.2.

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
 
#line((x1,y1,x2,y2), width)
canvas.line((30,45,160,15), 0)
 
#rectangle((x1,y1,x2,y2), color)
canvas.rectangle((30,45,190,130), fill=0xCC55AA)
 
#ellipse((x1,y1,x2,y2), color)
canvas.ellipse((22,250,78,280), fill=0x337700)
 
#point((x,y), (R,G,B), width)
canvas.point((200,180), (243,46,113), width=6)
 
app_lock.wait()
Figure 7.2 Some shapes drawn to the screen

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 following example shows how to draw the text 'Symbian' in a specified font, position and colour, which is shown in Figure 7.3

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
 
#text((x,y), Unicode string, color, font=(name,size,style))
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

We can define a special function to pass a redraw callback to the constructor for the Canvas. It specifies the operations to be performed when the canvas needs to be redrawn (which happens after it is obscured by something else on the screen). Here is a code fragment to illustrate:

...
def handle_redraw(rect):
c.clear(0xFF0000)
c = appuifw.Canvas(redraw_callback=handle_redraw)
appuifw.app.body = c
...

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 as follows:

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
#Other rotation options include ROTATE_180 and ROTATE_270
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" example of editing an image

Screenshot capture

It is often useful to capture screenshots of your application for promotional, debugging or other purposes. This can be done from within a Python application using the graphics module's screenshot() function.

The screenshot() function returns an image object, which can be used to save the image as a JPEG or PNG file (you can specify the type, or Python will work it out from the file name). By default, the image is saved synchronously; a callback can be specified as an optional parameter to save the file asynchronously.

The following example demonstrates capturing and synchronously saving a screenshot as a JPEG file:

import graphics
 
#Take the screenshot
img = graphics.screenshot()
#Save it to the specified path
img.save(u"C:\\Data\\screenshot.jpg")

Using the Camera

PySymbian 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

One of the most fundamental aspects of photography and videography is the ability to frame the subject as desired. The viewfinder allows us to do just that by showing the image that would be captured on the device's screen.

Figure 7.5 The viewfinder in action

The following code snippet demonstrates how to use the viewfinder. It defines a function that places the information received from the camera on the application's body (which is an instance of Canvas) and passes it as an argument to the start_finder() function. The size of the viewfinder can also be specified by passing a tuple of required width/height in pixels to the function's optional size argument.

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 function 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() has a number of (optional) arguments to specify various settings. The full form of the function is take_photo([mode, size, zoom, flash, exposure, white_balance, position]); the detailed meaning of each argument is described in the documentation.

Table 7.1 gives a brief description of each of the arguments:

Table 7.1: Arguments to take_photo()
Argument Description Possible values
mode The display mode of the image 'RGB12', 'RGB16', 'RGB', 'JPEG_Exif', 'JPEG_JFIF'; returned by the image_modes() function.
size The resolution of the image Two-element tuple returned by the image_sizes() function.
zoom The zoom factor From 0 to the value returned by the max_zoom() function.
flash The flash mode setting 'none', 'auto', 'forced', 'fill_in', 'red_eye_reduce'; returned by the flash_modes() function.
exposure The exposure adjustment level 'auto', 'night', 'backlight', 'center'; returned by the exposure_modes() function.
white_balance The color temperature of the current light 'auto', 'daylight', 'cloudy', 'tungsten', 'fluorescent', 'flash'; returned by the white_balance_modes() function.
position The camera used if the device has more than one From 0 to the value returned by the cameras_available() function minus 1.


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.

The following example illustrates how to capture a photo at maximum resolution. As was noted in the previous table, the image_sizes() function returns a list of possible image resolutions. By calling it with a mode as an argument, in our case 'JPEG_Exif', it returns the list of all the resolutions compatible with that mode. 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', camera.image_sizes('JPEG_Exif')[0])
#Save it
f = open(savepath, "wb")
f.write(photo)
f.close()
 
savepath = u"C:\\Data\\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 video is done through the start_record() function of the camera module. It's necessary to start the viewfinder prior to calling the function. The following code snippet uses a timer to record video for 10 seconds, after which time the 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
video_savepath = u"C:\\Data\\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. It pulls together the image capture and video examples discussed in the previous section into a fully functional application and uses tabs to switch between photo and video mode.

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 - camera.max_zoom() / 2 >= 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 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 and define the function to be called when the application is closed. On close, the function has to:

  1. get rid of the tabs as discussed in the tabs section of Chapter 4
  2. stop the viewfinder
  3. release the camera
  4. call the service's signal() method, telling the script to end.

We assign the function to the right softkey, to handle an exit request from the user.

recording = False

This flag keeps 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

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 we use to determine the number with which to name new images and videos. The function simply counts how many photos and videos there are in the location where we save the photos and videos. 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 or number_of_videos) is increased by 1 and appended to the name of the associated file type when it is saved.

Note.pngNote: This method of calculating a unique file number is not robust; If any files are deleted the next count will be the same as a name that has already been used. Production-quality code should further analyze whether each file name exists and continue to increase the counter until a unique file name is reached

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 - camera.max_zoom() / 2 >= 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 zooming 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 display 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(())

In the preceding code we define a function that takes the data from the camera in the form of an image and draws it onto another image (the first phase of double buffering). Then it calls the canvas' redraw callback which puts the image on the canvas, thus displaying it on the screen (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

The callback function will be called with an error code and status information as parameters. It has to be present in order to know that the device is ready to begin recording video. A list of possible states is available in the PySymbian 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 important lines of the 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.

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 complete executing until the right softkey is pressed (and thus the quit() function is called).

Text to speech

The audio module's say() function can be used to speak Unicode text phonetically (in the current device language). This makes it trivially easy to add text to speech capability to your applications. The following example 'plays' the text 'Python on Symbian':

import audio
 
audio.say(u"Python on Symbian")

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()
  • 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 example is a basic music player application. Its features include play/pause functionality, code to increase and decrease the volume, and to display information about the status of the playback. The implementation of pause and resume are noteworthy, since there are no standard methods for them.

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, if there are any
if len(self.list_of_songs) > 0:
try:
self.file_name = self.list_of_songs[appuifw.selection_list(self.list_of_songs)]
except TypeError: #Occurs when the user cancels the selection
pass
else:
appuifw.note(u"No appropriate sound files available", 'info')
 
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:
try: #If play is used before a song is selected, an error occurs
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()
except:
pass
 
#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 example, 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. The current sound object, however, is only defined in the play_pause() method after a sound file is selected.

#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, if there are any
if len(self.list_of_songs) > 0:
try:
self.file_name = self.list_of_songs[appuifw.selection_list(self.list_of_songs)]
except TypeError: #Occurs when the user cancels the selection
pass
else:
appuifw.note(u"No appropriate sound files available", 'info')

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 preceding two functions handle modifying the volume. They are called when the up or down arrow keys are pressed. The volume is increased/decreased by one unit until it reaches maximum/0. Exceptions that may arise from trying to set the volume beyond maximum or below minimum or from trying to modify the volume when no sound object is defined (the set_volume() method belongs to a sound object) are handled with try... except statements.

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:
try: #If play is used before a song is selected, an error occurs
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()
except:
pass

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. This approach is used 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 the information along with the name of the file. The function is called every second with the help of the timer's after() method.

The measure_text() method 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 PySymbian 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.

Summary

This chapter outlined Python's graphics and multimedia capabilities. It showed how to draw primitive shapes, text and images to the screen using the graphics module, how to use the camera module to capture images and record videos, and how to use the audio module recording and playing sound files and for text to speech. The accompanying examples include a camera application supporting both video and still images (similar to those used on some real mobile devices), and a simple but credible music player.


Licence icon cc-by-sa 3.0-88x31.png© 2010 Symbian Foundation Limited. Portions copyright Bogdan Galiceanu, Hamish Willee, Marcelo Barros de Almeida, Mike Jipping, Pankaj Nathani and others in wiki document history list. This document is licensed under the Creative Commons Attribution-Share Alike 2.0 license. See http://creativecommons.org/licenses/by-sa/2.0/legalcode for the full terms of the license.
Note that this content was originally hosted on the Symbian Foundation developer wiki.

This page was last modified on 8 May 2013, at 09:17.
110 page views in the last 30 days.
×