From 64821f8c6ff962c1d9e47a47abab6a6b1312f1a7 Mon Sep 17 00:00:00 2001 From: "J.A. de Jong" Date: Sat, 28 Jul 2018 14:43:57 +0200 Subject: [PATCH] In between state of connecting DAQ Devices --- .gitignore | 2 + lasp/CMakeLists.txt | 6 +- lasp/device/CMakeLists.txt | 4 + lasp/device/__init__.py | 6 + lasp/device/lasp_daqconfig.py | 138 +++++++++ lasp/device/lasp_daqdevice.pyx | 498 +++++++++++++++++++++++++++++++++ lasp/lasp_avstream.py | 134 ++++----- lasp/lasp_measurement.py | 42 +-- lasp/lasp_record.py | 6 +- 9 files changed, 729 insertions(+), 107 deletions(-) create mode 100644 lasp/device/CMakeLists.txt create mode 100644 lasp/device/__init__.py create mode 100644 lasp/device/lasp_daqconfig.py create mode 100644 lasp/device/lasp_daqdevice.pyx diff --git a/.gitignore b/.gitignore index ed3eb64..1ca4abf 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ LASP.egg-info lasp_octave_fir.* lasp/ui_* resources_rc.py +lasp/device/lasp_daqdevice.c +lasp/lasp_daqwidget.py diff --git a/lasp/CMakeLists.txt b/lasp/CMakeLists.txt index 3f53de0..edbd0eb 100644 --- a/lasp/CMakeLists.txt +++ b/lasp/CMakeLists.txt @@ -1,4 +1,4 @@ -add_subdirectory(c) + configure_file(config.pxi.in config.pxi) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") find_package(PythonLibs REQUIRED ) @@ -10,6 +10,8 @@ include_directories( . c ) + add_subdirectory(c) + add_subdirectory(device) # add the command to generate the source code # add_custom_command ( @@ -18,7 +20,7 @@ include_directories( # DEPENDS MakeTable # ) -set(ui_files ui_apsrt ui_mainwindow ui_figure ui_about ui_apswidget ui_revtime ui_slmwidget) +set(ui_files ui_apsrt ui_mainwindow ui_figure ui_about ui_apswidget ui_revtime ui_slmwidget ui_daq) foreach(fn ${ui_files}) add_custom_command( OUTPUT "${fn}.py" diff --git a/lasp/device/CMakeLists.txt b/lasp/device/CMakeLists.txt new file mode 100644 index 0000000..23e87b9 --- /dev/null +++ b/lasp/device/CMakeLists.txt @@ -0,0 +1,4 @@ +set_source_files_properties(lasp_daqdevice.c PROPERTIES COMPILE_FLAGS "${CMAKE_C_FLAGS} ${CYTHON_EXTRA_C_FLAGS}") +cython_add_module(lasp_daqdevice lasp_daqdevice.pyx) + +target_link_libraries(lasp_daqdevice asound) diff --git a/lasp/device/__init__.py b/lasp/device/__init__.py new file mode 100644 index 0000000..fb04157 --- /dev/null +++ b/lasp/device/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/python3 +__all__ = ['DAQDevice', 'DAQConfiguration', 'configs', 'query_devices', + 'roga_plugndaq', 'default_soundcard'] +from .lasp_daqdevice import DAQDevice, query_devices +from .lasp_daqconfig import (DAQConfiguration, configs, roga_plugndaq, + default_soundcard) diff --git a/lasp/device/lasp_daqconfig.py b/lasp/device/lasp_daqconfig.py new file mode 100644 index 0000000..efcd5df --- /dev/null +++ b/lasp/device/lasp_daqconfig.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""! +Author: J.A. de Jong - ASCEE + +Description: + +Data Acquistiion (DAQ) device descriptors, and the DAQ devices themselves + +""" +__all__ = ['DAQConfiguration', 'roga_plugndaq', 'default_soundcard'] + + +class DAQConfiguration: + def __init__(self, name, + cardname, + cardlongnamematch, + device_name, + en_format, + en_input_rate, + en_input_channels, + + input_sensitivity, + input_gain_settings, + en_input_gain_setting, + + en_output_rate, + en_output_channels): + """ + Initialize a device descriptor + + Args: + name: Name of the device to appear to the user + cardname: ALSA name identifier + cardlongnamematch: Long name according to ALSA + device_name: ASCII name with which to open the device when connected + en_format: index in the list of sample formats + en_input_rate: index of enabled input sampling frequency [Hz] + in the list of frequencies. + en_input_channels: list of channel indices which are used to + acquire data from. + input_sensitivity: List of sensitivity values, in units of [Pa^-1] + input_gain_setting: If a DAQ supports it, list of indices which + corresponds to a position in the possible input + gains for each channel. Should only be not equal + to None when the hardware supports changing the + input gain. + en_output_rate: index in the list of possible output sampling + frequencies. + en_output_channels: list of enabled output channels + + + """ + self.name = name + self.cardlongnamematch = cardlongnamematch + self.cardname = cardname + self.device_name = device_name + self.en_format = en_format + + self.en_input_rate = en_input_rate + self.en_input_channels = en_input_channels + + self.input_sensitivity = input_sensitivity + self.input_gain_settings = input_gain_settings + + self.en_output_rate = en_output_rate + self.en_output_channels = en_output_channels + + def __repr__(self): + """ + String representation of configuration + """ + rep = f"""Name: {self.name} + Enabled input channels: {self.en_input_channels} + Enabled input sampling frequency: {self.en_input_rate} + Input gain settings: {self.input_gain_settings} + Sensitivity: {self.input_sensitivity} + """ + return rep + + + def match(self, device): + """ + Returns True when a device specification dictionary matches to the + configuration. + + Args: + device: dictionary specifying device settings + """ + match = True + if self.cardlongnamematch is not None: + match &= self.cardlongnamematch in device.cardlongname + if self.cardname is not None: + match &= self.cardname == device.cardname + if self.device_name is not None: + match &= self.device_name == device.device_name + match &= self.en_format < len(device.available_formats) + match &= self.en_input_rate < len(device.available_input_rates) + match &= max( + self.en_input_channels) < device.max_input_channels + if len(self.en_output_channels) > 0: + match &= max( + self.en_output_channels) < device.max_output_channels + match &= self.en_output_rate < len( + device.available_output_rates) + + return match + + +roga_plugndaq = DAQConfiguration(name='Roga-instruments Plug.n.DAQ USB', + cardname='USB Audio CODEC', + cardlongnamematch='Burr-Brown from TI USB' + ' Audio CODEC', + device_name='iec958:CARD=CODEC,DEV=0', + en_format=0, + en_input_rate=2, + en_input_channels=[0], + input_sensitivity=[1., 1.], + input_gain_settings=[-20, 0, 20], + en_input_gain_setting=[1, 1], + en_output_rate=1, + en_output_channels=[False, False] + ) + +default_soundcard = DAQConfiguration(name="Default device", + cardname=None, + cardlongnamematch=None, + device_name='default', + en_format=0, + en_input_rate=2, + en_input_channels=[0, 1], + input_sensitivity=[1., 1.], + input_gain_settings=[0], + en_input_gain_setting=[0, 0], + en_output_rate=1, + en_output_channels=[] + ) +configs = (roga_plugndaq, default_soundcard) diff --git a/lasp/device/lasp_daqdevice.pyx b/lasp/device/lasp_daqdevice.pyx new file mode 100644 index 0000000..6ae470f --- /dev/null +++ b/lasp/device/lasp_daqdevice.pyx @@ -0,0 +1,498 @@ +include "config.pxi" +from libc.stdlib cimport malloc, free +from libc.stdio cimport printf, stderr, fprintf +import sys +import numpy as np +cimport numpy as cnp + +__all__ = ['DAQDevice'] + +from libc.errno cimport EPIPE, EBADFD, ESTRPIPE + + +cdef extern from "alsa/asoundlib.h": + int snd_card_get_longname(int index,char** name) + int snd_card_get_name(int index,char** name) + int snd_card_next(int* rcard) + + ctypedef struct snd_pcm_t + ctypedef struct snd_pcm_info_t + ctypedef struct snd_pcm_hw_params_t + ctypedef enum snd_pcm_stream_t: + SND_PCM_STREAM_PLAYBACK + SND_PCM_STREAM_CAPTURE + ctypedef enum snd_pcm_format_t: + SND_PCM_FORMAT_S16_LE + SND_PCM_FORMAT_S16_BE + SND_PCM_FORMAT_U16_LE + SND_PCM_FORMAT_U16_BE + SND_PCM_FORMAT_S24_LE + SND_PCM_FORMAT_S24_BE + SND_PCM_FORMAT_U24_LE + SND_PCM_FORMAT_U24_BE + SND_PCM_FORMAT_S32_LE + SND_PCM_FORMAT_S32_BE + SND_PCM_FORMAT_U32_LE + SND_PCM_FORMAT_U32_BE + SND_PCM_FORMAT_S24_3LE + SND_PCM_FORMAT_S24_3BE + SND_PCM_FORMAT_U24_3LE + SND_PCM_FORMAT_U24_3BE + SND_PCM_FORMAT_S16 + SND_PCM_FORMAT_U16 + SND_PCM_FORMAT_S24 + SND_PCM_FORMAT_U24 + const char* snd_pcm_format_name (snd_pcm_format_t format) + ctypedef enum snd_pcm_access_t: + SND_PCM_ACCESS_RW_INTERLEAVED + ctypedef unsigned long snd_pcm_uframes_t + int snd_pcm_open(snd_pcm_t** pcm,char* name, snd_pcm_stream_t type, int mode) + int snd_pcm_close(snd_pcm_t*) + + int snd_pcm_hw_params_set_access(snd_pcm_t*, snd_pcm_hw_params_t*, snd_pcm_access_t) + void snd_pcm_hw_params_alloca(snd_pcm_hw_params_t**) + int snd_pcm_hw_params_any(snd_pcm_t*, snd_pcm_hw_params_t* params) + int snd_pcm_hw_params_current(snd_pcm_t*, snd_pcm_hw_params_t* params) + int snd_pcm_hw_params_set_rate_resample(snd_pcm_t*, snd_pcm_hw_params_t*, int) + int snd_pcm_hw_params_set_rate(snd_pcm_t*, snd_pcm_hw_params_t*,unsigned int val,int dir) + int snd_pcm_hw_params_set_format(snd_pcm_t*, snd_pcm_hw_params_t*, snd_pcm_format_t) + int snd_pcm_hw_params_set_channels(snd_pcm_t*, snd_pcm_hw_params_t*, unsigned val) + int snd_pcm_hw_params_set_period_size_near(snd_pcm_t*,snd_pcm_hw_params_t*, + snd_pcm_uframes_t*,int* dir) + int snd_pcm_hw_params_get_period_size(snd_pcm_hw_params_t*, + snd_pcm_uframes_t*,int* dir) + int snd_pcm_info(snd_pcm_t*, snd_pcm_info_t*) + void snd_pcm_info_alloca(snd_pcm_info_t**) + int snd_pcm_info_get_card(snd_pcm_info_t*) + + int snd_pcm_drain(snd_pcm_t*) + int snd_pcm_readi(snd_pcm_t*,void* buf,snd_pcm_uframes_t nframes) + int snd_pcm_hw_params(snd_pcm_t*,snd_pcm_hw_params_t*) + int snd_pcm_hw_params_test_rate(snd_pcm_t*, snd_pcm_hw_params_t*, + unsigned int val,int dir) + int snd_pcm_hw_params_test_format(snd_pcm_t*, snd_pcm_hw_params_t*, + snd_pcm_format_t) + int snd_pcm_hw_params_get_channels_max(snd_pcm_hw_params_t*,unsigned int*) + int snd_device_name_hint(int card, const char* iface, void*** hints) + char* snd_device_name_get_hint(void* hint, const char* id) + int snd_device_name_free_hint(void**) + char* snd_strerror(int rval) + +# Check for these sample rates +check_rates = [8000, 44100, 48000, 96000, 19200] + +# First value in tuple: number of significant bits +# Second value: number of bits used in memory +# Third value: S for signed, U for unsigned, L for little endian, +# and B for big endian. +check_formats = {SND_PCM_FORMAT_S16_LE: (16,16,'SL'), + SND_PCM_FORMAT_S16_BE: (16,16,'SB'), + SND_PCM_FORMAT_U16_LE: (16,16,'UL'), + SND_PCM_FORMAT_U16_BE: (16,16,'UB'), + SND_PCM_FORMAT_S24_LE: (24,32,'SL'), + SND_PCM_FORMAT_S24_BE: (24,32,'SB'), + SND_PCM_FORMAT_U24_LE: (24,32,'UL'), + SND_PCM_FORMAT_U24_BE: (24,32,'UB'), + SND_PCM_FORMAT_S32_LE: (32,32,'SL'), + SND_PCM_FORMAT_S32_BE: (32,32,'SB'), + SND_PCM_FORMAT_U32_LE: (32,32,'UL'), + SND_PCM_FORMAT_U32_BE: (32,32,'UB'), + SND_PCM_FORMAT_S24_3LE: (24,24,'SL'), + SND_PCM_FORMAT_S24_3BE: (24,24,'SB'), + SND_PCM_FORMAT_U24_3LE: (24,24,'UL'), + SND_PCM_FORMAT_U24_3BE: (24,24,'UB')} + +devices_opened_card = [False, False, False, False, False, False] + +cdef snd_pcm_t* open_device(char* name, + snd_pcm_stream_t streamtype): + """ + Helper function to properly open the first device of a card + """ + cdef snd_pcm_t* pcm + # if name in devices_opened: + # raise RuntimeError('Device %s is already opened.' %name) + cdef int rval = snd_pcm_open(&pcm, name, streamtype, 0) + if rval == 0: + return pcm + else: + return NULL + +cdef int close_device(snd_pcm_t* dev): + rval = snd_pcm_close(dev) + if rval: + print('Error closing device') + return rval + +class DeviceInfo: + """ + Will later be replaced by a dataclass. Storage container for a lot of + device parameters. + """ + def __repr__(self): + rep = f"""Device name: {self.device_name} + """ + return rep + +def getDeviceInfo(char* device_name): + """ + Open the PCM device for both capture and playback, extract the number of + channels, the samplerates and encoding + + Args: + cardindex: Card number of device, numbered by ALSA + + Returns: + + """ + cdef: + snd_pcm_t* pcm + snd_pcm_hw_params_t* hwparams + snd_pcm_info_t* info + int rval, cardindex + unsigned max_input_channels=0, max_output_channels=0 + char* c_cardname + char *c_cardlongname + + deviceinfo = DeviceInfo() + deviceinfo.device_name = device_name.decode('ASCII') + pcm = open_device(device_name, SND_PCM_STREAM_CAPTURE) + if not pcm: + raise RuntimeError('Unable to open device') + + snd_pcm_info_alloca(&info) + rval = snd_pcm_info(pcm, info) + if rval: + snd_pcm_close(pcm) + raise RuntimeError('Unable to obtain device info') + + cardindex = snd_pcm_info_get_card(info) + cardname = '' + cardlongname = '' + if cardindex >= 0: + snd_card_get_name(cardindex, &c_cardname) + if c_cardname: + cardname = c_cardname.decode('ASCII') + printf('name: %s\n', c_cardname) + free(c_cardname) + + rval = snd_card_get_longname(cardindex, &c_cardlongname) + if c_cardlongname: + printf('longname: %s\n', c_cardlongname) + cardlongname = c_cardlongname.decode('ASCII') + free(c_cardlongname) + deviceinfo.cardname = cardname + deviceinfo.cardlongname = cardlongname + + # Check hardware parameters + snd_pcm_hw_params_alloca(&hwparams) + + # Nothing said about the return value of this function in the documentation + snd_pcm_hw_params_any(pcm, hwparams) + + # Check available sample formats + available_formats = [] + for format in check_formats.keys(): + rval = snd_pcm_hw_params_test_format(pcm, hwparams, format) + if rval == 0: + available_formats.append(check_formats[format]) + deviceinfo.available_formats = available_formats + # # Restrict a configuration space to contain only real hardware rates. + # rval = snd_pcm_hw_params_set_rate_resample(pcm, hwparams, 0) + # if rval !=0: + # fprintf(stderr, 'Unable disable resampling rates') + + # Check available input sample rates + available_input_rates = [] + for rate in check_rates: + rval = snd_pcm_hw_params_test_rate(pcm, hwparams, rate, 0) + if rval == 0: + available_input_rates.append(rate) + deviceinfo.available_input_rates = available_input_rates + + rval = snd_pcm_hw_params_get_channels_max(hwparams, &max_input_channels) + if rval != 0: + fprintf(stderr, "Could not obtain max input channels\n") + deviceinfo.max_input_channels = max_input_channels + + # Close device + rval = snd_pcm_close(pcm) + if rval: + fprintf(stderr, 'Unable to close pcm device.\n') + + deviceinfo.available_output_rates = [] + deviceinfo.max_output_channels = 0 + + # ############################################################### + # Open device for output + pcm = open_device(device_name, SND_PCM_STREAM_CAPTURE) + if pcm == NULL: + # We are unable to open the device for playback, but we were able to + # open in for capture. So this is a valid device. + return deviceinfo + + snd_pcm_hw_params_any(pcm, hwparams) + # Restrict a configuration space to contain only real hardware rates. + # rval = snd_pcm_hw_params_set_rate_resample(pcm, hwparams, 0) + # if rval != 0: + # fprintf(stderr, 'Unable disable resampling rates') + + # Check available input sample rates + available_output_rates = [] + for rate in check_rates: + rval = snd_pcm_hw_params_test_rate(pcm, hwparams, rate, 0) + if rval == 0: + available_output_rates.append(rate) + deviceinfo.available_output_rates = available_output_rates + rval = snd_pcm_hw_params_get_channels_max(hwparams, &max_output_channels) + if rval != 0: + fprintf(stderr, "Could not obtain max output channels") + deviceinfo.max_output_channels = max_output_channels + + # Close device + rval = close_device(pcm) + if rval: + fprintf(stderr, 'Unable to close pcm device %s.', device_name) + + return deviceinfo + + +def query_devices(): + """ + Returns a list of available DAQ devices, where each device is represented + by a dictionary containing parameters of the device + """ + + devices = [] + + cdef: + # Start cardindex at -1, such that the first one is picked by + # snd_card_next() + int cardindex = -1, rval=0, i=0 + void** namehints_opaque + char** namehints + char* c_device_name + + rval = snd_device_name_hint(-1, "pcm", &namehints_opaque) + if rval: + raise RuntimeError('Could not obtain name hints for card %i.' + %cardindex) + + namehints = namehints_opaque + while namehints[i] != NULL: + # printf('namehint[i]: %s\n', namehints[i]) + c_device_name = snd_device_name_get_hint(namehints[i], "NAME") + c_descr = snd_device_name_get_hint(namehints[i], "DESC") + if c_device_name: + device_name = c_device_name.decode('ASCII') + if c_descr: + device_desc = c_descr.decode('ASCII') + free(c_descr) + else: + device_desc = '' + try: + device = getDeviceInfo(c_device_name) + printf('device name: %s\n', c_device_name) + devices.append(device) + except RuntimeError: + pass + free(c_device_name) + + i+=1 + + snd_device_name_free_hint(namehints_opaque) + + return devices + + + +cdef class DAQDevice: + cdef: + snd_pcm_t* pcm + int device_index + object device, config + public snd_pcm_uframes_t blocksize + object callback + + + def __cinit(self): + self.pcm = NULL + + def __init__(self, config, blocksize=2048): + """ + Initialize the DAQ device + + Args: + config: DAQConfiguration instance + blocksize: Number of frames in a single acquisition block + callback: callback used to send data frames to + """ + + self.config = config + devices = query_devices() + + self.device = None + for device in devices: + if self.config.match(device): + # To open the underlying PCM device + device_name = device.device_name.encode('ASCII') + self.device = device + + if self.device is None: + raise RuntimeError(f'Device {self.config.name} is not available') + # if devices_opened[device_index]: + # raise RuntimeError(f'Device {self.config.name} is already opened') + # print('device_name opened:', device_name) + self.pcm = open_device(device_name, SND_PCM_STREAM_CAPTURE) + + # Device is opened. We are going to configure + cdef: + snd_pcm_hw_params_t* params + int rval + snd_pcm_format_t format_code + snd_pcm_uframes_t period_size + snd_pcm_hw_params_alloca(¶ms); + + # Fill it in with default values. + snd_pcm_hw_params_any(self.pcm, params); + + # Set access interleaved + rval = snd_pcm_hw_params_set_access(self.pcm, params, + SND_PCM_ACCESS_RW_INTERLEAVED) + if rval != 0: + snd_pcm_close(self.pcm) + raise RuntimeError('Could not set access mode to interleaved') + + # Set sampling frequency + cdef unsigned int rate + rate = device.available_input_rates[config.en_input_rate] + # printf('Set sample rate: %i\n', rate) + rval = snd_pcm_hw_params_set_rate(self.pcm,params, rate, 0) + if rval != 0: + snd_pcm_close(self.pcm) + raise RuntimeError('Could not set input sampling frequency') + + # Set number of channels + channels_max = max(self.channels_en)+1 + # print('channels_max:', channels_max) + if channels_max > self.device.max_input_channels: + snd_pcm_close(self.pcm) + raise ValueError('Highest required channel is larger than available' + ' channels.') + rval = snd_pcm_hw_params_set_channels(self.pcm, params, channels_max) + if rval != 0: + snd_pcm_close(self.pcm) + raise RuntimeError('Could not set input channels, highest required' + ' input channel: %i.' %channels_max) + + # Find the format description + format_descr = self.device.available_formats[config.en_format] + # Obtain key from value of dictionary + + format_code = list(check_formats.keys())[list(check_formats.values()).index(format_descr)] + # printf('Format code: %s\n', snd_pcm_format_name(format_code)) + + # print(format) + rval = snd_pcm_hw_params_set_format(self.pcm, params, format_code) + if rval != 0: + fprintf(stderr, "Could not set format: %s.", snd_strerror(rval)) + + # Set period size + cdef int dir = 0 + bytedepth = format_descr[1]//8 + # print('byte depth:', bytedepth) + period_size = blocksize + rval = snd_pcm_hw_params_set_period_size_near(self.pcm, + params, + &period_size, &dir) + + if rval != 0: + snd_pcm_close(self.pcm) + raise RuntimeError("Could not set period size: %s." + %snd_strerror(rval)) + + # Write the parameters to the driver + rc = snd_pcm_hw_params(self.pcm, params); + if (rc < 0): + snd_pcm_close(self.pcm) + raise RuntimeError('Could not set hw parameters: %s' %snd_strerror(rc)) + + # Check the block size again, and store it + snd_pcm_hw_params_get_period_size(params, &self.blocksize, + &dir) + # print('Period size:', self.blocksize) + + cdef object _getEmptyBuffer(self): + """ + Return right size empty buffer + """ + format_descr = self.device.available_formats[self.config.en_format] + LB = format_descr[2][1] + assert LB == 'L', 'Only little-endian data format supported' + if format_descr[1] == 16: + dtype = np.int16 + elif format_descr[1] == 32: + dtype = np.int32 + + # interleaved data, order = C + return np.zeros((self.blocksize, + max(self.channels_en)+1), + dtype=dtype, order='C') + + def read(self): + cdef int rval = 0 + buf = self._getEmptyBuffer() + # buf2 = self._getEmptyBuffer() + cdef cnp.int16_t[:, ::1] bufv = buf + rval = snd_pcm_readi(self.pcm, &bufv[0, 0], self.blocksize) + # rval = 2048 + if rval > 0: + # print('Samples obtained:' , rval) + return buf[:rval, self.channels_en] + + # return buf + elif rval == -EPIPE: + raise RuntimeError('Error: buffer overrun: %s', + snd_strerror(rval)) + elif rval == -EBADFD: + raise RuntimeError('Error: could not read from DAQ Device: %s', + snd_strerror(rval)) + elif rval == -ESTRPIPE: + raise RuntimeError('Error: could not read from DAQ Device: %s', + snd_strerror(rval)) + + + def __dealloc__(self): + # printf("dealloc\n") + cdef int rval + if self.pcm: + # print('Closing pcm') + # snd_pcm_drain(self.pcm) + rval = snd_pcm_close(self.pcm) + # devices_opened[self.device_index] = False + if rval != 0: + fprintf(stderr, 'Unable to properly close device: %s\n', + snd_strerror(rval)) + self.pcm = NULL + + @property + def nchannels_all(self): + return self.device.max_input_channels + + @property + def channels_en(self): + return self.config.en_input_channels + + @property + def input_rate(self): + return self.device.available_input_rates[self.config.en_input_rate] + + @property + def channels(self): + return [self.config.en_input_channels] + + cpdef bint isOpened(self): + if self.pcm: + return True + else: + return False diff --git a/lasp/lasp_avstream.py b/lasp/lasp_avstream.py index 65dca11..376745f 100644 --- a/lasp/lasp_avstream.py +++ b/lasp/lasp_avstream.py @@ -6,48 +6,41 @@ Created on Sat Mar 10 08:28:03 2018 @author: Read data from image stream and record sound at the same time """ import cv2 as cv -import sounddevice as sd from .lasp_atomic import Atomic from threading import Thread, Condition, Lock import time -__all__ = ['AvType','AvStream'] +from .device import DAQDevice, roga_plugndaq +__all__ = ['AvType', 'AvStream'] + +video_x, video_y = 640, 480 +dtype, sampwidth = 'int16', 2 -# %% -blocksize = 2048 -video_x,video_y = 640,480 -dtype, sampwidth = 'int32',4 class AvType: - video=0 - audio=1 + video = 0 + audio = 1 + class AvStream: - def __init__(self, audiodeviceno=None, video=None, nchannels = None, samplerate = None): - - audiodevice,audiodeviceno = self._findDevice(audiodeviceno) - if nchannels is None: - self.nchannels = audiodevice['max_input_channels'] - if self.nchannels == 0: - raise RuntimeError('Device has no input channels') - else: - self.nchannels = nchannels - - self.audiodeviceno = audiodeviceno - if samplerate is None: - self.samplerate = audiodevice['default_samplerate'] - else: - self.samplerate = samplerate - - self.blocksize = blocksize - - self.video_x, self.video_y = video_x,video_y + def __init__(self, daqconfig=roga_plugndaq, video=None): + + self.daqconfig = daqconfig + try: + daq = DAQDevice(daqconfig) + self.nchannels = len(daq.channels_enabled) + self.samplerate = daq.input_rate + self.blocksize = daq.blocksize + except Exception as e: + raise RuntimeError(f'Could not initialize DAQ device: {str(e)}') + + self.video_x, self.video_y = video_x, video_y self.dtype, self.sampwidth = dtype, sampwidth - + self._aframectr = Atomic(0) self._vframectr = Atomic(0) - + self._callbacklock = Lock() - + self._running = Atomic(False) self._running_cond = Condition() @@ -59,51 +52,29 @@ class AvStream: def addCallback(self, cb): """ - + Add as stream callback to the list of callbacks """ with self._callbacklock: - if not cb in self._callbacks: + if cb not in self._callbacks: self._callbacks.append(cb) - + def removeCallback(self, cb): with self._callbacklock: if cb in self._callbacks: self._callbacks.remove(cb) - def _findDevice(self, deviceno): - - if deviceno is None: - deviceno = 0 - devices = sd.query_devices() - found = [] - for device in devices: - name = device['name'] - if 'Umik' in name: - found.append((device,deviceno)) - elif 'nanoSHARC' in name: - found.append((device,deviceno)) - deviceno+=1 - - if len(found) == 0: - print('Please choose one of the following:') - print(sd.query_devices()) - raise RuntimeError('Could not find a proper device') - - return found[0] - else: - return (sd.query_devices(deviceno,kind='input'),deviceno) - def start(self): """ - + Start the stream, which means the callbacks are called with stream + data (audio/video) """ - + if self._running: raise RuntimeError('Stream already started') - - assert self._audiothread == None - assert self._videothread == None - + + assert self._audiothread is None + assert self._videothread is None + self._running <<= True self._audiothread = Thread(target=self._audioThread) if self._video is not None: @@ -115,19 +86,16 @@ class AvStream: def _audioThread(self): # Raw stream to allow for in24 packed data type - stream = sd.InputStream( - device=self.audiodeviceno, - dtype=self.dtype, - blocksize=blocksize, - channels=self.nchannels, - samplerate=self.samplerate, - callback=self._audioCallback) - - with stream: - with self._running_cond: - while self._running: - self._running_cond.wait() - print('stopped audiothread') + try: + daq = DAQDevice(self.daqconfig) + # Get a single block first and do not process it. This one often + # contains quite some rubbish. + data = daq.read() + while self._running: + data = daq.read() + self._audioCallback(data) + except RuntimeError as e: + print(f'Runtime error occured during audio capture: {str(e)}') def _videoThread(self): cap = cv.VideoCapture(self._video) @@ -138,32 +106,32 @@ class AvStream: while self._running: ret, frame = cap.read() # print(frame.shape) - if ret==True: + if ret is True: if vframectr == 0: self._video_started <<= True with self._callbacklock: for cb in self._callbacks: - cb(AvType.video,frame,self._aframectr(),vframectr) + cb(AvType.video, frame, self._aframectr(), vframectr) vframectr += 1 self._vframectr += 1 else: - + if loopctr == 10: print('Error: no video capture!') time.sleep(0.2) - loopctr +=1 + loopctr += 1 cap.release() print('stopped videothread') - def _audioCallback(self, indata, nframes, time, status): + def _audioCallback(self, indata): """This is called (from a separate thread) for each audio block.""" if not self._video_started: return - + with self._callbacklock: for cb in self._callbacks: - cb(AvType.audio,indata,self._aframectr(),self._vframectr()) + cb(AvType.audio, indata, self._aframectr(), self._vframectr()) self._aframectr += 1 def stop(self): @@ -181,6 +149,6 @@ class AvStream: def isStarted(self): return self._running() - + def hasVideo(self): return True if self._video is not None else False diff --git a/lasp/lasp_measurement.py b/lasp/lasp_measurement.py index 2e0fd2c..a8646d6 100644 --- a/lasp/lasp_measurement.py +++ b/lasp/lasp_measurement.py @@ -197,6 +197,22 @@ class Measurement: """ return self._time + def scaleBlock(self, block): + # When the data is stored as integers, we assume dB full-scale scaling. + # Hence, when we convert the data to floats, we divide by the maximum + # possible value. + if block.dtype == np.int32: + fac = 2**31 + elif block.dtype == np.int16: + fac = 2**15 + elif block.dtype == np.float64: + fac = 1.0 + else: + raise RuntimeError( + f'Unimplemented data type from recording: {block.dtype}.') + sens = self._sens + return block.astype(LASP_NUMPY_FLOAT_TYPE)/fac/sens[np.newaxis, :] + @property def prms(self): """ @@ -212,10 +228,12 @@ class Measurement: except AttributeError: pass - sens = self._sens pms = 0. - for block in self.iterBlocks(): - pms += np.sum(block/sens[np.newaxis, :], axis=0)**2/self.N + + with self.file() as f: + for block in self.iterBlocks(f): + block = self.scaleBlock(block) + pms += np.sum(block**2, axis=0)/self.N self._prms = np.sqrt(pms) return self._prms @@ -235,21 +253,7 @@ class Measurement: blocks = blocks.reshape(self.nblocks*self.blocksize, self.nchannels) - # When the data is stored as integers, we assume dB full-scale scaling. - # Hence, when we convert the data to floats, we divide by the maximum - # possible value. - if blocks.dtype == np.int32: - fac = 2**31 - elif blocks.dtype == np.int16: - fac = 2**15 - elif blocks.dtype == np.float64: - fac = 1.0 - else: - raise RuntimeError( - f'Unimplemented data type from recording: {blocks.dtype}.') - sens = self._sens - blocks = blocks.astype(LASP_NUMPY_FLOAT_TYPE)/fac/sens[np.newaxis, :] - + blocks = self.scaleBlock(blocks) return blocks def iterBlocks(self, opened_file): @@ -284,7 +288,7 @@ class Measurement: valid = sens.ndim == 1 valid &= sens.shape[0] == self.nchannels - valid &= isinstance(sens.dtype, float) + valid &= sens.dtype == float if not valid: raise ValueError('Invalid sensitivity value(s) given') with self.file('r+') as f: diff --git a/lasp/lasp_record.py b/lasp/lasp_record.py index db9b580..1e36dc0 100644 --- a/lasp/lasp_record.py +++ b/lasp/lasp_record.py @@ -8,7 +8,7 @@ Read data from stream and record sound and video at the same time import numpy as np from .lasp_atomic import Atomic from threading import Condition -from .lasp_avstream import AvType +from .lasp_avstream import AvType, AvStream import h5py import time @@ -122,6 +122,6 @@ class Recording: if __name__ == '__main__': - - rec = Recording('test', 5) + stream = AvStream() + rec = Recording('test', stream, 5) rec.start()