Skip to content

Commit

Permalink
Merge 080f2cc into 39c92f2
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew D. Cutone committed Apr 9, 2021
2 parents 39c92f2 + 080f2cc commit 1e1105a
Showing 1 changed file with 57 additions and 22 deletions.
79 changes: 57 additions & 22 deletions psychopy/sound/microphone.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import sys
import psychopy.logging as logging
from psychopy.constants import NOT_STARTED, STARTED
from psychopy.preferences import prefs
from .audioclip import *
from .audiodevice import *
from .exceptions import *
Expand Down Expand Up @@ -50,16 +51,10 @@ class RecordingBuffer(object):
maxRecordingSize : int
Maximum size of the recording in kilobytes. This specifies how much
memory is available to store samples.
loopback : bool
Allow for continuous recording. Usually if the recording buffer is full
no more samples can be saved to the buffer. If `loopback=True`, samples
wil be written to the beginning of the track once there is no more
space. Default is `False`.
"""
def __init__(self, sampleRateHz=SAMPLE_RATE_48kHz, channels=2,
maxRecordingSize=24000, loopback=True,
policyWhenFull='ignore'):
maxRecordingSize=24000, policyWhenFull='ignore'):
self._channels = channels
self._sampleRateHz = sampleRateHz
self._maxRecordingSize = maxRecordingSize
Expand All @@ -69,7 +64,6 @@ def __init__(self, sampleRateHz=SAMPLE_RATE_48kHz, channels=2,
self._spaceRemaining = None # set in `_allocRecBuffer`
self._totalSamples = None # set in `_allocRecBuffer`

self._loopback = loopback
self._policyWhenFull = policyWhenFull
self._warnedRecBufferFull = False
self._loops = 0
Expand Down Expand Up @@ -318,8 +312,10 @@ class Microphone(object):
out of memory. By default, the recording buffer is set to 24000 KB (or
24 MB). At a sample rate of 48kHz, this will result in 62.5 seconds of
continuous audio being recorded before the buffer is full.
volume : float
Input volume (or gain). Specified as a value between 0 and 1.
audioLatencyMode : int or None
Audio latency mode to use, values range between 0-4. If `None`, the
setting from preferences will be used. By default, mode `3` is adequate
for most applications.
warmUp : bool
Warm-up the stream after opening it. This helps prevent additional
latency the first time `start` is called on some systems.
Expand Down Expand Up @@ -366,7 +362,7 @@ def __init__(self,
channels=2,
streamBufferSecs=2.0,
maxRecordingSize=24000,
volume=0.5,
audioLatencyMode=None,
warmUp=True):

if not _hasPTB: # fail if PTB is not installed
Expand Down Expand Up @@ -395,6 +391,9 @@ def __init__(self,

self._device = devices[0] # use first

logging.info('Using audio device #{} ({}) for audio capture'.format(
self._device.deviceIndex, self._device.deviceName))

# error if specified device is not suitable for capture
if not self._device.isCapture:
raise AudioInvalidCaptureDeviceError(
Expand All @@ -406,10 +405,16 @@ def __init__(self,
self._device.defaultSampleRate if sampleRateHz is None else int(
sampleRateHz)

logging.debug('Set stream sample rate to {} Hz'.format(
self._sampleRateHz))

# set the number of recording channels
self._channels = \
self._device.inputChannels if channels is None else int(channels)

logging.debug('Set recording channels to {} ({})'.format(
self._channels, 'stereo' if self._channels > 1 else 'mono'))

if self._channels > self._device.inputChannels:
raise AudioInvalidDeviceError(
'Invalid number of channels for audio input specified.')
Expand All @@ -421,24 +426,44 @@ def __init__(self,
# PTB specific stuff
self._mode = 2 # open a stream in capture mode

# set the audio latency mode
if audioLatencyMode is None:
self._audioLatencyMode = int(prefs.hardware["audioLatencyMode"])
else:
self._audioLatencyMode = audioLatencyMode

logging.debug('Set audio latency mode to {}'.format(
self._audioLatencyMode))

assert 0 <= self._audioLatencyMode <= 4 # sanity check for pref

# Handle for the recording stream, should only be opened once per
# session
logging.debug('Opening audio stream for device #{}'.format(
self._device.deviceIndex))

self._stream = audio.Stream(
device_id=self._device.deviceIndex,
latency_class=self._audioLatencyMode,
mode=self._mode,
freq=self._sampleRateHz,
channels=self._channels)

logging.debug('Stream opened.')

# set latency bias
self._stream.latency_bias = 0.0

# set the volume
assert 0. <= volume <= 1.
self._stream.volume = float(volume)
logging.debug('Set stream latency bias to {} ms'.format(
self._stream.latency_bias))

# pre-allocate recording buffer, called once
self._stream.get_audio_data(self._streamBufferSecs)

logging.debug(
'Allocated stream buffer to hold {} seconds of data'.format(
self._streamBufferSecs))

# status flag
self._statusFlag = NOT_STARTED

Expand All @@ -450,8 +475,11 @@ def __init__(self,

# do the warm-up
if warmUp:
logging.debug('Waking up the audio driver and hardware.')
self.warmUp()

logging.debug('Audio capture device #{} ready')

@staticmethod
def getDevices():
"""Get a `list` of audio capture device (i.e. microphones) descriptors.
Expand Down Expand Up @@ -537,15 +565,12 @@ def latencyBias(self, value):
self._stream.latency_bias = float(value)

@property
def volume(self):
"""The recording volume (`float`). Specified as a value between 0 and 1
and applied to all channels.
"""
return self._stream.volume
def audioLatencyMode(self):
"""Audio latency mode in use (`int`). Cannot be set after
initialization.
@volume.setter
def volume(self, value):
self._stream.volume = float(value)
"""
return self._audioLatencyMode

@property
def streamBufferSecs(self):
Expand Down Expand Up @@ -665,6 +690,10 @@ def start(self, when=None, waitForStart=0, stopTime=None):
# recording has begun or is scheduled to do so
self._statusFlag = STARTED

logging.debug(
'Scheduled start of audio capture for device #{} at t={}.'.format(
self._device.deviceIndex, startTime))

return startTime

def stop(self, blockUntilStopped=True, stopTime=None):
Expand Down Expand Up @@ -702,6 +731,11 @@ def stop(self, blockUntilStopped=True, stopTime=None):
stopTime=stopTime)
self._statusFlag = NOT_STARTED

logging.debug(
('Device #{} stopped capturing audio samples at estimated time '
't={}. Total overruns: {} Total recording time: {}').format(
self._device.deviceIndex, estStopTime, xruns, endPositionSecs))

return startTime, endPositionSecs, xruns, estStopTime

def close(self):
Expand All @@ -713,6 +747,7 @@ def close(self):
"""
self._stream.close()
logging.debug('Stream closed')

def poll(self):
"""Poll audio samples.
Expand Down

0 comments on commit 1e1105a

Please sign in to comment.