Top

rstem.sound module

This module provides interfaces to the Ready Set STEM CREATOR Kit speaker.

Additionally, it can be used for any audio out over the analog audio jack.

#
# Copyright (c) 2014, Scott Silver Labs, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
'''
This module provides interfaces to the Ready Set STEM CREATOR Kit speaker.

Additionally, it can be used for any audio out over the analog audio jack.
'''

import os
import sys
import time
import re
import io
import select
from functools import partial
from . import soundutil     # c extension
import tempfile
from threading import RLock, Thread, Condition, Event
from queue import Queue, Full, Empty
from subprocess import call, check_output
from struct import pack, unpack
import socket

'''
    Future Sound class member function:
        def seek(self, position, absolute=False, percentage=False)
            - relative +/- seconds
            - absolute +/- seconds (-negative seconds from end)
            - absolute percentage
            - returns previous position, in seconds
'''

STOP, PLAY, FLUSH, STOPPING = range(4)
CHUNK_BYTES = 1024
SOUND_CACHE = '/home/pi/.rstem_sounds'
SOUND_DIR = '/opt/readysetstem/sounds'
MIXER_EXE_BASENAME = 'rstem_mixer'
MIXER_EXE_DIRNAME = '/opt/readysetstem/bin'
MIXER_EXE = os.path.join(MIXER_EXE_DIRNAME, MIXER_EXE_BASENAME)
SERVER_PORT = 8888

def shell_cmd(cmd):
    with open(os.devnull, "w") as devnull:
        call(cmd, stdout=devnull, stderr=devnull, shell=True)

def start_server():
    # start server (if it is not already running)
    shell_cmd('pgrep -c {} || {} &'.format(MIXER_EXE_BASENAME, MIXER_EXE))

    # Force audio to always come out of the analog audio jack.  Some HDMI
    # monitors will cause the audio auto detect to set sound out of HDMI even
    # the connected monitor has no sound (or disabled sound)
    shell_cmd('amixer cset numid=3 1')

    # Wait until server is up
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    for tries in range(30):
        try:
            sock.connect(("localhost", SERVER_PORT))
        except socket.error:
            pass
        else:
            sock.close()
            break
        time.sleep(0.1)

def sound_dir():
    return SOUND_DIR

def master_volume(level):
    if level < 0 or level > 100:
        raise ValueError("level must be between 0 and 100.")

    shell_cmd('amixer sset PCM {}%'.format(int(level)))

def clean_close(sock):
    try:
        sock.shutdown(socket.SHUT_RDWR)
    except socket.error:
        pass
    try:
        sock.close()
    except socket.error:
        pass

