262 lines
8.1 KiB
Python
262 lines
8.1 KiB
Python
#!/usr/bin/python3.8
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Read data from stream and record sound and video at the same time
|
|
"""
|
|
import dataclasses
|
|
import logging
|
|
import os
|
|
import time
|
|
import h5py
|
|
import numpy as np
|
|
from .lasp_cpp import InDataHandler, StreamMgr
|
|
from .lasp_atomic import Atomic
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class RecordStatus:
|
|
curT: float = 0
|
|
done: bool = False
|
|
|
|
|
|
class Recording:
|
|
"""
|
|
Class used to perform a recording.
|
|
"""
|
|
|
|
def __init__(self, fn: str, streammgr: StreamMgr,
|
|
rectime: float = None, wait: bool = True,
|
|
progressCallback=None,
|
|
startDelay: float = 0):
|
|
"""
|
|
Start a recording. Blocks if wait is set to True.
|
|
|
|
Args:
|
|
fn: Filename to record to. Extension is automatically added if not
|
|
provided.
|
|
stream: AvStream instance to record from. Should have input
|
|
channels!
|
|
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.
|
|
startDelay: Optional delay added before the recording is *actually*
|
|
started in [s].
|
|
"""
|
|
ext = '.h5'
|
|
if ext not in fn:
|
|
fn += ext
|
|
|
|
self.smgr = streammgr
|
|
self.metadata = None
|
|
|
|
if startDelay < 0:
|
|
raise RuntimeError("Invalid start delay value. Should be >= 0")
|
|
|
|
self.startDelay = startDelay
|
|
|
|
# Flag used to indicate that we have passed the start delay
|
|
self.startDelay_passed = False
|
|
|
|
# The amount of seconds (float) that is to be recorded
|
|
self.rectime = rectime
|
|
|
|
# The file name to store data to
|
|
self.fn = fn
|
|
|
|
self.curT_rounded_to_seconds = 0
|
|
|
|
# Counter of the number of blocks
|
|
self.ablockno = Atomic(0)
|
|
|
|
self.progressCallback = progressCallback
|
|
|
|
# Open the file
|
|
self.f = h5py.File(self.fn, 'w')
|
|
f = self.f
|
|
|
|
# This flag is used to delete the file on finish(), and can be used
|
|
# when a recording is canceled.
|
|
self.deleteFile = False
|
|
|
|
# Try to obtain stream metadata
|
|
streamstatus = streammgr.getStreamStatus(StreamMgr.StreamType.input)
|
|
if not streamstatus.runningOK():
|
|
raise RuntimeError(
|
|
"Stream is not running properly. Please first start the stream")
|
|
|
|
self.ad = None
|
|
|
|
logging.debug('Starting record....')
|
|
|
|
self.stop = Atomic(False)
|
|
|
|
self.indh = InDataHandler(streammgr, self.inCallback)
|
|
|
|
if wait:
|
|
logging.debug('Stop recording with CTRL-C')
|
|
try:
|
|
while not self.stop():
|
|
time.sleep(0.01)
|
|
except KeyboardInterrupt:
|
|
logging.debug("Keyboard interrupt on record")
|
|
finally:
|
|
self.finish()
|
|
|
|
def firstFrames(self, data):
|
|
"""
|
|
When the first frames arrive, we setup the file and add all metadata,
|
|
and make ready for storing data.
|
|
"""
|
|
daq = self.smgr.getDaq(StreamMgr.StreamType.input)
|
|
in_ch = daq.enabledInChannels()
|
|
blocksize = daq.framesPerBlock()
|
|
self.blocksize = blocksize
|
|
|
|
# The array data type cannot
|
|
# datatype = daq.dataType()
|
|
dtype = np.dtype(data.dtype)
|
|
|
|
f = self.f
|
|
|
|
# Set the bunch of attributes
|
|
f.attrs['samplerate'] = daq.samplerate()
|
|
f.attrs['nchannels'] = daq.neninchannels()
|
|
f.attrs['blocksize'] = blocksize
|
|
f.attrs['sensitivity'] = [ch.sensitivity for ch in in_ch]
|
|
f.attrs['channelNames'] = [ch.name for ch in in_ch]
|
|
|
|
# Add the start delay here, as firstFrames() is called right after the
|
|
# constructor is called.
|
|
f.attrs['time'] = time.time() + self.startDelay
|
|
|
|
nchannels = len(in_ch)
|
|
self.ad = f.create_dataset('audio',
|
|
(1, blocksize, nchannels),
|
|
dtype=dtype,
|
|
maxshape=(
|
|
None, # This means, we can add blocks
|
|
# indefinitely
|
|
blocksize,
|
|
nchannels),
|
|
compression='gzip'
|
|
)
|
|
self.fs = daq.samplerate()
|
|
|
|
# Measured physical quantity metadata
|
|
# f.attrs['qtys'] = [ch.qty.to_json() for ch in in_ch]
|
|
|
|
# In V2, we do not store JSON metadata anymore, but just an enumeration
|
|
# index to a physical quantity.
|
|
f.attrs['qtys_enum_idx'] = [ch.qty.value for ch in in_ch]
|
|
|
|
def inCallback(self, adata):
|
|
"""
|
|
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.
|
|
|
|
|
|
"""
|
|
if self.ad is None:
|
|
self.firstFrames(adata)
|
|
|
|
self.__addTimeData(adata)
|
|
return True
|
|
|
|
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
|
|
|
|
def finish(self):
|
|
"""
|
|
This method should be called to finish and a close a recording file,
|
|
remove the queue from the stream, etc.
|
|
|
|
"""
|
|
self.stop <<= True
|
|
logging.debug('Recording::finish()')
|
|
|
|
# Remove indata handler
|
|
self.indh = None
|
|
|
|
try:
|
|
# Close the recording file
|
|
self.f.close()
|
|
except Exception as e:
|
|
logging.error(f'Error closing file: {e}')
|
|
|
|
logging.debug('Recording ended')
|
|
if self.deleteFile:
|
|
self.__deleteFile()
|
|
|
|
def __deleteFile(self):
|
|
"""
|
|
Cleanup the recording file.
|
|
"""
|
|
try:
|
|
os.remove(self.fn)
|
|
except Exception as e:
|
|
logging.error(f'Error deleting file: {self.fn}')
|
|
|
|
def __addTimeData(self, indata):
|
|
"""
|
|
Called by handleQueue() and adds new time data to the storage file.
|
|
"""
|
|
# logging.debug('Recording::__addTimeData()')
|
|
|
|
if self.stop():
|
|
# Stop flag is raised. We stop recording here.
|
|
return
|
|
|
|
curT = self.ablockno()*self.blocksize/self.fs
|
|
|
|
# Increase the block counter
|
|
self.ablockno += 1
|
|
|
|
if curT < self.startDelay and not self.startDelay_passed:
|
|
# Start delay has not been passed
|
|
return
|
|
elif curT >= 0 and not self.startDelay_passed:
|
|
# Start delay passed, switch the flag!
|
|
self.startDelay_passed = True
|
|
|
|
# Reset the audio block counter and the recording time
|
|
self.ablockno = Atomic(1)
|
|
curT = 0
|
|
|
|
ablockno = self.ablockno()
|
|
recstatus = RecordStatus(
|
|
curT=curT,
|
|
done=False)
|
|
|
|
if self.progressCallback is not None:
|
|
self.progressCallback(recstatus)
|
|
|
|
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)
|
|
|
|
if self.rectime is not None and curT > self.rectime:
|
|
# We are done!
|
|
if self.progressCallback is not None:
|
|
recstatus.done = True
|
|
self.progressCallback(recstatus)
|
|
self.stop <<= True
|
|
return
|
|
|
|
# Add the data to the file, and resize the audio data blocks
|
|
self.ad.resize(ablockno, axis=0)
|
|
self.ad[ablockno-1, :, :] = indata
|
|
|