In between state of connecting DAQ Devices
This commit is contained in:
parent
e95b02ae9a
commit
64821f8c6f
2
.gitignore
vendored
2
.gitignore
vendored
@ -21,3 +21,5 @@ LASP.egg-info
|
||||
lasp_octave_fir.*
|
||||
lasp/ui_*
|
||||
resources_rc.py
|
||||
lasp/device/lasp_daqdevice.c
|
||||
lasp/lasp_daqwidget.py
|
||||
|
@ -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"
|
||||
|
4
lasp/device/CMakeLists.txt
Normal file
4
lasp/device/CMakeLists.txt
Normal file
@ -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)
|
6
lasp/device/__init__.py
Normal file
6
lasp/device/__init__.py
Normal file
@ -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)
|
138
lasp/device/lasp_daqconfig.py
Normal file
138
lasp/device/lasp_daqconfig.py
Normal file
@ -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)
|
498
lasp/device/lasp_daqdevice.pyx
Normal file
498
lasp/device/lasp_daqdevice.pyx
Normal file
@ -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 = <char**> 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,<void*> &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
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user