class BaseSound(object):
    # Default master volume
    master_volume(100)
    start_server()

    def __init__(self):
        self._SAMPLE_RATE = 44100
        self._BYTES_PER_SAMPLE = 2
        self._CHANNELS = 1
        self._length = 0
        self.gain = 1
        self.internal_gain = 1
        self.start_time = None
        self.stop_play_mutex = RLock()
        self.stopped = Event()
        self.stopped.set()

        # Create play msg queue, with added member function that allows a
        # get_nowait() that can return empty if nothing is available.
        self.play_msg = Queue()
        def get_nowait_noempty():
            try:
                return self.play_msg.get_nowait()
            except Empty:
                return (None, None)
        self.play_msg.get_nowait_noempty = get_nowait_noempty

        self.play_count = 0
        self.play_thread = Thread(target=self.__play_thread)
        self.play_thread.daemon = True
        self.play_thread.start()

    def length(self):
        '''Returns the length of the sound in seconds'''
        return self._length

    def is_playing(self):
        '''Returns `True` if the sound is currently playing'''
        return not self.stopped.is_set()

    def wait(self, timeout=None):
        '''Wait until the sound has finished playing.
        
        If timeout is given (seconds), will return early (after the timeout
        time) even if the sound is not finished playing.

        Returns itself, so this function can be chained.
        '''
        assert self.play_thread.is_alive()
        self.stopped.wait(timeout)
        return self

    def stop(self):
        '''Immediately stop the sound from playing.

        Does nothing if the sound is not currently playing.

        Returns itself, so this function can be chained.
        '''
        assert self.play_thread.is_alive()
        with self.stop_play_mutex:
            self.play_msg.put((STOP, None))
            self.wait()
        return self

    def play(self, loops=1, duration=None):
        '''Starts playing the sound.

        This function starts playing the sound, and returns immediately - the
        sound plays in the background.  To wait for the sound, use `wait()`.
        Because sound functions can be chained, to create, play and wait for a
        sound to complete can be done in one compound command.  For example:

            Sound('mysound.wav').play().wait()

        `loops` is the number of times the sound should be played.  `duration`
        is the length of the sound to play (or `None` to play forever, or until
        the sound ends).

        Returns itself, so this function can be chained.
        '''
        assert self.play_thread.is_alive()
        if duration and duration < 0:
            raise ValueError("duration must be a positive number")
        with self.stop_play_mutex:
            self.stop()
            self.end_time = time.time() 
            previous_play_count = self.play_count
            self.play_msg.put((PLAY, (loops, duration)))

            # Wait until we know the play has started (i.e., the state ===
            # PLAY).  Ugly (polled), but simple.
            while previous_play_count == self.play_count:
                time.sleep(0.001)
        return self

    def __play_thread(self):
        state = STOP
        while True:
            if state == STOP:
                msg, payload = self.play_msg.get()
                if msg == PLAY:
                    self.stopped.clear()
                    self.play_count += 1
                    loops, duration = payload
                    chunk = self._chunker(loops, duration)
                    count = 0
                    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                    sock.connect(("localhost", SERVER_PORT))
                    state = PLAY

            elif state == PLAY:
                msg, payload = self.play_msg.get_nowait_noempty()
                if msg == STOP:
                    state = STOPPING
                else:
                    try:
                        try:
                            header = pack('if', count, self.gain)
                            sock.send(header + next(chunk))
                            count += 1
                        except StopIteration:
                            header = pack('if', -1, 0)
                            sock.send(header)
                            state = FLUSH

                        readable, writable, exceptional = select.select([sock], [], [sock], 0)
                        if readable:
                            c = sock.recv(1)
                            eof = not c or ord(c)
                            if eof:
                                state = FLUSH
                        if exceptional:
                            state = FLUSH
                    except socket.error:
                        state = STOPPING

                # Throttle
                time.sleep(0.005)

            elif state == FLUSH:
                msg, payload = self.play_msg.get_nowait_noempty()
                if msg == STOP:
                    state = STOPPING
                else:
                    # Server will play sound to end and close socket.
                    eof_ack = sock.recv(1)
                    if not eof_ack:
                        state = STOPPING

                # Throttle
                time.sleep(0.005)

            elif state == STOPPING:
                clean_close(sock)
                self.stopped.set()
                state = STOP

    def _time_to_bytes(self, duration):
        if duration == None:
            return None
        samples = duration * self._SAMPLE_RATE
        return samples * self._BYTES_PER_SAMPLE

    @property
    def volume(self):
        '''The volume of the sound object
        
        Each sound object has an volume (independent of the `master_volume()`),
        between 0 (muted) and 100 (loudest).

        The volume is readable/writeable.
        '''
        return round(self.gain * self.internal_gain * 100)

    @volume.setter
    def volume(self, level):
        if level < 0:
            raise ValueError("level must be a positive number")
        self.gain = (level/100)/self.internal_gain

    # dummy chunking function
    def _chunker(self, loops, duration):
        return bytes(CHUNK_BYTES)

