2019-12-22 14:00:50 +00:00
|
|
|
#!/usr/bin/env python3.6
|
2018-04-01 08:57:29 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
2018-08-01 18:36:58 +00:00
|
|
|
Description: Read data from image stream and record sound at the same time
|
2018-04-01 08:57:29 +00:00
|
|
|
"""
|
2020-04-26 12:41:11 +00:00
|
|
|
#import cv2 as cv
|
2018-04-01 08:57:29 +00:00
|
|
|
from .lasp_atomic import Atomic
|
|
|
|
from threading import Thread, Condition, Lock
|
2020-02-25 13:35:49 +00:00
|
|
|
import numpy as np
|
2019-12-22 14:00:50 +00:00
|
|
|
|
2018-04-01 08:57:29 +00:00
|
|
|
import time
|
2019-12-22 14:00:50 +00:00
|
|
|
from .device import (RtAudio, DeviceInfo, DAQConfiguration,
|
|
|
|
get_numpy_dtype_from_format_string,
|
2020-08-03 18:17:52 +00:00
|
|
|
get_sampwidth_from_format_string, AvType)
|
2019-12-22 14:00:50 +00:00
|
|
|
|
2020-08-03 18:17:52 +00:00
|
|
|
__all__ = ['AvStream']
|
2018-07-28 12:43:57 +00:00
|
|
|
|
|
|
|
video_x, video_y = 640, 480
|
2018-04-01 08:57:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AvStream:
|
2020-02-25 13:35:49 +00:00
|
|
|
"""Audio and video data stream, to which callbacks can be added for
|
|
|
|
processing the data."""
|
2019-12-22 14:00:50 +00:00
|
|
|
|
2019-12-18 09:02:20 +00:00
|
|
|
def __init__(self,
|
2019-12-22 14:00:50 +00:00
|
|
|
device: DeviceInfo,
|
|
|
|
avtype: AvType,
|
|
|
|
daqconfig: DAQConfiguration,
|
|
|
|
video=None):
|
2020-02-25 13:35:49 +00:00
|
|
|
"""Open a stream for audio in/output and video input. For audio output,
|
|
|
|
by default all available channels are opened for outputting data.
|
2019-12-22 14:00:50 +00:00
|
|
|
|
|
|
|
Args:
|
|
|
|
device: DeviceInfo for the audio device
|
|
|
|
avtype: Type of stream. Input, output or duplex
|
|
|
|
|
|
|
|
daqconfig: DAQConfiguration instance. If duplex mode flag is set,
|
2020-02-25 13:35:49 +00:00
|
|
|
please make sure that output_device is None, as in that case the
|
2019-12-22 14:00:50 +00:00
|
|
|
output config will be taken from the input device.
|
|
|
|
video:
|
|
|
|
"""
|
2018-07-28 12:43:57 +00:00
|
|
|
|
|
|
|
self.daqconfig = daqconfig
|
2020-08-03 18:17:52 +00:00
|
|
|
self.device = device
|
2019-12-22 14:00:50 +00:00
|
|
|
self.avtype = avtype
|
2019-12-23 11:25:37 +00:00
|
|
|
self.duplex_mode = daqconfig.duplex_mode
|
2019-12-18 09:02:20 +00:00
|
|
|
|
2020-08-03 18:17:52 +00:00
|
|
|
self.output_channel_names = [ch.channel_name for ch in daqconfig.getEnabledOutputChannels()]
|
2020-03-30 18:23:17 +00:00
|
|
|
|
2020-02-25 13:35:49 +00:00
|
|
|
if daqconfig.monitor_gen:
|
2020-08-03 18:17:52 +00:00
|
|
|
self.input_channel_names = self.output_channel_names
|
|
|
|
self.input_sensitivity = daqconfig.getEnabledOutputChannelSensitivities()
|
|
|
|
else:
|
|
|
|
self.input_channel_names = []
|
|
|
|
self.input_sensitivity = []
|
2019-12-22 14:00:50 +00:00
|
|
|
|
2020-08-03 18:17:52 +00:00
|
|
|
self.input_sensitivity += daqconfig.getEnabledInputChannelSensitivities()
|
|
|
|
self.input_sensitivity = np.asarray(self.input_sensitivity)
|
2019-12-22 14:00:50 +00:00
|
|
|
|
2019-12-18 09:02:20 +00:00
|
|
|
|
2020-08-03 18:17:52 +00:00
|
|
|
self.input_channel_names += [
|
|
|
|
ch.channel_name for ch in daqconfig.getEnabledInputChannels()]
|
2018-07-28 12:43:57 +00:00
|
|
|
|
2020-08-03 18:17:52 +00:00
|
|
|
self.input_samplerate = float(daqconfig.en_input_rate)
|
|
|
|
self.output_samplerate = float(daqconfig.en_output_rate)
|
2018-07-28 12:43:57 +00:00
|
|
|
|
2020-02-25 13:35:49 +00:00
|
|
|
# Counters for the number of frames that have been coming in
|
2018-04-01 08:57:29 +00:00
|
|
|
self._aframectr = Atomic(0)
|
|
|
|
self._vframectr = Atomic(0)
|
2018-07-28 12:43:57 +00:00
|
|
|
|
2020-02-25 13:35:49 +00:00
|
|
|
# Lock
|
2018-04-01 08:57:29 +00:00
|
|
|
self._callbacklock = Lock()
|
2018-07-28 12:43:57 +00:00
|
|
|
|
2018-04-01 08:57:29 +00:00
|
|
|
self._running = Atomic(False)
|
|
|
|
self._running_cond = Condition()
|
|
|
|
|
|
|
|
self._video = video
|
|
|
|
self._video_started = Atomic(False)
|
2020-02-25 13:35:49 +00:00
|
|
|
|
|
|
|
# Storage for callbacks, specified by type
|
2019-12-22 14:00:50 +00:00
|
|
|
self._callbacks = {
|
|
|
|
AvType.audio_input: [],
|
|
|
|
AvType.audio_output: [],
|
|
|
|
AvType.video: []
|
|
|
|
}
|
2020-02-25 13:35:49 +00:00
|
|
|
|
|
|
|
# Possible, but long not tested: store video
|
2018-04-01 08:57:29 +00:00
|
|
|
self._videothread = None
|
2020-08-03 18:17:52 +00:00
|
|
|
self._rtaudio = RtAudio(daqconfig.api)
|
|
|
|
self.blocksize = self._rtaudio.openStream(self)
|
2018-04-01 08:57:29 +00:00
|
|
|
|
2020-08-03 18:17:52 +00:00
|
|
|
# Fill in numpy data type, and sample width
|
|
|
|
self.input_numpy_dtype = get_numpy_dtype_from_format_string(
|
|
|
|
daqconfig.en_input_sample_format)
|
|
|
|
self.output_numpy_dtype = get_numpy_dtype_from_format_string(
|
|
|
|
daqconfig.en_output_sample_format)
|
2020-04-03 09:12:49 +00:00
|
|
|
|
2020-08-03 18:17:52 +00:00
|
|
|
self.input_sampwidth = get_sampwidth_from_format_string(
|
|
|
|
daqconfig.en_input_sample_format)
|
|
|
|
self.output_sampwidth = get_sampwidth_from_format_string(
|
|
|
|
daqconfig.en_output_sample_format)
|
2020-04-03 09:12:49 +00:00
|
|
|
|
2019-12-18 09:02:20 +00:00
|
|
|
def close(self):
|
|
|
|
self._rtaudio.closeStream()
|
2020-02-25 13:35:49 +00:00
|
|
|
self._rtaudio = None
|
2019-12-18 09:02:20 +00:00
|
|
|
|
2018-09-13 13:53:46 +00:00
|
|
|
def nCallbacks(self):
|
2020-02-25 13:35:49 +00:00
|
|
|
"""Returns the current number of installed callbacks."""
|
2019-12-23 11:25:37 +00:00
|
|
|
return len(self._callbacks[AvType.audio_input]) + \
|
2020-02-25 13:35:49 +00:00
|
|
|
len(self._callbacks[AvType.audio_output]) + \
|
|
|
|
len(self._callbacks[AvType.video])
|
2018-09-13 13:53:46 +00:00
|
|
|
|
2019-12-22 14:00:50 +00:00
|
|
|
def addCallback(self, cb: callable, cbtype: AvType):
|
2020-02-25 13:35:49 +00:00
|
|
|
"""Add as stream callback to the list of callbacks."""
|
2018-04-01 08:57:29 +00:00
|
|
|
with self._callbacklock:
|
2019-12-22 14:00:50 +00:00
|
|
|
outputcallbacks = self._callbacks[AvType.audio_output]
|
|
|
|
if cbtype == AvType.audio_output and len(outputcallbacks) > 0:
|
2020-02-25 13:35:49 +00:00
|
|
|
raise RuntimeError(
|
|
|
|
'Only one audio output callback can be allowed')
|
2019-12-28 11:01:56 +00:00
|
|
|
|
2019-12-22 14:00:50 +00:00
|
|
|
if cb not in self._callbacks[cbtype]:
|
|
|
|
self._callbacks[cbtype].append(cb)
|
2018-07-28 12:43:57 +00:00
|
|
|
|
2019-12-22 14:00:50 +00:00
|
|
|
def removeCallback(self, cb, cbtype: AvType):
|
2018-04-01 08:57:29 +00:00
|
|
|
with self._callbacklock:
|
2019-12-22 14:00:50 +00:00
|
|
|
if cb in self._callbacks[cbtype]:
|
|
|
|
self._callbacks[cbtype].remove(cb)
|
2018-04-01 08:57:29 +00:00
|
|
|
|
|
|
|
def start(self):
|
2020-02-25 13:35:49 +00:00
|
|
|
"""Start the stream, which means the callbacks are called with stream
|
|
|
|
data (audio/video)"""
|
2018-07-28 12:43:57 +00:00
|
|
|
|
2018-04-01 08:57:29 +00:00
|
|
|
if self._running:
|
|
|
|
raise RuntimeError('Stream already started')
|
2018-07-28 12:43:57 +00:00
|
|
|
|
|
|
|
assert self._videothread is None
|
|
|
|
|
2018-04-01 08:57:29 +00:00
|
|
|
self._running <<= True
|
|
|
|
if self._video is not None:
|
|
|
|
self._videothread = Thread(target=self._videoThread)
|
|
|
|
self._videothread.start()
|
|
|
|
else:
|
|
|
|
self._video_started <<= True
|
2019-12-18 09:02:20 +00:00
|
|
|
self._rtaudio.startStream()
|
2018-04-01 08:57:29 +00:00
|
|
|
|
|
|
|
def _videoThread(self):
|
|
|
|
cap = cv.VideoCapture(self._video)
|
|
|
|
if not cap.isOpened():
|
|
|
|
cap.open()
|
|
|
|
vframectr = 0
|
|
|
|
loopctr = 0
|
|
|
|
while self._running:
|
|
|
|
ret, frame = cap.read()
|
|
|
|
# print(frame.shape)
|
2018-07-28 12:43:57 +00:00
|
|
|
if ret is True:
|
2018-04-01 08:57:29 +00:00
|
|
|
if vframectr == 0:
|
|
|
|
self._video_started <<= True
|
|
|
|
with self._callbacklock:
|
2019-12-22 14:00:50 +00:00
|
|
|
for cb in self._callbacks[AvType.video]:
|
|
|
|
cb(frame, vframectr)
|
2018-04-01 08:57:29 +00:00
|
|
|
vframectr += 1
|
|
|
|
self._vframectr += 1
|
|
|
|
else:
|
2019-12-22 14:00:50 +00:00
|
|
|
loopctr += 1
|
2018-04-01 08:57:29 +00:00
|
|
|
if loopctr == 10:
|
|
|
|
print('Error: no video capture!')
|
|
|
|
time.sleep(0.2)
|
|
|
|
|
|
|
|
cap.release()
|
|
|
|
print('stopped videothread')
|
|
|
|
|
2020-04-03 09:12:49 +00:00
|
|
|
def _audioCallback(self, indata, outdata, nframes):
|
2020-02-25 13:35:49 +00:00
|
|
|
"""This is called (from a separate thread) for each audio block."""
|
|
|
|
self._aframectr += nframes
|
2018-04-01 08:57:29 +00:00
|
|
|
with self._callbacklock:
|
2020-02-25 13:35:49 +00:00
|
|
|
# Count the number of output callbacks. If no output callbacks are
|
|
|
|
# present, and there should be output callbacks, we explicitly set
|
|
|
|
# the output buffer to zero
|
|
|
|
noutput_cb = len(self._callbacks[AvType.audio_output])
|
|
|
|
shouldhaveoutput = (self.avtype == AvType.audio_output or
|
2020-08-03 18:17:52 +00:00
|
|
|
self.daqconfig.duplex_mode)
|
|
|
|
if noutput_cb == 0 and shouldhaveoutput and outdata is not None:
|
2020-02-25 13:35:49 +00:00
|
|
|
outdata[:, :] = 0
|
|
|
|
|
|
|
|
# Loop over callbacks
|
2020-08-03 18:17:52 +00:00
|
|
|
if outdata is not None:
|
|
|
|
for cb in self._callbacks[AvType.audio_output]:
|
|
|
|
try:
|
|
|
|
cb(indata, outdata, self._aframectr())
|
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
|
|
|
return 2
|
|
|
|
if indata is not None:
|
|
|
|
for cb in self._callbacks[AvType.audio_input]:
|
|
|
|
try:
|
|
|
|
cb(indata, outdata, self._aframectr())
|
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
|
|
|
return 1
|
2020-02-25 13:35:49 +00:00
|
|
|
|
2019-12-26 15:04:49 +00:00
|
|
|
return 0 if self._running else 1
|
2018-04-01 08:57:29 +00:00
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self._running <<= False
|
|
|
|
with self._running_cond:
|
|
|
|
self._running_cond.notify()
|
|
|
|
if self._video:
|
|
|
|
self._videothread.join()
|
|
|
|
self._videothread = None
|
|
|
|
self._aframectr <<= 0
|
|
|
|
self._vframectr <<= 0
|
|
|
|
self._video_started <<= False
|
2019-12-22 14:00:50 +00:00
|
|
|
self._rtaudio.stopStream()
|
2018-04-01 08:57:29 +00:00
|
|
|
|
2018-09-13 11:56:05 +00:00
|
|
|
def isRunning(self):
|
2018-04-01 08:57:29 +00:00
|
|
|
return self._running()
|
2018-07-28 12:43:57 +00:00
|
|
|
|
2018-04-01 08:57:29 +00:00
|
|
|
def hasVideo(self):
|
|
|
|
return True if self._video is not None else False
|