Source code for reiz._audio.primitives

# -*- coding: utf-8 -*-
"""
Building blocks for auditory stimuli
....................................
"""

from .tts import tts_Mixin
from sys import platform
from threading import Timer
import time
from pyglet.media.codecs.base import StaticSource
from pyglet.media import Player
from pyglet.media.synthesis import ADSREnvelope, Sine
import pyglet


class Sound:
    """interface for a generic sound object"""

    source = None
    volume = 1

    def play(self):
        """start playing the sound and return immediatly

        returns
        -------
        remainder: float
            seconds until the sound would be finished playing
        """
        t0 = time.time()
        player = self.source.play()
        player.volume = self.volume
        t = Timer(player.source.duration, player.delete)
        t.start()
        self._keep_audio_buffer_full()
        return player.source.duration + t0 - time.time()

    def _keep_audio_buffer_full(self, now=None):
        """update the audio buffer continually
        
        important for sounds longer than ~2s
        https://github.com/pyglet/pyglet/issues/158
        """
        now = now or time.time()
        passed = time.time()-now
        t = Timer(.5, self._keep_audio_buffer_full, args=(now,))
        if passed < self.duration:
            pyglet.clock.tick()
          #  print("Buffer up")
            t.start()
        #else:
        #    print("Stopped keeping the audiobuffer full at", now)

    def play_blocking(self) -> float:
        """"play the sound and block until playing is finished

        returns
        -------
        remainder: float
            seconds until the sound would finish playing. Naturally, it always
            returns 0. The output is kept here to allow easy substitution with
            : func: `Sound.play`
        """    
        rest = self.play()
        t0 = time.time()                
        while (time.time() - t0) < rest:
            pass
            
        return 0

    @property
    def duration(self) -> float:
        "returns duration of the sound in seconds"
        return self.source.duration

    def __repr__(self):
        return str(self.source)


class AnySource(Sound):
    """
    args
    ----
    source: StaticSource
        the source of the sound. Usually you instantiate one of the
        inheriting objects, e.g. :class:`~.AudioFile` or :class`~.Hertz`
        which allow more parameters.

    """

    def __init__(self, source: StaticSource = None):
        self.source = source


[docs]class Hertz(Sound): """instantiate a sine wave with ramp-up and ramp-down period of volume args ---- duration_in_ms: float duration of the sound in ms frequency: int frequency in Hz volume: float relative volume """ def __init__( self, duration_in_ms: float = 500, frequency: int = 440, volume: float = 1 ): adsr = ADSREnvelope( attack=duration_in_ms / 8000, decay=duration_in_ms / 2000, release=duration_in_ms / 8000, sustain_amplitude=volume, ) sine = Sine( duration=duration_in_ms / 1000, frequency=frequency, sample_size=16, sample_rate=44100, envelope=adsr, ) self._duration_in_ms = duration_in_ms self._frequency = frequency self.source = StaticSource(sine) self.volume = volume def __repr__(self): return f"Hertz(duration_in_ms={self._duration_in_ms}, frequency= {self._frequency})"
[docs]class AudioFile(Sound): """instantiate a sound object from a wav-file args ---- filepath: str path to the wav-file """ def __init__(self, filepath: str): self.source = pyglet.media.load(filepath, streaming=False)
[docs]class Message(tts_Mixin, Sound): """Text-to-Speech Audio Stimulus If reiz was installed with `[tts]` as extra, this class will use pyttsx3 to generate auditory stimuli on demand. If pyttsx3 is not found, there will be only silence when creating an `audio.Message`. """ pass
[docs]class Noise(Sound): """instantiate a noise wave args ---- duration_in_ms: float duration of the sound in ms kind: str either "white" or "brown" volume: float relative volume """ def __init__( self, duration_in_ms: float = 500, kind: str = "white", volume: float = 1 ): if kind.lower() == "white": from reiz._audio.noise import WhiteNoise as _Noise elif kind.lower() == "brown": from reiz._audio.noise import BrownNoise as _Noise else: raise NotImplementedError(f"{kind} is not implemented as noise form") self.source = StaticSource(_Noise(duration=duration_in_ms / 1000)) self.volume = volume