class Sound(BaseSound):
    '''
    A Sound object, that plays sounds read in from sound files.

    In addition to the Sound object, this module provides some useful global
    functions:

        master_volume(level):
            Sets the master volume (between 0 and 100) 
            of the audio out.

        sound_dir():
            Returns the sounds dir, where all sound 
            files are stored.
    '''
    
    def __init__(self, filename):
        '''A playable sound backed by the sound file `filename` on disk.
        
        Throws `IOError` if the sound file cannot be read.
        '''
        super().__init__()

        self.bytes = None
        if isinstance(filename, bytes):
            data = filename 
            self.file_opener = partial(io.BytesIO, data)
            byte_length = len(data)

        else:
            # normalize path, raltive to SOUND_DIR
            try:
                filename = os.path.normpath(os.path.join(SOUND_DIR, filename))
            except:
                raise ValueError("Filename '{}' is not valid".format(filename))

            # Is it a file?  Not a definitive test here, but used as a courtesy to
            # give a better error when the filename is wrong.
            if not os.path.isfile(filename):
                raise IOError("Sound file '{}' cannot be found".format(filename))

            # Create cached file
            if not os.path.isdir(SOUND_CACHE):
                os.makedirs(SOUND_CACHE)
        
            _, file_ext = os.path.splitext(filename)
            if file_ext != '.raw':
                # Use sox to convert sound file to raw cached sound
                elongated_file_name = re.sub('/', '_', filename)
                raw_name = os.path.join(SOUND_CACHE, elongated_file_name)

                # If cached file doesn't exist, create it using sox
                if not os.path.isfile(raw_name):
                    soxcmd = 'sox -q {} -L -r44100 -b16 -c1 -traw {}'.format(filename, raw_name)
                    shell_cmd(soxcmd)
                    # test error
                filename = raw_name

            self.file_opener = partial(open, filename, 'rb')

            byte_length = os.path.getsize(filename)

        self._length = round(byte_length / (self._SAMPLE_RATE * self._BYTES_PER_SAMPLE), 6)

    def _chunker(self, loops, duration):
        with self.file_opener() as f:
            duration_bytes = self._time_to_bytes(duration)
            leftover = b''
            for loop in reversed(range(loops)):
                f.seek(0)
                bytes_written = 0
                while duration_bytes == None or bytes_written < duration_bytes:
                    if leftover:
                        chunk = leftover + f.read(CHUNK_BYTES - len(leftover))
                        leftover = b''
                    else:
                        chunk = f.read(CHUNK_BYTES)
                    if chunk:
                        if len(chunk) < CHUNK_BYTES and loop > 0:
                            # Save partial chunk as leftovers
                            leftover = chunk
                            break
                        else:
                            # Pad silence, if we're on the last loop and it's not a full chunk
                            if loop == 0:
                                chunk = chunk + bytes(CHUNK_BYTES)[len(chunk):]
                            bytes_written += CHUNK_BYTES
                            yield chunk
                    else:
                        # EOF
                        break

class Note(BaseSound):
    '''A sine wave sound object. '''

    def __init__(self, pitch):
        '''Create a sound object that is a sine wave of the given `pitch`.

        `pitch` can either be a number that is the frequency Hz (for example
        440 or 256.7), or it can be a string which represents the musical note.
        As a string, it should be 1 to 3 characters, of the form:

            NSO

        where

         1. N is the note (required), from A to G
         1. S is an optional semitone, either '#' (sharp) or 'b' (flat)
         1. O is the optional octave, default octave is 4.

        A 440Hz 'A' could be represented by any of the following:

         - 440
         - 'A'
         - 'A4'

        For example, a ascending/descending chromatic scale on C could be
        represented by:

            ascending = \\
                ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#',
                'G', 'G#', 'A5', 'A#5', 'B5', 'C5']
            descending = \\
                ['C5', 'B5', 'Bb5', 'A5', 'Ab5', 'G',
                'Gb', 'F', 'E', 'Eb', 'D', 'Db', 'C']

        Also note, semitones are extact halftones, so for example 'C#' is
        identical to 'Db'.
        '''
        super().__init__()

        A4_frequency = 440
        A6_frequency = A4_frequency * 2 * 2

        try:
            self.frequency = float(pitch)
        except ValueError:
            match = re.search('^([A-G])([b#]?)([0-9]?)$', pitch)
            if not match:
                raise ValueError("pitch parameter must be a frequency or note (e.g. 'A', 'B#', or 'Cb4')")
            note, semitone, octave = match.groups()

            if not semitone:
                semitone_adjust = 0
            elif semitone == 'b':
                semitone_adjust = -1
            else:
                semitone_adjust = 1

            if not octave:
                octave = 4
            octave = int(octave)

            half_step_map = {'C' : 0, 'D' : 2, 'E' : 4, 'F' : 5, 'G' : 7, 'A' : 9, 'B' : 11}
            half_steps = octave * 12 + half_step_map[note]

            half_steps += semitone_adjust

            # Adjust half steps relative to A4 440Hz
            half_steps -= 4 * 12 + 9

            self.frequency = 2 ** (half_steps / 12.0) * A4_frequency

        # Simple bass boost: scale up the volume of lower frequency notes.  For
        # each octave below a 'A6', double the volume
        if self.frequency < A6_frequency:
            self.internal_gain = A6_frequency / self.frequency

    def play(self, duration=1):
        '''Starts playing the Note.

        This function starts playing the sound, and returns immediately - the
        sound plays in the background.  To wait for the sound, use `wait()`.
        Because sound functions can be chained, to create, play and wait for a
        sound to complete can be done in one compound command.  For example:

            Note('A').play().wait()

        Returns itself, so this function can be chained.
        '''
        super().play(duration=duration)
        return self

    def _chunker(self, loops, duration):
        if duration == None:
            chunks = 999999999
        else:
            chunks = int((self._time_to_bytes(duration) * loops) / CHUNK_BYTES)
        for chunk in range(chunks):
            yield soundutil.note(chunk, float(self.frequency))

