koenvervloesem/hermes-audio-server

View on GitHub
src/hermes_audio_server/player.py

Summary

Maintainability
B
4 hrs
Test Coverage
"""Module with the Hermes audio player class."""
import io
import json
import wave

from humanfriendly import format_size

from hermes_audio_server.exceptions import NoDefaultAudioDeviceError
from hermes_audio_server.mqtt import MQTTClient

PLAY_BYTES = 'hermes/audioServer/{}/playBytes/+'
PLAY_FINISHED = 'hermes/audioServer/{}/playFinished'
CHUNK = 256


class AudioPlayer(MQTTClient):
    """This class creates an MQTT client that acts as an audio player for the
    Hermes protocol.
    """

    def initialize(self):
        """Initialize a Hermes audio player."""
        self.logger.debug('Probing for available output devices...')
        for index in range(self.audio.get_device_count()):
            device = self.audio.get_device_info_by_index(index)
            name = device['name']
            channels = device['maxOutputChannels']
            if channels:
                self.logger.debug('[%d] %s', index, name)
        try:
            self.audio_out = self.audio.get_default_output_device_info()['name']
        except OSError:
            raise NoDefaultAudioDeviceError('output')
        self.logger.info('Connected to audio output %s.', self.audio_out)

    def on_connect(self, client, userdata, flags, result_code):
        """Callback that is called when the audio player connects to the MQTT
        broker."""
        super().on_connect(client, userdata, flags, result_code)
        # Listen to the MQTT topic defined in the Hermes protocol to play a WAV
        # file.
        # See https://docs.snips.ai/reference/hermes#playing-a-wav-sound
        play_bytes = PLAY_BYTES.format(self.config.site)
        self.mqtt.subscribe(play_bytes)
        self.mqtt.message_callback_add(play_bytes, self.on_play_bytes)
        self.logger.info('Subscribed to %s topic.', play_bytes)

    def on_play_bytes(self, client, userdata, message):
        """Callback that is called when the audio player receives a PLAY_BYTES
        message on MQTT.
        """
        request_id = message.topic.split('/')[4]
        length = format_size(len(message.payload), binary=True)
        self.logger.info('Received an audio message of length %s'
                         ' with request id %s on site %s.',
                         length,
                         request_id,
                         self.config.site)

        with io.BytesIO(message.payload) as wav_buffer:
            try:
                with wave.open(wav_buffer, 'rb') as wav:
                    sample_width = wav.getsampwidth()
                    sample_format = self.audio.get_format_from_width(sample_width)
                    n_channels = wav.getnchannels()
                    frame_rate = wav.getframerate()

                    self.logger.debug('Sample width: %s', sample_width)
                    self.logger.debug('Channels: %s', n_channels)
                    self.logger.debug('Frame rate: %s', frame_rate)

                    self.logger.debug('Opening audio output stream...')
                    stream = self.audio.open(format=sample_format,
                                             channels=n_channels,
                                             rate=frame_rate,
                                             output=True)

                    self.logger.debug('Playing WAV buffer on audio output...')
                    data = wav.readframes(CHUNK)

                    while data:
                        stream.write(data)
                        data = wav.readframes(CHUNK)

                    stream.stop_stream()
                    self.logger.debug('Closing audio output stream...')
                    stream.close()

                    self.logger.info('Finished playing audio message with id %s'
                                     ' on device %s on site %s.',
                                     request_id,
                                     self.audio_out,
                                     self.config.site)

                    # Publish a message that the audio service has finished
                    # playing the sound.
                    # See https://docs.snips.ai/reference/hermes#being-notified-when-sound-has-finished-playing
                    # This implementation doesn't publish a session ID.
                    play_finished_topic = PLAY_FINISHED.format(self.config.site)
                    play_finished_message = json.dumps({'id': request_id,
                                                        'siteId': self.config.site})
                    self.mqtt.publish(play_finished_topic,
                                      play_finished_message)
                    self.logger.debug('Published message on MQTT topic:')
                    self.logger.debug('Topic: %s', play_finished_topic)
                    self.logger.debug('Message: %s', play_finished_message)
            except wave.Error as error:
                self.logger.warning('%s', str(error))
            except EOFError:
                self.logger.warning('End of WAV buffer')