Refactor experimental demo into final state for developing the game
[base2-runner.git] / engine / Channel.py
diff --git a/engine/Channel.py b/engine/Channel.py
new file mode 100755 (executable)
index 0000000..71c6651
--- /dev/null
@@ -0,0 +1,140 @@
+#!/usr/bin/python3
+# (c) Copyright 2013 TJ <hacker@iam.tj>
+# Licensed on the terms of the GNU General Public License version 3 (see COPYING)
+#
+# Game engine: Sound channels
+
+import os, random, pygame
+import engine
+
+class Channel:
+ """ Represents an audio output. Can represent a user-created SDL channel or the single
+ Music channel (which can load/play Ogg, MP3 and other audio media). The Music channel
+ can be primed with a playlist which will shuffle and loop.
+ Many methods have dual functionality depending on whether the instance object represents
+ the Music channel or a Sound channel, since those PyGame Classes have some differences in
+ functionality.
+ """
+ # use integers to control volume which avoids float rounding error issues
+ MAX = 100
+ MIN = 0
+ volume_step = 5
+ def __init__(self, volume=1.0, channel_id=0, debug=False):
+  """ Constructor.
+   volume:     an SDL/PyGame floating-point with range 0.00 to 1.00. It is stored internally as an 
+               integer with range 0 to 100 to avoid rounding errors when stepping the volume.
+   channel_id: -1 == create the single Music channel. 0,1,2,3... one of several Sound channels.
+   debug:      enables debug messages written to console
+
+   self.channel == None indicates this is the Music channel.
+  """
+  self.volume = int(volume*100)
+  self.channel_id = 0
+  self.debug = debug
+  self.paused = False
+  if channel_id == -1:
+   self.channel_id = channel_id
+   self.channel = None # flag to show this uses pygame.mixer.music 
+  elif channel_id < pygame.mixer.get_num_channels():
+   self.channel_id = channel_id
+   self.channel = pygame.mixer.Channel(self.channel_id)
+
+ def set_volume(self, new_level):
+  """ Ensure the new level is within legal range and alter the mixer level """
+  if new_level >= Channel.MIN and new_level <= Channel.MAX:
+   self.volume = new_level
+   if self.channel == None:
+    pygame.mixer.music.set_volume(self.volume/100)
+   else:
+    self.channel.set_volume(self.volume/100)
+   if self.debug: engine.debug_pr("Channel.set_volume(", self.volume, ")")
+
+ def volume_up(self):
+  """ Increase volume by a single step """
+  if self.volume + self.volume_step <= Channel.MAX:
+   self.set_volume(self.volume + self.volume_step)
+
+ def volume_down(self):
+  """ Decrease volume by a single step """
+  if self.volume - self.volume_step >= Channel.MIN:
+   self.set_volume(self.volume - self.volume_step)
+
+ def get_volume(self):
+  return self.volume
+
+ def queue(self, sound):
+  """ Sound channels can queue one Sound in addition to the currently playing Sound """
+  if self.channel != None:
+   self.channel.queue(sound)
+
+ def get_queue(self):
+  """ Retrieve the queued Sound (if any) """
+  return self.channel.get_queue() if self.channel != None else None
+
+ def fadeout(self, time):
+  """ Fade out the sound nicely
+   time: milliseconds   
+  """
+  if self.channel == None:
+   pygame.mixer.music.fadeout(time)
+  else:
+   self.channel.fadeout(time)
+
+ def play(self, sound, loops=0, maxtime=0, fade_ms=0):
+  """ Play a Music track or Sound
+   sound:
+   loops:    number of repeats (after initial play - see pyGame API docs)
+   maxtime:  seconds to play. 0 == to the end
+   fade_ms:  fade-out time in milliseconds
+  """
+  if self.channel != None:
+   # allow 'sound' to be a path string or an object
+   sound = pygame.mixer.Sound(sound)
+   self.channel.play(sound, loops, maxtime, fade_ms)
+  else:
+   pygame.mixer.music.load(sound)
+   pygame.mixer.music.play(loops)
+
+ def pause(self):
+  """ Toggle the play/pause state of the channel """
+  if self.paused:
+   self.paused = False
+   if self.channel == None:
+    pygame.mixer.music.unpause()
+   else:
+    self.channel.unpause()
+  else:
+   self.paused = True
+   if self.channel == None:
+    pygame.mixer.music.pause()
+   else:
+    self.channel.pause()
+
+ def playlist(self, path):
+  """ Create a playlist from a given file-system directory
+   path:   absolute or relative path to a directory containing *only* valid media files (Ogg, MP3, etc.)
+
+   See pygame.mixer.music API docs
+  """
+  self.music_path = path
+  # build a list of the filenames in the directory (ignores sub-directories)
+  self.playlist = [ f for f in os.listdir(self.music_path) if os.path.isfile(os.path.join(self.music_path, f)) ]
+  random.shuffle(self.playlist) # randomise the play order
+
+  if self.debug: engine.debug_pr("playlist=", self.playlist)
+
+  self.playlist_index = -1 # flag for 'start from the beginning of list'
+  self.playlist_next()     # begin play-back
+  # generate an event when the current track ends
+  pygame.mixer.music.set_endevent(pygame.USEREVENT)
+
+ def playlist_next(self):
+  """ Begin playing the next track in the playlist. Shuffle and loop when it reaches the end of the list. """
+  if self.playlist_index == len(self.playlist)-1:
+   random.shuffle(self.playlist)
+  self.playlist_index = self.playlist_index + 1 if self.playlist_index < len(self.playlist)-1 else 0
+  if self.debug: engine.debug_pr("Channel.playlist_next() = [%d] %s" %(self.playlist_index, self.playlist[self.playlist_index] ))
+
+  pygame.mixer.music.load(os.path.join(self.music_path, self.playlist[self.playlist_index]))
+  pygame.mixer.music.play()
+