class Speech(Sound):
    '''A text-to-speech sound object.'''

    def __init__(self, text, espeak_options=''):
        '''Create a sound object that is text-to-speech of the given `text`.

        The sound is created using the espeak engine (an external program).
        Command line options to espeak can be added using `espeak_options`.
        '''
        wav_fd, wav_name = tempfile.mkstemp(suffix='.wav')
        os.system('espeak {} -w {} "{}"'.format(espeak_options, wav_name, text))
        os.close(wav_fd)
        self.wav_name = wav_name
        super().__init__(wav_name)

    def __del__(self):
        os.remove(self.wav_name)
        
__all__ = ['Sound', 'Note', 'Speech', 'master_volume' 'sound_dir']

Classes

class Note

A sine wave sound object.

class Note(BaseSound):
    '''A sine wave sound object. '''

    def __init__(self, pitch):
        '''Create a sound object that is a sine wave of the given `pitch`.

        `pitch` can either be a number that is the frequency Hz (for example
        440 or 256.7), or it can be a string which represents the musical note.
        As a string, it should be 1 to 3 characters, of the form:

            NSO

        where

         1. N is the note (required), from A to G
         1. S is an optional semitone, either '#' (sharp) or 'b' (flat)
         1. O is the optional octave, default octave is 4.

        A 440Hz 'A' could be represented by any of the following:

         - 440
         - 'A'
         - 'A4'

        For example, a ascending/descending chromatic scale on C could be
        represented by:

            ascending = \\
                ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#',
                'G', 'G#', 'A5', 'A#5', 'B5', 'C5']
            descending = \\
                ['C5', 'B5', 'Bb5', 'A5', 'Ab5', 'G',
                'Gb', 'F', 'E', 'Eb', 'D', 'Db', 'C']

        Also note, semitones are extact halftones, so for example 'C#' is
        identical to 'Db'.
        '''
        super().__init__()

        A4_frequency = 440
        A6_frequency = A4_frequency * 2 * 2

        try:
            self.frequency = float(pitch)
        except ValueError:
            match = re.search('^([A-G])([b#]?)([0-9]?)$', pitch)
            if not match:
                raise ValueError("pitch parameter must be a frequency or note (e.g. 'A', 'B#', or 'Cb4')")
            note, semitone, octave = match.groups()

            if not semitone:
                semitone_adjust = 0
            elif semitone == 'b':
                semitone_adjust = -1
            else:
                semitone_adjust = 1

            if not octave:
                octave = 4
            octave = int(octave)

            half_step_map = {'C' : 0, 'D' : 2, 'E' : 4, 'F' : 5, 'G' : 7, 'A' : 9, 'B' : 11}
            half_steps = octave * 12 + half_step_map[note]

            half_steps += semitone_adjust

            # Adjust half steps relative to A4 440Hz
            half_steps -= 4 * 12 + 9

            self.frequency = 2 ** (half_steps / 12.0) * A4_frequency

        # Simple bass boost: scale up the volume of lower frequency notes.  For
        # each octave below a 'A6', double the volume
        if self.frequency < A6_frequency:
            self.internal_gain = A6_frequency / self.frequency

    def play(self, duration=1):
        '''Starts playing the Note.

        This function starts playing the sound, and returns immediately - the
        sound plays in the background.  To wait for the sound, use `wait()`.
        Because sound functions can be chained, to create, play and wait for a
        sound to complete can be done in one compound command.  For example:

            Note('A').play().wait()

        Returns itself, so this function can be chained.
        '''
        super().play(duration=duration)
        return self

    def _chunker(self, loops, duration):
        if duration == None:
            chunks = 999999999
        else:
            chunks = int((self._time_to_bytes(duration) * loops) / CHUNK_BYTES)
        for chunk in range(chunks):
            yield soundutil.note(chunk, float(self.frequency))

Ancestors (in MRO)

  • Note
  • rstem.sound.BaseSound
  • builtins.object

Static methods

def __init__(

self, pitch)

Create a sound object that is a sine wave of the given pitch.

