182 lines
5.7 KiB
Python
182 lines
5.7 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Read data from stream and record sound and video at the same time
|
|
"""
|
|
from .lasp_atomic import Atomic
|
|
from threading import Condition
|
|
from .lasp_avstream import AvType, AvStream
|
|
import h5py
|
|
import dataclasses
|
|
import os
|
|
import time
|
|
|
|
@dataclasses.dataclass
|
|
class RecordStatus:
|
|
curT: float
|
|
done: bool
|
|
|
|
class Recording:
|
|
|
|
def __init__(self, fn: str, stream: AvStream,
|
|
rectime: float=None, wait: bool = True,
|
|
progressCallback=None):
|
|
"""
|
|
|
|
Args:
|
|
fn: Filename to record to. extension is added
|
|
stream: AvStream instance to record from. Should have input
|
|
channels!
|
|
rectime: Recording time, None for infinite
|
|
"""
|
|
ext = '.h5'
|
|
if ext not in fn:
|
|
fn += ext
|
|
|
|
self._stream = stream
|
|
if stream.avtype != AvType.audio_input:
|
|
raise RuntimeError('Stream does not have any input channels')
|
|
self.blocksize = stream.blocksize
|
|
self.samplerate = stream.samplerate
|
|
self._running = Atomic(False)
|
|
self._running_cond = Condition()
|
|
self.rectime = rectime
|
|
self._fn = fn
|
|
|
|
self._video_frame_positions = []
|
|
self._curT_rounded_to_seconds = 0
|
|
|
|
self._ablockno = Atomic(0)
|
|
self._vframeno = 0
|
|
|
|
self._progressCallback = progressCallback
|
|
self._wait = wait
|
|
|
|
self._f = h5py.File(self._fn, 'w')
|
|
self._deleteFile = False
|
|
|
|
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 __enter__(self):
|
|
"""
|
|
|
|
with Recording(fn, stream, wait=False):
|
|
event_loop_here()
|
|
|
|
or:
|
|
|
|
with Recording(fn, stream, wait=True):
|
|
pass
|
|
"""
|
|
|
|
stream = self._stream
|
|
f = self._f
|
|
nchannels = stream.nchannels
|
|
|
|
self._ad = f.create_dataset('audio',
|
|
(1, stream.blocksize, nchannels),
|
|
dtype=stream.numpy_dtype,
|
|
maxshape=(None, stream.blocksize,
|
|
nchannels),
|
|
compression='gzip'
|
|
)
|
|
if stream.hasVideo():
|
|
video_x, video_y = stream.video_x, stream.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'
|
|
)
|
|
|
|
f.attrs['samplerate'] = stream.samplerate
|
|
f.attrs['nchannels'] = stream.nchannels
|
|
f.attrs['blocksize'] = stream.blocksize
|
|
f.attrs['sensitivity'] = stream.sensitivity
|
|
f.attrs['channel_names'] = stream.channel_names
|
|
f.attrs['time'] = time.time()
|
|
self._running <<= True
|
|
|
|
if not stream.isRunning():
|
|
stream.start()
|
|
|
|
print('Starting record....')
|
|
stream.addCallback(self._aCallback, AvType.audio_input)
|
|
if stream.hasVideo():
|
|
stream.addCallback(self._aCallback, AvType.audio_input)
|
|
|
|
if self._wait:
|
|
with self._running_cond:
|
|
print('Stop recording with CTRL-C')
|
|
try:
|
|
while self._running:
|
|
self._running_cond.wait()
|
|
except KeyboardInterrupt:
|
|
print("Keyboard interrupt on record")
|
|
self._running <<= False
|
|
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
self._running <<= False
|
|
stream = self._stream
|
|
stream.removeCallback(self._aCallback, AvType.audio_input)
|
|
if stream.hasVideo():
|
|
stream.removeCallback(self._vCallback, AvType.video_input)
|
|
self._f['video_frame_positions'] = self._video_frame_positions
|
|
|
|
self._f.close()
|
|
print('\nEnding record')
|
|
if self._deleteFile:
|
|
try:
|
|
os.remove(self._fn)
|
|
except Exception as e:
|
|
print(f'Error deleting file: {self._fn}')
|
|
|
|
|
|
def _aCallback(self, indata, outdata, aframe):
|
|
if indata is None:
|
|
return
|
|
|
|
curT = self._ablockno()*self.blocksize/self.samplerate
|
|
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!
|
|
self._running <<= False
|
|
with self._running_cond:
|
|
self._running_cond.notify()
|
|
if self._progressCallback is not None:
|
|
recstatus.done = True
|
|
self._progressCallback(recstatus)
|
|
return
|
|
|
|
self._ad.resize(self._ablockno()+1, axis=0)
|
|
self._ad[self._ablockno(), :, :] = indata
|
|
self._ablockno += 1
|
|
|
|
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
|
|
|