2021-05-08 13:06:11 +00:00
|
|
|
#!/usr/bin/python3.8
|
2018-04-01 08:57:29 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
2018-05-02 14:29:53 +00:00
|
|
|
Read data from stream and record sound and video at the same time
|
2018-04-01 08:57:29 +00:00
|
|
|
"""
|
2021-05-08 13:06:11 +00:00
|
|
|
import dataclasses, logging, os, time, h5py
|
|
|
|
from .lasp_avstream import StreamManager, StreamMetaData, StreamMsg
|
2021-05-07 20:53:29 +00:00
|
|
|
from .lasp_common import AvType
|
2021-05-04 13:10:13 +00:00
|
|
|
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2019-12-23 11:25:37 +00:00
|
|
|
@dataclasses.dataclass
|
|
|
|
class RecordStatus:
|
|
|
|
curT: float
|
|
|
|
done: bool
|
2018-04-01 08:57:29 +00:00
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
|
2018-04-01 08:57:29 +00:00
|
|
|
class Recording:
|
2019-12-22 14:00:50 +00:00
|
|
|
|
2021-05-07 20:53:29 +00:00
|
|
|
def __init__(self, fn: str, streammgr: StreamManager,
|
2021-05-04 13:10:13 +00:00
|
|
|
rectime: float = None, wait: bool = True,
|
2019-12-23 11:25:37 +00:00
|
|
|
progressCallback=None):
|
2018-05-02 14:29:53 +00:00
|
|
|
"""
|
2021-05-04 13:10:13 +00:00
|
|
|
Start a recording. Blocks if wait is set to True.
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2018-07-31 11:09:42 +00:00
|
|
|
Args:
|
2021-05-04 13:10:13 +00:00
|
|
|
fn: Filename to record to. Extension is automatically added if not
|
|
|
|
provided.
|
2020-02-25 13:35:49 +00:00
|
|
|
stream: AvStream instance to record from. Should have input
|
|
|
|
channels!
|
2021-05-04 13:10:13 +00:00
|
|
|
rectime: Recording time [s], None for infinite, in seconds. If set
|
|
|
|
to None, or np.inf, the recording continues indefintely.
|
|
|
|
progressCallback: callable that is called with an instance of
|
|
|
|
RecordStatus instance as argument.
|
2018-05-02 14:29:53 +00:00
|
|
|
"""
|
2018-04-01 08:57:29 +00:00
|
|
|
ext = '.h5'
|
2018-05-02 14:29:53 +00:00
|
|
|
if ext not in fn:
|
2018-04-01 08:57:29 +00:00
|
|
|
fn += ext
|
2019-12-22 14:00:50 +00:00
|
|
|
|
2021-05-08 13:06:11 +00:00
|
|
|
self.smgr = streammgr
|
|
|
|
self.metadata = None
|
2021-05-04 13:10:13 +00:00
|
|
|
|
2018-04-01 08:57:29 +00:00
|
|
|
self.rectime = rectime
|
|
|
|
self._fn = fn
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2018-04-01 08:57:29 +00:00
|
|
|
self._video_frame_positions = []
|
2019-12-18 09:02:20 +00:00
|
|
|
self._curT_rounded_to_seconds = 0
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
# Counter of the number of blocks
|
|
|
|
self._ablockno = 0
|
2018-04-01 08:57:29 +00:00
|
|
|
self._vframeno = 0
|
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
self._progressCallback = progressCallback
|
2019-12-23 11:25:37 +00:00
|
|
|
self._wait = wait
|
|
|
|
|
|
|
|
self._f = h5py.File(self._fn, 'w')
|
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
# This flag is used to delete the file on finish(), and can be used
|
|
|
|
# when a recording is canceled.
|
|
|
|
self._deleteFile = False
|
2018-04-01 08:57:29 +00:00
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
try:
|
2021-05-08 13:06:11 +00:00
|
|
|
# Input queue
|
|
|
|
self.inq = streammgr.addListener()
|
|
|
|
|
|
|
|
except RuntimeError:
|
2021-05-04 13:10:13 +00:00
|
|
|
# Cleanup stuff, something is going wrong when starting the stream
|
|
|
|
try:
|
2021-05-08 13:06:11 +00:00
|
|
|
self._f.close()
|
|
|
|
except Exception as e:
|
|
|
|
logging.error(
|
|
|
|
'Error preliminary closing measurement file {fn}: {str(e)}')
|
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
self.__deleteFile()
|
2021-05-08 13:06:11 +00:00
|
|
|
raise
|
|
|
|
|
|
|
|
# Try to obtain stream metadata
|
|
|
|
streammgr.getStreamStatus(AvType.audio_input)
|
|
|
|
streammgr.getStreamStatus(AvType.audio_duplex)
|
|
|
|
|
|
|
|
self._ad = None
|
|
|
|
|
|
|
|
logging.debug('Starting record....')
|
|
|
|
# TODO: Fix this later when we want video
|
|
|
|
# if stream.hasVideo():
|
|
|
|
# stream.addCallback(self._aCallback, AvType.audio_input)
|
|
|
|
self.stop = False
|
|
|
|
|
|
|
|
if self._wait:
|
|
|
|
logging.debug('Stop recording with CTRL-C')
|
|
|
|
try:
|
|
|
|
while not self.stop:
|
|
|
|
self.handleQueue()
|
|
|
|
time.sleep(0.01)
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
logging.debug("Keyboard interrupt on record")
|
|
|
|
finally:
|
|
|
|
self.finish()
|
|
|
|
|
|
|
|
def handleQueue(self):
|
|
|
|
"""
|
|
|
|
This method should be called to grab data from the input queue, which
|
|
|
|
is filled by the stream, and put it into a file. It should be called at
|
|
|
|
a regular interval to prevent overflowing of the queue. It is called
|
|
|
|
within the start() method of the recording, if block is set to True.
|
|
|
|
Otherwise, it should be called from its parent at regular intervals.
|
|
|
|
For example, in Qt this can be done using a QTimer.
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
while self.inq.qsize() > 0:
|
|
|
|
msg, data = self.inq.get()
|
|
|
|
if msg == StreamMsg.streamData:
|
|
|
|
samples, = data
|
|
|
|
self.__addTimeData(samples)
|
|
|
|
elif msg == StreamMsg.streamStarted:
|
|
|
|
logging.debug(f'handleQueue obtained message {msg}')
|
|
|
|
avtype, metadata = data
|
|
|
|
if metadata is None:
|
|
|
|
raise RuntimeError('BUG: no stream metadata')
|
|
|
|
self.processStreamMetaData(metadata)
|
|
|
|
elif msg == StreamMsg.streamMetaData:
|
|
|
|
logging.debug(f'handleQueue obtained message {msg}')
|
|
|
|
avtype, metadata = data
|
|
|
|
if metadata is not None:
|
|
|
|
self.processStreamMetaData(metadata)
|
|
|
|
else:
|
|
|
|
logging.debug(f'handleQueue obtained message {msg}')
|
|
|
|
# An error occured, we do not remove the file, but we stop.
|
|
|
|
self.stop = True
|
|
|
|
logging.debug(f'Stream message: {msg}. Recording stopped unexpectedly')
|
|
|
|
raise RuntimeError('Recording stopped unexpectedly')
|
2021-05-04 13:10:13 +00:00
|
|
|
|
2021-05-08 13:06:11 +00:00
|
|
|
|
|
|
|
def processStreamMetaData(self, md: StreamMetaData):
|
|
|
|
"""
|
|
|
|
Stream metadata has been catched. This is used to set all metadata in
|
|
|
|
the measurement file
|
|
|
|
|
|
|
|
"""
|
|
|
|
logging.debug('Recording::processStreamMetaData()')
|
2021-05-04 13:10:13 +00:00
|
|
|
# The 'Audio' dataset as specified in lasp_measurement, where data is
|
|
|
|
# send to. We use gzip as compression, this gives moderate a moderate
|
|
|
|
# compression to the data.
|
2021-05-08 13:06:11 +00:00
|
|
|
f = self._f
|
|
|
|
blocksize = md.blocksize
|
|
|
|
nchannels = len(md.in_ch)
|
2019-12-23 11:25:37 +00:00
|
|
|
self._ad = f.create_dataset('audio',
|
2021-05-08 13:06:11 +00:00
|
|
|
(1, blocksize, nchannels),
|
|
|
|
dtype=md.dtype,
|
2021-05-04 13:10:13 +00:00
|
|
|
maxshape=(
|
|
|
|
None, # This means, we can add blocks
|
|
|
|
# indefinitely
|
2021-05-08 13:06:11 +00:00
|
|
|
blocksize,
|
2021-05-04 13:10:13 +00:00
|
|
|
nchannels),
|
2019-12-23 11:25:37 +00:00
|
|
|
compression='gzip'
|
|
|
|
)
|
2021-05-04 13:10:13 +00:00
|
|
|
|
|
|
|
# TODO: This piece of code is not up-to-date and should be changed at a
|
|
|
|
# later instance once we really want to record video simultaneously
|
|
|
|
# with audio.
|
2021-05-08 13:06:11 +00:00
|
|
|
# if smgr.hasVideo():
|
|
|
|
# video_x, video_y = smgr.video_x, smgr.video_y
|
|
|
|
# self._vd = f.create_dataset('video',
|
|
|
|
# (1, video_y, video_x, 3),
|
|
|
|
# dtype='uint8',
|
|
|
|
# maxshape=(
|
|
|
|
# None, video_y, video_x, 3),
|
|
|
|
# compression='gzip'
|
|
|
|
# )
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
# Set the bunch of attributes
|
2021-05-08 13:06:11 +00:00
|
|
|
f.attrs['samplerate'] = md.fs
|
2020-08-05 07:56:58 +00:00
|
|
|
f.attrs['nchannels'] = nchannels
|
2021-05-08 13:06:11 +00:00
|
|
|
f.attrs['blocksize'] = blocksize
|
|
|
|
f.attrs['sensitivity'] = [ch.sensitivity for ch in md.in_ch]
|
|
|
|
f.attrs['channel_names'] = [ch.channel_name for ch in md.in_ch]
|
2019-12-23 11:25:37 +00:00
|
|
|
f.attrs['time'] = time.time()
|
2021-05-08 13:06:11 +00:00
|
|
|
self.blocksize = blocksize
|
|
|
|
self.fs = md.fs
|
2019-12-23 11:25:37 +00:00
|
|
|
|
2021-05-08 13:06:11 +00:00
|
|
|
# Measured physical quantity metadata
|
|
|
|
f.attrs['qtys'] = [ch.qty.to_json() for ch in md.in_ch]
|
|
|
|
self.metadata = md
|
2021-05-04 13:10:13 +00:00
|
|
|
|
|
|
|
def setDelete(self, val: bool):
|
|
|
|
"""
|
|
|
|
Set the delete flag. If set, measurement file is deleted at the end of
|
|
|
|
the recording. Typically used for cleaning up after canceling a
|
|
|
|
recording.
|
|
|
|
"""
|
|
|
|
self._deleteFile = val
|
2019-12-23 11:25:37 +00:00
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
def finish(self):
|
|
|
|
"""
|
|
|
|
This method should be called to finish and a close a recording file,
|
|
|
|
remove the queue from the stream, etc.
|
|
|
|
|
|
|
|
"""
|
2021-05-08 13:06:11 +00:00
|
|
|
logging.debug('Recording::finish()')
|
|
|
|
smgr = self.smgr
|
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
# TODO: Fix when video
|
2021-05-08 13:06:11 +00:00
|
|
|
# if smgr.hasVideo():
|
|
|
|
# smgr.removeCallback(self._vCallback, AvType.video_input)
|
2021-05-04 13:10:13 +00:00
|
|
|
# self._f['video_frame_positions'] = self._video_frame_positions
|
|
|
|
|
|
|
|
try:
|
2021-05-08 13:06:11 +00:00
|
|
|
smgr.removeListener(self.inq)
|
2021-05-04 13:10:13 +00:00
|
|
|
except Exception as e:
|
2021-05-08 13:06:11 +00:00
|
|
|
logging.error(f'Could not remove queue from smgr: {e}')
|
2021-05-04 13:10:13 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
# Close the recording file
|
|
|
|
self._f.close()
|
|
|
|
except Exception as e:
|
|
|
|
logging.error(f'Error closing file: {e}')
|
|
|
|
|
|
|
|
logging.debug('Recording ended')
|
2019-12-23 11:25:37 +00:00
|
|
|
if self._deleteFile:
|
2021-05-04 13:10:13 +00:00
|
|
|
self.__deleteFile()
|
2018-04-01 08:57:29 +00:00
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
def __deleteFile(self):
|
|
|
|
"""
|
|
|
|
Cleanup the recording file.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
os.remove(self._fn)
|
|
|
|
except Exception as e:
|
2021-05-08 13:06:11 +00:00
|
|
|
logging.error(f'Error deleting file: {self._fn}')
|
2021-05-04 13:10:13 +00:00
|
|
|
|
|
|
|
def __addTimeData(self, indata):
|
|
|
|
"""
|
|
|
|
Called by handleQueue() and adds new time data to the storage file.
|
|
|
|
"""
|
2021-05-08 13:06:11 +00:00
|
|
|
# logging.debug('Recording::__addTimeData()')
|
|
|
|
|
|
|
|
if self.stop:
|
|
|
|
# Stop flag is raised. We stop recording here.
|
|
|
|
return
|
2021-05-04 13:10:13 +00:00
|
|
|
|
|
|
|
# The current time that is recorded and stored into the file, without
|
|
|
|
# the new data
|
2021-05-08 13:06:11 +00:00
|
|
|
if not self.metadata:
|
|
|
|
# We obtained stream data, but metadata is not yet available.
|
|
|
|
# Therefore, we request it explicitly and then we return
|
|
|
|
logging.info('Requesting stream metadata')
|
|
|
|
self.smgr.getStreamStatus(AvType.audio_input)
|
|
|
|
self.smgr.getStreamStatus(AvType.audio_duplex)
|
|
|
|
return
|
|
|
|
|
|
|
|
curT = self._ablockno*self.blocksize/self.fs
|
2019-12-23 11:25:37 +00:00
|
|
|
recstatus = RecordStatus(
|
2021-05-04 13:10:13 +00:00
|
|
|
curT=curT,
|
|
|
|
done=False)
|
|
|
|
|
2019-12-23 11:25:37 +00:00
|
|
|
if self._progressCallback is not None:
|
|
|
|
self._progressCallback(recstatus)
|
|
|
|
|
2019-12-18 09:02:20 +00:00
|
|
|
curT_rounded_to_seconds = int(curT)
|
|
|
|
if curT_rounded_to_seconds > self._curT_rounded_to_seconds:
|
|
|
|
self._curT_rounded_to_seconds = curT_rounded_to_seconds
|
|
|
|
print(f'{curT_rounded_to_seconds}', end='', flush=True)
|
|
|
|
else:
|
|
|
|
print('.', end='', flush=True)
|
|
|
|
|
2018-04-01 08:57:29 +00:00
|
|
|
if self.rectime is not None and curT > self.rectime:
|
|
|
|
# We are done!
|
2019-12-23 11:25:37 +00:00
|
|
|
if self._progressCallback is not None:
|
|
|
|
recstatus.done = True
|
|
|
|
self._progressCallback(recstatus)
|
2021-05-04 13:10:13 +00:00
|
|
|
self.stop = True
|
2019-12-23 11:25:37 +00:00
|
|
|
return
|
2018-04-01 08:57:29 +00:00
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
# Add the data to the file
|
|
|
|
self._ad.resize(self._ablockno+1, axis=0)
|
|
|
|
self._ad[self._ablockno, :, :] = indata
|
2018-04-01 08:57:29 +00:00
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
# Increase the block counter
|
|
|
|
self._ablockno += 1
|
2018-05-02 14:29:53 +00:00
|
|
|
|
2021-05-04 13:10:13 +00:00
|
|
|
# def _vCallback(self, frame, framectr):
|
|
|
|
# self._video_frame_positions.append(self._ablockno())
|
|
|
|
# vframeno = self._vframeno
|
|
|
|
# self._vd.resize(vframeno+1, axis=0)
|
|
|
|
# self._vd[vframeno, :, :] = frame
|
|
|
|
# self._vframeno += 1
|