pitch can either be a number that is the frequency Hz (for example 440 or 256.7), or it can be a string which represents the musical note. As a string, it should be 1 to 3 characters, of the form:

NSO

where

  1. N is the note (required), from A to G
  2. S is an optional semitone, either '#' (sharp) or 'b' (flat)
  3. O is the optional octave, default octave is 4.

A 440Hz 'A' could be represented by any of the following:

  • 440
  • 'A'
  • 'A4'

For example, a ascending/descending chromatic scale on C could be represented by:

ascending = \
    ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#',
    'G', 'G#', 'A5', 'A#5', 'B5', 'C5']
descending = \
    ['C5', 'B5', 'Bb5', 'A5', 'Ab5', 'G',
    'Gb', 'F', 'E', 'Eb', 'D', 'Db', 'C']

Also note, semitones are extact halftones, so for example 'C#' is identical to 'Db'.

def __init__(self, pitch):
    '''Create a sound object that is a sine wave of the given `pitch`.
    `pitch` can either be a number that is the frequency Hz (for example
    440 or 256.7), or it can be a string which represents the musical note.
    As a string, it should be 1 to 3 characters, of the form:
        NSO
    where
     1. N is the note (required), from A to G
     1. S is an optional semitone, either '#' (sharp) or 'b' (flat)
     1. O is the optional octave, default octave is 4.
    A 440Hz 'A' could be represented by any of the following:
     - 440
     - 'A'
     - 'A4'
    For example, a ascending/descending chromatic scale on C could be
    represented by:
        ascending = \\
            ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#',
            'G', 'G#', 'A5', 'A#5', 'B5', 'C5']
        descending = \\
            ['C5', 'B5', 'Bb5', 'A5', 'Ab5', 'G',
            'Gb', 'F', 'E', 'Eb', 'D', 'Db', 'C']
    Also note, semitones are extact halftones, so for example 'C#' is
    identical to 'Db'.
    '''
    super().__init__()
    A4_frequency = 440
    A6_frequency = A4_frequency * 2 * 2
    try:
        self.frequency = float(pitch)
    except ValueError:
        match = re.search('^([A-G])([b#]?)([0-9]?)$', pitch)
        if not match:
            raise ValueError("pitch parameter must be a frequency or note (e.g. 'A', 'B#', or 'Cb4')")
        note, semitone, octave = match.groups()
        if not semitone:
            semitone_adjust = 0
        elif semitone == 'b':
            semitone_adjust = -1
        else:
            semitone_adjust = 1
        if not octave:
            octave = 4
        octave = int(octave)
        half_step_map = {'C' : 0, 'D' : 2, 'E' : 4, 'F' : 5, 'G' : 7, 'A' : 9, 'B' : 11}
        half_steps = octave * 12 + half_step_map[note]
        half_steps += semitone_adjust
        # Adjust half steps relative to A4 440Hz
        half_steps -= 4 * 12 + 9
        self.frequency = 2 ** (half_steps / 12.0) * A4_frequency
    # Simple bass boost: scale up the volume of lower frequency notes.  For
    # each octave below a 'A6', double the volume
    if self.frequency < A6_frequency:
        self.internal_gain = A6_frequency / self.frequency

def is_playing(

self)

Returns True if the sound is currently playing

def is_playing(self):
    '''Returns `True` if the sound is currently playing'''
    return not self.stopped.is_set()

def length(

self)

Returns the length of the sound in seconds

def length(self):
    '''Returns the length of the sound in seconds'''
    return self._length

def play(

self, duration=1)

Starts playing the Note.

This function starts playing the sound, and returns immediately - the sound plays in the background. To wait for the sound, use wait(). Because sound functions can be chained, to create, play and wait for a sound to complete can be done in one compound command. For example:

Note('A').play().wait()

Returns itself, so this function can be chained.

def play(self, duration=1):
    '''Starts playing the Note.
    This function starts playing the sound, and returns immediately - the
    sound plays in the background.  To wait for the sound, use `wait()`.
    Because sound functions can be chained, to create, play and wait for a
    sound to complete can be done in one compound command.  For example:
        Note('A').play().wait()
    Returns itself, so this function can be chained.
    '''
    super().play(duration=duration)
    return self

def stop(

self)

Immediately stop the sound from playing.

Does nothing if the sound is not currently playing.

Returns itself, so this function can be chained.

