rstem.sound module
This module provides interfaces to the Speaker RaspberrySTEM Cell.
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 Speaker RaspberrySTEM Cell.
Additionally, it can be used for any audio out over the analog audio jack.
'''
import os
import time
import re
import tempfile
from threading import Lock
from subprocess import Popen, PIPE
'''
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
'''
class Sox(Popen):
def __init__(self, *args, play=False):
fullcmd = ['play' if play else 'sox']
# split args by whitespace and flatten.
for arg in args:
if isinstance(arg, str):
fullcmd += arg.split()
else:
fullcmd += arg
super().__init__(fullcmd, stdout=PIPE, stderr=PIPE)
class SoxPlay(Sox):
def __init__(self, *args, **kwargs):
super().__init__(*args, play=True, **kwargs)
class BaseSound(object):
def __init__(self):
self._length = 0
self.mutex = Lock()
self.sox = None
def length(self):
'''Returns the length in seconds of the sound'''
return self._length
def is_playing(self):
with self.mutex:
if not self.sox:
return False
playing = self.sox.poll() == None
if not playing:
self.sox = None
return playing
def wait(self):
'''Wait until the sound has finished playing.'''
with self.mutex:
_sox = self.sox
self.sox = None
if _sox:
_sox.wait()
def _stop(self):
if self.sox:
self.sox.kill()
self.sox = None
def stop(self):
with self.mutex:
self._stop()
class Sound(BaseSound):
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.filename = 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))
out, err = Sox([filename], '-n stat').communicate()
matches = (re.search('^Length.*?([^ ]+)$', line) for line in err.decode().splitlines())
try:
firstmatch = [match for match in matches if match][0]
self._length = float(firstmatch.group(1))
except IndexError:
raise IOError("Sox could not get sound file's length")
def play(self, loops=1, duration=None):
with self.mutex:
self._stop()
args = ['-q', [self.filename]]
if duration != None:
if duration < 0:
duration = self._length + duration
if duration >= 0 and duration <= self._length:
args += ['trim 0 {}'.format(duration)]
args += ['repeat {}'.format(loops-1)]
self.sox = SoxPlay(*args)
return self
class Note(BaseSound):
def __init__(self, pitch):
'''
'''
super().__init__()
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) * 440.0
def play(self, duration=1):
with self.mutex:
self._stop()
duration = duration if duration else 0
args = ['-q -n synth {} sine {} gain 20'.format(duration, self.frequency)]
self.sox = SoxPlay(*args)
return self
class Speech(Sound):
def __init__(self, text, 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']
Classes
class Note
class Note(BaseSound):
def __init__(self, pitch):
'''
'''
super().__init__()
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) * 440.0
def play(self, duration=1):
with self.mutex:
self._stop()
duration = duration if duration else 0
args = ['-q -n synth {} sine {} gain 20'.format(duration, self.frequency)]
self.sox = SoxPlay(*args)
return self
Ancestors (in MRO)
- Note
- rstem.sound.BaseSound
- builtins.object
Static methods
def __init__(
self, pitch)
def __init__(self, pitch):
'''
'''
super().__init__()
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) * 440.0
def is_playing(
self)
def is_playing(self):
with self.mutex:
if not self.sox:
return False
playing = self.sox.poll() == None
if not playing:
self.sox = None
return playing
def length(
self)
Returns the length in seconds of the sound
def length(self):
'''Returns the length in seconds of the sound'''
return self._length
def play(
self, duration=1)
def play(self, duration=1):
with self.mutex:
self._stop()
duration = duration if duration else 0
args = ['-q -n synth {} sine {} gain 20'.format(duration, self.frequency)]
self.sox = SoxPlay(*args)
return self
def stop(
self)
def stop(self):
with self.mutex:
self._stop()
def wait(
self)
Wait until the sound has finished playing.
def wait(self):
'''Wait until the sound has finished playing.'''
with self.mutex:
_sox = self.sox
self.sox = None
if _sox:
_sox.wait()
class Sound
class Sound(BaseSound):
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.filename = 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))
out, err = Sox([filename], '-n stat').communicate()
matches = (re.search('^Length.*?([^ ]+)$', line) for line in err.decode().splitlines())
try:
firstmatch = [match for match in matches if match][0]
self._length = float(firstmatch.group(1))
except IndexError:
raise IOError("Sox could not get sound file's length")
def play(self, loops=1, duration=None):
with self.mutex:
self._stop()
args = ['-q', [self.filename]]
if duration != None:
if duration < 0:
duration = self._length + duration
if duration >= 0 and duration <= self._length:
args += ['trim 0 {}'.format(duration)]
args += ['repeat {}'.format(loops-1)]
self.sox = SoxPlay(*args)
return self
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.filename = 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))
out, err = Sox([filename], '-n stat').communicate()
matches = (re.search('^Length.*?([^ ]+)$', line) for line in err.decode().splitlines())
try:
firstmatch = [match for match in matches if match][0]
self._length = float(firstmatch.group(1))
except IndexError:
raise IOError("Sox could not get sound file's length")
def is_playing(
self)
def is_playing(self):
with self.mutex:
if not self.sox:
return False
playing = self.sox.poll() == None
if not playing:
self.sox = None
return playing
def length(
self)
Returns the length in seconds of the sound
def length(self):
'''Returns the length in seconds of the sound'''
return self._length
def play(
self, loops=1, duration=None)
def play(self, loops=1, duration=None):
with self.mutex:
self._stop()
args = ['-q', [self.filename]]
if duration != None:
if duration < 0:
duration = self._length + duration
if duration >= 0 and duration <= self._length:
args += ['trim 0 {}'.format(duration)]
args += ['repeat {}'.format(loops-1)]
self.sox = SoxPlay(*args)
return self
def stop(
self)
def stop(self):
with self.mutex:
self._stop()
def wait(
self)
Wait until the sound has finished playing.
def wait(self):
'''Wait until the sound has finished playing.'''
with self.mutex:
_sox = self.sox
self.sox = None
if _sox:
_sox.wait()
Instance variables
var filename
class Speech
class Speech(Sound):
def __init__(self, text, 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='')
def __init__(self, text, 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)
def is_playing(self):
with self.mutex:
if not self.sox:
return False
playing = self.sox.poll() == None
if not playing:
self.sox = None
return playing
def length(
self)
Returns the length in seconds of the sound
def length(self):
'''Returns the length in seconds of the sound'''
return self._length
def play(
self, loops=1, duration=None)
def play(self, loops=1, duration=None):
with self.mutex:
self._stop()
args = ['-q', [self.filename]]
if duration != None:
if duration < 0:
duration = self._length + duration
if duration >= 0 and duration <= self._length:
args += ['trim 0 {}'.format(duration)]
args += ['repeat {}'.format(loops-1)]
self.sox = SoxPlay(*args)
return self
def stop(
self)
def stop(self):
with self.mutex:
self._stop()
def wait(
self)
Wait until the sound has finished playing.
def wait(self):
'''Wait until the sound has finished playing.'''
with self.mutex:
_sox = self.sox
self.sox = None
if _sox:
_sox.wait()
Instance variables
var wav_name