def stop(self):
    '''Immediately stop the sound from playing.
    Does nothing if the sound is not currently playing.
    Returns itself, so this function can be chained.
    '''
    assert self.play_thread.is_alive()
    with self.stop_play_mutex:
        self.play_msg.put((STOP, None))
        self.wait()
    return self

def wait(

self, timeout=None)

Wait until the sound has finished playing.

If timeout is given (seconds), will return early (after the timeout time) even if the sound is not finished playing.

Returns itself, so this function can be chained.

def wait(self, timeout=None):
    '''Wait until the sound has finished playing.
    
    If timeout is given (seconds), will return early (after the timeout
    time) even if the sound is not finished playing.
    Returns itself, so this function can be chained.
    '''
    assert self.play_thread.is_alive()
    self.stopped.wait(timeout)
    return self

Instance variables

var volume

The volume of the sound object

Each sound object has an volume (independent of the master_volume()), between 0 (muted) and 100 (loudest).

The volume is readable/writeable.

class Sound

A Sound object, that plays sounds read in from sound files.

In addition to the Sound object, this module provides some useful global functions:

master_volume(level):
    Sets the master volume (between 0 and 100) 
    of the audio out.

sound_dir():
    Returns the sounds dir, where all sound 
    files are stored.
class Sound(BaseSound):
    '''
    A Sound object, that plays sounds read in from sound files.

    In addition to the Sound object, this module provides some useful global
    functions:

        master_volume(level):
            Sets the master volume (between 0 and 100) 
            of the audio out.

        sound_dir():
            Returns the sounds dir, where all sound 
            files are stored.
    '''
    
    def __init__(self, filename):
        '''A playable sound backed by the sound file `filename` on disk.
        
        Throws `IOError` if the sound file cannot be read.
        '''
        super().__init__()

        self.bytes = None
        if isinstance(filename, bytes):
            data = filename 
            self.file_opener = partial(io.BytesIO, data)
            byte_length = len(data)

        else:
            # normalize path, raltive to SOUND_DIR
            try:
                filename = os.path.normpath(os.path.join(SOUND_DIR, filename))
            except:
                raise ValueError("Filename '{}' is not valid".format(filename))

            # Is it a file?  Not a definitive test here, but used as a courtesy to
            # give a better error when the filename is wrong.
            if not os.path.isfile(filename):
                raise IOError("Sound file '{}' cannot be found".format(filename))

            # Create cached file
            if not os.path.isdir(SOUND_CACHE):
                os.makedirs(SOUND_CACHE)
        
            _, file_ext = os.path.splitext(filename)
            if file_ext != '.raw':
                # Use sox to convert sound file to raw cached sound
                elongated_file_name = re.sub('/', '_', filename)
                raw_name = os.path.join(SOUND_CACHE, elongated_file_name)

                # If cached file doesn't exist, create it using sox
                if not os.path.isfile(raw_name):
                    soxcmd = 'sox -q {} -L -r44100 -b16 -c1 -traw {}'.format(filename, raw_name)
                    shell_cmd(soxcmd)
                    # test error
                filename = raw_name

            self.file_opener = partial(open, filename, 'rb')

            byte_length = os.path.getsize(filename)

        self._length = round(byte_length / (self._SAMPLE_RATE * self._BYTES_PER_SAMPLE), 6)

    def _chunker(self, loops, duration):
        with self.file_opener() as f:
            duration_bytes = self._time_to_bytes(duration)
            leftover = b''
            for loop in reversed(range(loops)):
                f.seek(0)
                bytes_written = 0
                while duration_bytes == None or bytes_written < duration_bytes:
                    if leftover:
                        chunk = leftover + f.read(CHUNK_BYTES - len(leftover))
                        leftover = b''
                    else:
                        chunk = f.read(CHUNK_BYTES)
                    if chunk:
                        if len(chunk) < CHUNK_BYTES and loop > 0:
                            # Save partial chunk as leftovers
                            leftover = chunk
                            break
                        else:
                            # Pad silence, if we're on the last loop and it's not a full chunk
                            if loop == 0:
                                chunk = chunk + bytes(CHUNK_BYTES)[len(chunk):]
                            bytes_written += CHUNK_BYTES
                            yield chunk
                    else:
                        # EOF
                        break

Ancestors (in MRO)

  • Sound
  • rstem.sound.BaseSound
  • builtins.object

Static methods

def __init__(

self, filename)

A playable sound backed by the sound file filename on disk.

Throws IOError if the sound file cannot be read.

def __init__(self, filename):
    '''A playable sound backed by the sound file `filename` on disk.
    
    Throws `IOError` if the sound file cannot be read.
    '''
    super().__init__()
    self.bytes = None
    if isinstance(filename, bytes):
        data = filename 
        self.file_opener = partial(io.BytesIO, data)
        byte_length = len(data)
    else:
        # normalize path, raltive to SOUND_DIR
        try:
            filename = os.path.normpath(os.path.join(SOUND_DIR, filename))
        except:
            raise ValueError("Filename '{}' is not valid".format(filename))
        # Is it a file?  Not a definitive test here, but used as a courtesy to
        # give a better error when the filename is wrong.
        if not os.path.isfile(filename):
            raise IOError("Sound file '{}' cannot be found".format(filename))
        # Create cached file
        if not os.path.isdir(SOUND_CACHE):
            os.makedirs(SOUND_CACHE)
    
        _, file_ext = os.path.splitext(filename)
        if file_ext != '.raw':
            # Use sox to convert sound file to raw cached sound
            elongated_file_name = re.sub('/', '_', filename)
            raw_name = os.path.join(SOUND_CACHE, elongated_file_name)
            # If cached file doesn't exist, create it using sox
            if not os.path.isfile(raw_name):
                soxcmd = 'sox -q {} -L -r44100 -b16 -c1 -traw {}'.format(filename, raw_name)
                shell_cmd(soxcmd)
                # test error
            filename = raw_name
        self.file_opener = partial(open, filename, 'rb')
        byte_length = os.path.getsize(filename)
    self._length = round(byte_length / (self._SAMPLE_RATE * self._BYTES_PER_SAMPLE), 6)

def is_playing(

self)

Returns True if the sound is currently playing

def is_playing(self):
    '''Returns `True` if the sound is currently playing'''
    return not self.stopped.is_set()

def length(

self)

Returns the length of the sound in seconds

def length(self):
    '''Returns the length of the sound in seconds'''
    return self._length

def play(

self, loops=1, duration=None)

Starts playing the sound.

This function starts playing the sound, and returns immediately - the sound plays in the background. To wait for the sound, use wait(). Because sound functions can be chained, to create, play and wait for a sound to complete can be done in one compound command. For example:

Sound('mysound.wav').play().wait()

loops is the number of times the sound should be played. duration is the length of the sound to play (or None to play forever, or until the sound ends).

Returns itself, so this function can be chained.

def play(self, loops=1, duration=None):
    '''Starts playing the sound.
    This function starts playing the sound, and returns immediately - the
    sound plays in the background.  To wait for the sound, use `wait()`.
    Because sound functions can be chained, to create, play and wait for a
    sound to complete can be done in one compound command.  For example:
        Sound('mysound.wav').play().wait()
    `loops` is the number of times the sound should be played.  `duration`
    is the length of the sound to play (or `None` to play forever, or until
    the sound ends).
    Returns itself, so this function can be chained.
    '''
    assert self.play_thread.is_alive()
    if duration and duration < 0:
        raise ValueError("duration must be a positive number")
    with self.stop_play_mutex:
        self.stop()
        self.end_time = time.time() 
        previous_play_count = self.play_count
        self.play_msg.put((PLAY, (loops, duration)))
        # Wait until we know the play has started (i.e., the state ===
        # PLAY).  Ugly (polled), but simple.
        while previous_play_count == self.play_count:
            time.sleep(0.001)
    return self

def stop(

self)

Immediately stop the sound from playing.

Does nothing if the sound is not currently playing.

Returns itself, so this function can be chained.

def stop(self):
    '''Immediately stop the sound from playing.
    Does nothing if the sound is not currently playing.
    Returns itself, so this function can be chained.
    '''
    assert self.play_thread.is_alive()
    with self.stop_play_mutex:
        self.play_msg.put((STOP, None))
        self.wait()
    return self

def wait(

self, timeout=None)

Wait until the sound has finished playing.

If timeout is given (seconds), will return early (after the timeout time) even if the sound is not finished playing.

Returns itself, so this function can be chained.

def wait(self, timeout=None):
    '''Wait until the sound has finished playing.
    
    If timeout is given (seconds), will return early (after the timeout
    time) even if the sound is not finished playing.
    Returns itself, so this function can be chained.
    '''
    assert self.play_thread.is_alive()
    self.stopped.wait(timeout)
    return self

Instance variables

var bytes

var volume

The volume of the sound object

Each sound object has an volume (independent of the master_volume()), between 0 (muted) and 100 (loudest).

The volume is readable/writeable.

class Speech

A text-to-speech sound object.

class Speech(Sound):
    '''A text-to-speech sound object.'''

    def __init__(self, text, espeak_options=''):
        '''Create a sound object that is text-to-speech of the given `text`.

        The sound is created using the espeak engine (an external program).
        Command line options to espeak can be added using `espeak_options`.
        '''
        wav_fd, wav_name = tempfile.mkstemp(suffix='.wav')
        os.system('espeak {} -w {} "{}"'.format(espeak_options, wav_name, text))
        os.close(wav_fd)
        self.wav_name = wav_name
        super().__init__(wav_name)

    def __del__(self):
        os.remove(self.wav_name)

Ancestors (in MRO)

Static methods

def __init__(

self, text, espeak_options='')

Create a sound object that is text-to-speech of the given text.

The sound is created using the espeak engine (an external program). Command line options to espeak can be added using espeak_options.

def __init__(self, text, espeak_options=''):
    '''Create a sound object that is text-to-speech of the given `text`.
    The sound is created using the espeak engine (an external program).
    Command line options to espeak can be added using `espeak_options`.
    '''
    wav_fd, wav_name = tempfile.mkstemp(suffix='.wav')
    os.system('espeak {} -w {} "{}"'.format(espeak_options, wav_name, text))
    os.close(wav_fd)
    self.wav_name = wav_name
    super().__init__(wav_name)

def is_playing(

self)

Returns True if the sound is currently playing

def is_playing(self):
    '''Returns `True` if the sound is currently playing'''
    return not self.stopped.is_set()

def length(

self)

Returns the length of the sound in seconds

def length(self):
    '''Returns the length of the sound in seconds'''
    return self._length

def play(

self, loops=1, duration=None)

Starts playing the sound.

This function starts playing the sound, and returns immediately - the sound plays in the background. To wait for the sound, use wait(). Because sound functions can be chained, to create, play and wait for a sound to complete can be done in one compound command. For example:

Sound('mysound.wav').play().wait()

loops is the number of times the sound should be played. duration is the length of the sound to play (or None to play forever, or until the sound ends).

Returns itself, so this function can be chained.

def play(self, loops=1, duration=None):
    '''Starts playing the sound.
    This function starts playing the sound, and returns immediately - the
    sound plays in the background.  To wait for the sound, use `wait()`.
    Because sound functions can be chained, to create, play and wait for a
    sound to complete can be done in one compound command.  For example:
        Sound('mysound.wav').play().wait()
    `loops` is the number of times the sound should be played.  `duration`
    is the length of the sound to play (or `None` to play forever, or until
    the sound ends).
    Returns itself, so this function can be chained.
    '''
    assert self.play_thread.is_alive()
    if duration and duration < 0:
        raise ValueError("duration must be a positive number")
    with self.stop_play_mutex:
        self.stop()
        self.end_time = time.time() 
        previous_play_count = self.play_count
        self.play_msg.put((PLAY, (loops, duration)))
        # Wait until we know the play has started (i.e., the state ===
        # PLAY).  Ugly (polled), but simple.
        while previous_play_count == self.play_count:
            time.sleep(0.001)
    return self

def stop(

self)

Immediately stop the sound from playing.

Does nothing if the sound is not currently playing.

Returns itself, so this function can be chained.

def stop(self):
    '''Immediately stop the sound from playing.
    Does nothing if the sound is not currently playing.
    Returns itself, so this function can be chained.
    '''
    assert self.play_thread.is_alive()
    with self.stop_play_mutex:
        self.play_msg.put((STOP, None))
        self.wait()
    return self

def wait(

self, timeout=None)

Wait until the sound has finished playing.

If timeout is given (seconds), will return early (after the timeout time) even if the sound is not finished playing.

Returns itself, so this function can be chained.

def wait(self, timeout=None):
    '''Wait until the sound has finished playing.
    
    If timeout is given (seconds), will return early (after the timeout
    time) even if the sound is not finished playing.
    Returns itself, so this function can be chained.
    '''
    assert self.play_thread.is_alive()
    self.stopped.wait(timeout)
    return self

Instance variables

var volume

The volume of the sound object

Each sound object has an volume (independent of the master_volume()), between 0 (muted) and 100 (loudest).

The volume is readable/writeable.

var wav_name