Compare commits

...

60 Commits

Author SHA1 Message Date
Anne de Jong 3738012c3e Merge remote-tracking branch 'origin/develop' into develop
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 1m13s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-04-05 06:49:57 -07:00
Anne de Jong a91640cd8d Explicit picking of driver for windows. 2024-04-05 06:49:48 -07:00
Anne de Jong 0bf621e45c Updated documentation for Windows installation
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Blocked by required conditions Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Has been cancelled Details
2024-04-05 15:49:20 +02:00
Anne de Jong 1f7deca3fd Updated scripts for building. Documentation follows
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 1m26s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-29 04:34:24 -07:00
Anne de Jong 1fb98412b2 Removed hard-coded Numpy include dir
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 1m35s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-27 13:55:24 +01:00
Anne de Jong d50dd35745 Silence warnings from portaudio ALSA backend during device enumeration. Do device enumeration on background thread
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 1m30s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-27 13:45:13 +01:00
Anne de Jong 1765042d20 Downgraded a logging.info() to logging.debug
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 1m45s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-19 14:17:59 +01:00
Anne de Jong 46d1eda94d Merged in develop
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 1m51s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-19 13:40:39 +01:00
Anne de Jong 3005f17400 Added extra getReferemenceMeasurements() method to MeasurementSet. Bumped therefore to v1.6.0 2024-03-19 13:39:17 +01:00
Casper Jansen 33439354f8 Sort MeasurementSet by time stamp
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 1m56s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-14 11:31:07 +01:00
Anne de Jong da023273d8 Bump 1.5.1
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Blocked by required conditions Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Has been cancelled Details
2024-03-14 08:47:32 +01:00
Anne de Jong 84db689e56 Ignore error on rm when no files in build copy dir
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Blocked by required conditions Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Has been cancelled Details
2024-03-14 08:43:47 +01:00
Anne de Jong 83c7aa6ade More subtle locking and unlocking of mutexes in stopstream
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 2m1s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-14 08:25:47 +01:00
Anne de Jong 3c16e33453 Removed deadlock in output stream deletion
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 2m1s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-13 13:29:29 +01:00
Anne de Jong e973f14884 Weak refs to Recording methods. Made the mutexes more simple for stream manager. Added extra guards and statements here and there. Code passes a sever stress test.
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 2m3s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-13 12:19:24 +01:00
Anne de Jong e24cac2805 Some more bugfixes: weak references stored in indatahandler, to avoid calling destructor from wrong thread. Removed some unneccessary include statements on the way 2024-03-12 21:13:13 +01:00
Anne de Jong d0d494fcb2 Added some stuff to gitignore, removed explicit dependency on Numpy
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 2m1s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-12 15:53:37 +01:00
Anne de Jong 15cd62baf8 New smoothing algorithm - minor version bump
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 2m6s Details
2024-03-12 11:20:31 +01:00
Anne de Jong ab080910fc Made power correction in smoothing algorithm optional. Window decreases in size symmetrically around the edged of the frequency spectrum
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Blocked by required conditions Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Has been cancelled Details
2024-03-12 11:19:52 +01:00
Anne de Jong 6799ee9287 Bugfix new smoother, including ac signal power correction
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 2m1s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-12 09:21:07 +01:00
Anne de Jong f9cf059c90 Forgot to actually commit the Cpp files of the smoother
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 8s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-11 16:33:28 +01:00
Anne de Jong 3ec15ec645 New smoothing implementation, that runs a bit faster
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after -1m19s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-11 16:04:24 +01:00
Anne de Jong 48d262fbf0 Bugfix in sensitivity correction of realtime spectra
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 2m8s Details
2024-03-07 09:36:50 +01:00
Anne de Jong 204e431d79 Bugfix on GIL release
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 2m9s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-06 22:12:42 +01:00
Anne de Jong bf06402b11 BUGfix of segfault. Very subtle. ThreadedInDataHandler could be deleted, while a task was just pushed to the thread pool. Then, when the task is finally run, the object could be deleted, as the _thread_running flag was not set. Besides this, we made some fixes that makes sure that the handles to a Recording class are stored as a weakref inside of the C++ code. This makes it easier to garbage-collect a recording, even when the IndataHandler is still running. 2024-03-06 21:41:04 +01:00
Anne de Jong 26eef040a4 More locks on signal generator.
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 1m33s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-03-04 15:49:29 +01:00
Anne de Jong b61e836f35 Bumped 1.4.6
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 1m38s Details
2024-03-04 14:44:37 +01:00
Anne de Jong 0841dbd73b Create InDataHandler only from the moment startThread() is called. This is safer, and might fix a segfault 2024-03-04 14:44:00 +01:00
Anne de Jong 5e8e40db7a Updated tag. forgot in previous tag updates
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 1m47s Details
2024-02-29 20:05:10 +01:00
Casper Jansen 3b2f2f7c41 Bugfix record indefinitely
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 1m44s Details
2024-02-27 11:02:45 +01:00
Anne de Jong 878da3369b Bugfix (delete measurement when no data is in it) and cleanup of recording code
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 1m49s Details
2024-02-26 11:51:59 +01:00
Anne de Jong e9f500d460 Small change in portaudio.cmake
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Failing after 2m42s Details
2024-02-20 15:47:12 +01:00
Anne de Jong 6bda124196 Allow duplex mode for PortAudio ALSA devices
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m53s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-02-06 15:02:25 +01:00
Anne de Jong 7ce45e9c82 Some comment improvements, and portaudio API improvements. Also, disabled PortAudio PulseAudio backend as it is not working properly.
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m35s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Successful in -31s Details
2024-02-06 14:59:51 +01:00
Anne de Jong 7c8e6368ba Removed accidental use of wrong time weighting for impulse (35 ms). 2024-02-06 11:22:31 +01:00
Anne de Jong 7430e2c600 Updated armadillo
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 3m0s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-01-30 14:40:43 +01:00
Anne de Jong 6b337df2a9 Bugfix in channel counter for getHighestEnabledOutChannel, added Api -subapis for Portaudio backend. Switch to defaulting Portaudio as audio backend. Added PulseAudio as extra sub-api to default compile in portaudio
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 3m2s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-01-25 15:31:53 +01:00
Anne de Jong c713806bbe RtAudio not updated to 6.0.1?
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m52s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-01-23 15:18:04 +01:00
Anne de Jong 08010e56dd From now on build default LASP with Portaudio backend. Also on Linux. Code cleanup of Portaudio glue code
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 3m3s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-01-20 11:52:16 +01:00
Anne de Jong 292a9d5938 Merge branch 'RtAudioV6' into develop 2024-01-19 12:37:16 +01:00
Anne de Jong 373dcfb60f Bugfixes (that could potentially segfault) in PortAudio backend.
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m54s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Successful in 2s Details
2024-01-19 12:35:56 +01:00
Anne de Jong e8408ab53d Updated to RtAudio V6 2024-01-19 12:32:45 +01:00
Anne de Jong 0d152f6c14 Maded API changes to match RtAudio V6 2024-01-19 12:32:03 +01:00
Anne de Jong 46bef007ca Added muZ series impedance reference as measurementtype
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Has been cancelled Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been cancelled Details
2024-01-15 16:41:29 +01:00
Anne de Jong fd8366c362 Bugfix in measurementset
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Waiting to run Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Blocked by required conditions Details
2024-01-12 15:18:58 +01:00
Anne de Jong 6d5899c880 Merged in develop
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m39s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-01-10 17:31:59 +01:00
Anne de Jong 061beaf88b Small bugfix of some dead code 2024-01-10 17:30:56 +01:00
Thijs Hekman e8ba3b86bf Bugfix on bugfix. KeyError instead of AttributeError
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m29s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-01-10 13:14:54 +01:00
Anne de Jong 14ab3d9dfe Version bump: bugfix
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m27s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-01-10 13:01:38 +01:00
Anne de Jong 695a05b262 BUGFIX: Prevent corrupting all files when no UUID is yet stored in a file 2024-01-10 13:01:07 +01:00
Anne de Jong 514ed1aa32 Added physicalOutputQty for daq devices, added possibility to inspect from Python whether device has monitor. Added unit for equation in Qtys. Version bump 1.3.0
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m49s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2024-01-10 12:26:38 +01:00
Anne de Jong 0be8dd71d9 Bugfixes: store UUID attribute early when recording is done. Some small improvements
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m15s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Successful in -1m38s Details
2023-12-19 14:34:47 +01:00
Anne de Jong 2cd4c616b3 Bump 1.2.0
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 2m11s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Successful in -1m25s Details
2023-12-19 14:04:43 +01:00
Anne de Jong 311a1274bf Added fromFile() method to overcome problem of multiple times opening same file 2023-12-19 14:03:46 +01:00
Anne de Jong 936f2d5708 Merge remote-tracking branch 'origin/develop' into develop
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 3m18s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2023-12-19 12:28:18 +01:00
Anne de Jong 98d4e8dad2 More checks on refMeas. Renamed method refMeas to getRefMeas. Added removeRefMeas. Stabilized API? 2023-12-19 12:27:44 +01:00
Anne de Jong 87283e4aba Changed pure random UUID of measurement to time-based UUID 2023-12-19 12:26:22 +01:00
Thijs Hekman e5c40c6af3 Merged in lasp from remote
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in 3m8s Details
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped Details
2023-12-19 11:24:04 +01:00
Thijs Hekman 8c7dbed606 Bugfix in qty input of Measurement.fromnpy() 2023-12-19 11:21:13 +01:00
Anne de Jong c610c6350d Implemented reference measurements, and renaming as method in Measurement object. 2023-12-18 12:37:46 +01:00
50 changed files with 1791 additions and 784 deletions

View File

@ -25,7 +25,7 @@ jobs:
- name: Cleanup old dist files and copy new to /dist dir
run: |-
rm /dist/*
rm -f /dist/*
cp -v dist/* /dist
Release-Ubuntu:

3
.gitignore vendored
View File

@ -21,3 +21,6 @@ acme_log.log
.venv
.py-build-cmake_cache
cpp_src/lasp_config.h
.cache
.vscode
build

View File

@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 3.16)
project(LASP LANGUAGES C CXX VERSION 1.0)
project(LASP LANGUAGES C CXX VERSION 1.1)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED)
@ -13,8 +13,8 @@ if(WIN32)
set(DEFAULT_PORTAUDIO ON)
set(DEFAULT_ULDAQ OFF)
else()
set(DEFAULT_RTAUDIO ON)
set(DEFAULT_PORTAUDIO OFF)
set(DEFAULT_RTAUDIO OFF)
set(DEFAULT_PORTAUDIO ON)
set(DEFAULT_ULDAQ ON)
endif()
@ -109,9 +109,12 @@ set(CMAKE_C_FLAGS_RELEASE "-O3 -flto -mfpmath=sse -march=x86-64 -mtune=native \
-fdata-sections -ffunction-sections -fomit-frame-pointer -finline-functions")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-type-limits -Werror=return-type")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -flto -mfpmath=sse -march=x86-64 -mtune=native \
-fdata-sections -ffunction-sections -fomit-frame-pointer -finline-functions")
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g -Wall ")
# ############################# End compilation flags
include_directories(/usr/lib/python3.10/site-packages/numpy/core/include)
# ####################################### End of user-adjustable variables section
include(OSSpecific)

View File

@ -1,4 +1,5 @@
# Library for Acoustic Signal Processing
Library for Acoustic Signal Processing
======================================
Welcome to LASP: Library for Acoustic Signal Processing. LASP is a C++ library
@ -45,17 +46,14 @@ If you have any question(s), please feel free to contact us: [email](info@ascee.
# Installation - Linux (Ubuntu-based)
## From wheel (recommended for non-developers)
### Prerequisites
## Prerequisites
Run the following on the command line to install all prerequisites on
Debian-based Linux:
- `sudo apt install python3-pip libfftw3-3 libopenblas-base libusb-1.0-0
libpulse0`
- `sudo apt install python3-pip libfftw3-3 libopenblas-base libusb-1.0-0 libpulse0`
### Download and install LASP
## Installation from wheel (recommended for non-developers)
Go to: [LASP releases](https://code.ascee.nl/ASCEE/lasp/releases/latest/) and
download the latest `.whl`. Then run:
@ -84,35 +82,43 @@ If building RtAudio with the Jack Audio Connection Kit (JACK) backend, you will
- `$ cd lasp`
- `pip install -e .`
# Installation - (x86_64) Windows (with WinPython), build with MSYS2 (NOT YET UPDATED!!)
# Installation - (x86_64) Windows (with WinPython), build with MSYS2
## Prerequisites
- Download and install [WinPython](https://winpython.github.io)
## From wheel
- Download latest wheel from [LASP releases](https://code.ascee.nl/ASCEE/lasp/releases/latest/) and
download the latest `.whl`. Then install with `pip`.
## From source
- Download and install [MSYS2](https://msys2.org). Make sure to install the
x86_64 version.
- Download and install [Git for Windows](https://git-scm.com)
- When unzipping WinPython, make sure to choose a proper and simple path, i.e.
C:\winpython
- Append C:\winpython\ to the PATH environment variable.
- Run Python and install Pybind11
- `python -m pip install pybind11`
- Open a msys2 **MINGW64** terminal. And run:
- `pacman -S git`
- Then clone the LASP repo:
- `git clone https://code.ascee.nl/ascee/lasp`
- `cd lasp`
- Configure MSYS2 further, and run cmake:
- `scripts/install_msys2_buiddeps.sh`
- `scripts/configur_cmake_msys2.sh`
- Download and install [Git for Windows](https://git-scm.com)
- Open an MSYS2 **MINGW64** terminal, and install some tools we require:
- `$ pacman -S git`
- Create a new virtualenv:
- `$ /c/winpython/<py-distr-dir>/python.exe -m venv venv`
- Add the venv-python to the path (eases a lot of commands)
- `$ export PATH=$PATH:~/venv/Scripts`
- Install `build`:
- `$ pip install build`
- Clone LASP:
- `$ git clone --recurse-submodules https://code.ascee.nl/ascee/lasp && cd lasp`
- If run for the first time, we have to install the libraries we depend on in
MSYS2 (this only has to be done on a fresh MSYS2 installation):
- `$ scripts/install_msys2_builddeps.sh`
- Copy over required DLL's to be included in distribution:
- `scripts/copy_windows_dlls.sh`
- And... build!
- `pyproject-build`
- Lastly: the generated wheel can be installed in the current virtualenv:
- `pip install dist/lasp*.whl`
# Documentation
@ -121,7 +127,7 @@ If building RtAudio with the Jack Audio Connection Kit (JACK) backend, you will
[Online LASP documentation](https://lasp.ascee.nl/).
## In directory
## In directory (Linux/Debian)
`$ sudo apt install doxygen graphviz`
`$ pip install doxypypy`

View File

@ -2,13 +2,27 @@
if(LASP_HAS_PORTAUDIO)
message("Building with Portaudio backend")
if(WIN32)
set(PA_USE_ALSA FALSE CACHE BOOL "Build PortAudio with ALSA backend")
set(PA_USE_ASIO TRUE CACHE BOOL "Build PortAudio with ASIO backend")
set(PA_USE_DS FALSE CACHE BOOL "Build PortAudio with Directsound backend")
set(PA_USE_WMME FALSE CACHE BOOL "Build PortAudio with WMME backend")
set(PA_USE_WDMKS FALSE CACHE BOOL "Build PortAudio with WDMKS backend")
else()
# Unix
set(PA_USE_ALSA TRUE CACHE BOOL "Build PortAudio with ALSA backend")
set(PA_USE_JACK TRUE CACHE BOOL "Build PortAudio with Jack backend")
# set(PA_ALSA_DYNAMIC FALSE CACHE BOOL "Build static library of ALSA")
set(PA_USE_JACK FALSE CACHE BOOL "Build PortAudio with Jack backend")
set(PA_USE_PULSEAUDIO FALSE CACHE BOOL "Build PortAudio with PulseAudio backend")
set(PA_BUILD_SHARED_LIBS FALSE CACHE BOOL "Build static library")
endif()
add_subdirectory(third_party/portaudio)
include_directories(third_party/portaudio/include)
link_directories(third_party/portaudio)
if(PA_USE_ALSA)
add_definitions(-DLASP_HAS_PA_ALSA=1)
else()
add_definitions(-DLASP_HAS_PA_ALSA=0)
endif()
endif()

View File

@ -38,16 +38,26 @@ pybind11_add_module(lasp_cpp MODULE lasp_cpp.cpp
target_link_libraries(lasp_cpp PRIVATE lasp_device_lib lasp_dsp_lib
${OpenMP_CXX_LIBRARIES} ${LASP_FFT_LIBS} ${TARGET_OS_LINKLIBS})
# Install the Python module
install(TARGETS lasp_cpp
EXCLUDE_FROM_ALL
COMPONENT python_modules
DESTINATION ${PY_BUILD_CMAKE_MODULE_NAME})
# Install the debug file for the Python module (Windows only)
if (WIN32)
install(FILES $<TARGET_PDB_FILE:_add_module>
target_compile_definitions(lasp_cpp PRIVATE
MODULE_NAME=$<TARGET_FILE_BASE_NAME:lasp_cpp>
VERSION_INFO="${PY_FULL_VERSION}"
)
# Hide all symbols by default (including external libraries on Linux)
if(CMAKE_BUILD_TYPE STREQUAL "Release")
set_target_properties(lasp_cpp PROPERTIES
CXX_VISIBILITY_PRESET "hidden"
VISIBILITY_INLINES_HIDDEN true)
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
target_link_options(lasp_cpp PRIVATE "LINKER:--exclude-libs,ALL")
endif()
endif()
if(DEFINED PY_BUILD_CMAKE_MODULE_NAME)
# Install the Python module
install(TARGETS lasp_cpp
EXCLUDE_FROM_ALL
COMPONENT python_modules
DESTINATION ${PY_BUILD_CMAKE_MODULE_NAME}
OPTIONAL)
endif()
DESTINATION ${PY_BUILD_CMAKE_MODULE_NAME})
endif()

View File

@ -34,7 +34,7 @@ if(LASP_HAS_RTAUDIO)
target_link_libraries(lasp_device_lib rtaudio)
endif()
if(LASP_HAS_PORTAUDIO)
target_link_libraries(lasp_device_lib portaudio)
target_link_libraries(lasp_device_lib PortAudio)
if(WIN32)
else()
target_link_libraries(lasp_device_lib asound)

View File

@ -1,4 +1,4 @@
/* #define DEBUGTRACE_ENABLED */
// #define DEBUGTRACE_ENABLED
#include "debugtrace.hpp"
#include "lasp_daqconfig.h"
@ -44,34 +44,41 @@ Daq::Daq(const DeviceInfo &devinfo, const DaqConfiguration &config)
: DaqConfiguration(config), DeviceInfo(devinfo) {
DEBUGTRACE_ENTER;
if (duplexMode()) {
if (neninchannels() == 0) {
throw rte("Duplex mode enabled, but no input channels enabled");
}
if (nenoutchannels() == 0) {
throw rte("Duplex mode enabled, but no output channels enabled");
}
}
if(!duplexMode() && monitorOutput) {
throw rte("Output monitoring only allowed when running in duplex mode");
throw rte("Duplex mode requires enabling both input and output channels. Please make sure at least one output channel is enabled, or disable hardware output loopback in DAQ configuration.");
}
if (!hasInternalOutputMonitor && monitorOutput) {
throw rte(
"Output monitor flag set, but device does not have output monitor");
"Output monitor flag set, but device does not have hardware output monitor.");
}
if (!config.match(devinfo)) {
throw rte("DaqConfiguration does not match device info");
}
if (neninchannels(false) > devinfo.ninchannels) {
throw rte(
"Number of enabled input channels is higher than device capability");
{
const int hich = getHighestEnabledInChannel();
if (hich + 1 > devinfo.ninchannels)
{
throw rte(
string("Highest of enabled input channel: ") +
to_string(hich) +
string(" is higher than device capability, which is: ") +
to_string(ninchannels) + ".");
}
}
if (nenoutchannels() > devinfo.noutchannels) {
throw rte(
"Number of enabled output channels is higher than device capability");
{
const int hoch = getHighestEnabledOutChannel();
if (hoch + 1 > devinfo.noutchannels)
{
throw rte(
string("Highest of enabled output channel: ") +
to_string(hoch) +
string(" is higher than device capability, which is: ") +
to_string(noutchannels) + ".");
}
}
}

View File

@ -48,6 +48,10 @@ public:
logicError,
};
// Below the only members of this class, which are public.
bool isRunning = false;
StreamError errorType{StreamError::noError};
/**
* @brief Map between error types and messages
*/
@ -61,7 +65,7 @@ public:
{StreamError::logicError, "Logic error (probably a bug)"},
};
bool isRunning = false;
/**
* @brief Check if stream has error
*
@ -69,8 +73,6 @@ public:
*/
bool error() const { return errorType != StreamError::noError; };
StreamError errorType{StreamError::noError};
std::string errorMsg() const { return errorMessages.at(errorType); }
/**

View File

@ -35,12 +35,14 @@ DaqConfiguration::DaqConfiguration(const DeviceInfo &device) {
us i = 0;
for (auto &inch : inchannel_config) {
inch.name = "Unnamed input channel " + std::to_string(i);
inch.rangeIndex = device.prefInputRangeIndex;
i++;
}
i = 0;
for (auto &outch : outchannel_config) {
outch.name = "Unnamed output channel " + std::to_string(i);
outch.rangeIndex = device.prefOutputRangeIndex;
i++;
}
@ -54,10 +56,12 @@ DaqConfiguration::DaqConfiguration(const DeviceInfo &device) {
}
bool DaqConfiguration::match(const DeviceInfo &dev) const {
DEBUGTRACE_ENTER;
return (dev.device_name == device_name && dev.api == api);
}
int DaqConfiguration::getHighestEnabledInChannel() const {
DEBUGTRACE_ENTER;
for (int i = inchannel_config.size() - 1; i > -1; i--) {
if (inchannel_config.at(i).enabled)
return i;
@ -66,13 +70,15 @@ int DaqConfiguration::getHighestEnabledInChannel() const {
}
int DaqConfiguration::getHighestEnabledOutChannel() const {
for (us i = outchannel_config.size() - 1; i >= 0; i--) {
DEBUGTRACE_ENTER;
for (int i = outchannel_config.size() - 1; i > -1; i--) {
if (outchannel_config.at(i).enabled)
return i;
}
return -1;
}
int DaqConfiguration::getLowestEnabledInChannel() const {
DEBUGTRACE_ENTER;
for (us i = 0; i < inchannel_config.size(); i++) {
if (inchannel_config.at(i).enabled)
return i;

View File

@ -148,7 +148,14 @@ const DaqApi rtaudioAsioApi("RtAudio Windows ASIO", LASP_RTAUDIO_APICODE,
#endif
#if LASP_HAS_PORTAUDIO == 1
const us LASP_PORTAUDIO_APICODE = 2;
const DaqApi portaudioApi("PortAudio Linux ALSA", LASP_PORTAUDIO_APICODE, 0);
const DaqApi portaudioALSAApi("PortAudio Linux ALSA", LASP_PORTAUDIO_APICODE, 0);
const DaqApi portaudioPulseApi("PortAudio Linux PulseAudio", LASP_PORTAUDIO_APICODE, 1);
const DaqApi portaudioASIOApi("PortAudio Windows ASIO", LASP_PORTAUDIO_APICODE, 2);
const DaqApi portaudioDSApi("PortAudio Windows DirectSound", LASP_PORTAUDIO_APICODE, 3);
const DaqApi portaudioWMMEApi("PortAudio Windows WMME", LASP_PORTAUDIO_APICODE, 4);
const DaqApi portaudioWASAPIApi("PortAudio Windows WASAPI", LASP_PORTAUDIO_APICODE, 5);
const DaqApi portaudioWDMKS("PortAudio Windows WDMKS", LASP_PORTAUDIO_APICODE, 6);
const DaqApi portaudioDirectSoundApi("PortAudio Windows DirectSound", LASP_PORTAUDIO_APICODE, 7);
#endif
class DeviceInfo;

View File

@ -68,14 +68,26 @@ public:
us prefFramesPerBlockIndex = 0;
/**
* @brief Available ranges for the input, i.e. +/- 1V and/or +/- 10 V etc.
* @brief Available ranges for the input, i.e. +/- 1V and/or +/- 10 V etc.
*/
dvec availableInputRanges;
/**
* @brief Its preffered range
* @brief Available ranges for the output, i.e. +/- 1V and/or +/- 10 V etc.
*/
dvec availableOutputRanges;
/**
* @brief Its preffered input range
*/
int prefInputRangeIndex = 0;
/**
* @brief Its preffered output range
*/
int prefOutputRangeIndex = 0;
/**
* @brief The number of input channels available for the device
*/
@ -125,13 +137,29 @@ public:
bool duplexModeForced = false;
/**
* @brief The physical quantity of the output signal. For 'normal' audio
* @brief Indicates whether the device is able to run in duplex mode. If false,
* devices cannot run in duplex mode, and the `duplexModeForced` flag is meaningless.
*/
bool hasDuplexMode = false;
/**
* @brief The physical quantity of the input signal from DAQ. For 'normal' audio
* interfaces, this is typically a 'number' between +/- full scale. For some
* real DAQ devices however, the input quantity corresponds to a physical signal,
* such a Volts.
*/
DaqChannel::Qty physicalInputQty = DaqChannel::Qty::Number;
/**
* @brief The physical quantity of the output signal from DAQ. For 'normal' audio
* devices, this is typically a 'number' between +/- full scale. For some
* devices however, the output quantity corresponds to a physical signal,
* real DAQ devices however, the input quantity corresponds to a physical signal,
* such a Volts.
*/
DaqChannel::Qty physicalOutputQty = DaqChannel::Qty::Number;
/**
* @brief String representation of DeviceInfo
*

View File

@ -1,11 +1,11 @@
/* #define DEBUGTRACE_ENABLED */
// #define DEBUGTRACE_ENABLED
#include "lasp_indatahandler.h"
#include "debugtrace.hpp"
#include "lasp_streammgr.h"
#include <thread>
InDataHandler::InDataHandler(SmgrHandle mgr, const InCallbackType cb,
const InResetType resetfcn)
const ResetCallbackType resetfcn)
: _mgr(mgr), inCallback(cb), reset(resetfcn)
#if LASP_DEBUG == 1
,
@ -29,20 +29,22 @@ void InDataHandler::start() {
}
void InDataHandler::stop() {
DEBUGTRACE_ENTER;
checkRightThread();
// checkRightThread();
#if LASP_DEBUG == 1
stopCalled = true;
#endif
if (SmgrHandle handle = _mgr.lock()) {
handle->removeInDataHandler(*this);
if (SmgrHandle smgr = _mgr.lock()) {
smgr->removeInDataHandler(*this);
} else {
DEBUGTRACE_PRINT("No stream manager alive anymore!");
}
}
InDataHandler::~InDataHandler() {
DEBUGTRACE_ENTER;
checkRightThread();
#if LASP_DEBUG == 1
// checkRightThread();
if (!stopCalled) {
std::cerr << "************ BUG: Stop function not called while arriving at "
"InDataHandler's destructor. Fix this by calling "

View File

@ -22,7 +22,7 @@ using InCallbackType = std::function<void(const DaqData &)>;
/**
* @brief Function definition for the reset callback.
*/
using InResetType = std::function<void(const Daq *)>;
using ResetCallbackType = std::function<void(const Daq *)>;
class InDataHandler {
@ -38,7 +38,7 @@ protected:
public:
~InDataHandler();
const InCallbackType inCallback;
const InResetType reset;
const ResetCallbackType reset;
/**
* @brief When constructed, the handler is added to the stream manager, which
@ -50,7 +50,7 @@ public:
* changes state.
*/
InDataHandler(SmgrHandle mgr, InCallbackType cb,
InResetType resetfcn);
ResetCallbackType resetfcn);
/**
* @brief Adds the current InDataHandler to the list of handlers in the

View File

@ -17,37 +17,46 @@ using rte = std::runtime_error;
using std::vector;
using lck = std::scoped_lock<std::mutex>;
class RtAudioDeviceInfo : public DeviceInfo {
const unsigned RTAUDIO_MAX_CHANNELS = 8;
class RtAudioDeviceInfo : public DeviceInfo
{
public:
/**
* @brief Specific for the device (Sub-API). Important for the RtAudio
* backend, as RtAudio is able to handle different API's.
*/
int _api_devindex;
virtual std::unique_ptr<DeviceInfo> clone() const override {
int ID; // Copy of RtAudio::DeviceInfo::ID
virtual std::unique_ptr<DeviceInfo> clone() const override
{
return std::make_unique<DeviceInfo>(*this);
}
};
void fillRtAudioDeviceInfo(DeviceInfoList &devinfolist) {
void fillRtAudioDeviceInfo(DeviceInfoList &devinfolist)
{
DEBUGTRACE_ENTER;
vector<RtAudio::Api> apis;
RtAudio::getCompiledApi(apis);
for (auto api : apis) {
for (auto api : apis)
{
RtAudio rtaudio(api);
us count = rtaudio.getDeviceCount();
for (us devno = 0; devno < count; devno++) {
const us count = rtaudio.getDeviceCount();
const auto ids = rtaudio.getDeviceIds();
for (us i = 0; i < count; i++)
{
us id = ids.at(i);
RtAudio::DeviceInfo devinfo = rtaudio.getDeviceInfo(id);
RtAudio::DeviceInfo devinfo = rtaudio.getDeviceInfo(devno);
if (!devinfo.probed) {
// Device capabilities not successfully probed. Continue to next
continue;
}
// "Our device info struct"
RtAudioDeviceInfo d;
switch (api) {
switch (api)
{
case RtAudio::LINUX_ALSA:
d.api = rtaudioAlsaApi;
break;
@ -70,42 +79,49 @@ void fillRtAudioDeviceInfo(DeviceInfoList &devinfolist) {
}
d.device_name = devinfo.name;
d._api_devindex = devno;
d.ID = id;
/// When 48k is available we overwrite the default sample rate with the 48
/// kHz value, which is our preffered rate,
bool rate_48k_found = false;
for (us j = 0; j < devinfo.sampleRates.size(); j++) {
for (us j = 0; j < devinfo.sampleRates.size(); j++)
{
us rate_int = devinfo.sampleRates[j];
d.availableSampleRates.push_back((double)rate_int);
if (!rate_48k_found) {
if (!rate_48k_found)
{
if (devinfo.preferredSampleRate == rate_int) {
if (devinfo.preferredSampleRate == rate_int)
{
d.prefSampleRateIndex = j;
}
if (rate_int == 48000) {
if (rate_int == 48000)
{
d.prefSampleRateIndex = j;
rate_48k_found = true;
}
}
}
d.noutchannels = devinfo.outputChannels;
d.ninchannels = devinfo.inputChannels;
d.noutchannels = std::min(devinfo.outputChannels, RTAUDIO_MAX_CHANNELS);
d.ninchannels = std::min(devinfo.inputChannels, RTAUDIO_MAX_CHANNELS);
d.availableInputRanges = {1.0};
d.availableOutputRanges = {1.0};
RtAudioFormat formats = devinfo.nativeFormats;
if (formats & RTAUDIO_SINT8) {
if (formats & RTAUDIO_SINT8)
{
d.availableDataTypes.push_back(
DataTypeDescriptor::DataType::dtype_int8);
}
if (formats & RTAUDIO_SINT16) {
if (formats & RTAUDIO_SINT16)
{
d.availableDataTypes.push_back(
DataTypeDescriptor::DataType::dtype_int16);
}
@ -113,15 +129,18 @@ void fillRtAudioDeviceInfo(DeviceInfoList &devinfolist) {
/* d.availableDataTypes.push_back(DataTypeDescriptor::DataType::dtype_int24);
*/
/* } */
if (formats & RTAUDIO_SINT32) {
if (formats & RTAUDIO_SINT32)
{
d.availableDataTypes.push_back(
DataTypeDescriptor::DataType::dtype_fl32);
}
if (formats & RTAUDIO_FLOAT64) {
if (formats & RTAUDIO_FLOAT64)
{
d.availableDataTypes.push_back(
DataTypeDescriptor::DataType::dtype_fl64);
}
if (d.availableDataTypes.size() == 0) {
if (d.availableDataTypes.size() == 0)
{
std::cerr << "RtAudio: No data types found in device!" << endl;
}
@ -139,9 +158,8 @@ static int mycallback(void *outputBuffer, void *inputBuffer,
unsigned int nFrames, double streamTime,
RtAudioStreamStatus status, void *userData);
static void myerrorcallback(RtAudioError::Type, const string &errorText);
class RtAudioDaq : public Daq {
class RtAudioDaq : public Daq
{
RtAudio rtaudio;
const us nFramesPerBlock;
@ -181,10 +199,11 @@ public:
/// 0. For now, our fix is to shift out the channels we want, and let
/// RtAudio pass on all channels.
inParams->firstChannel = 0;
inParams->nChannels = devinfo_gen.ninchannels;
inParams->deviceId = devinfo._api_devindex;
} else {
inParams->nChannels = devinfo.ninchannels;
inParams->deviceId = devinfo.ID;
}
else
{
outParams = std::make_unique<RtAudio::StreamParameters>();
@ -192,8 +211,8 @@ public:
/// 0. For now, our fix is to shift out the channels we want, and let
/// RtAudio pass on all channels.
outParams->firstChannel = 0;
outParams->nChannels = devinfo_gen.noutchannels;
outParams->deviceId = devinfo._api_devindex;
outParams->nChannels = devinfo.noutchannels;
outParams->deviceId = devinfo.ID;
}
RtAudio::StreamOptions streamoptions;
@ -206,7 +225,8 @@ public:
RtAudioFormat format;
using Dtype = DataTypeDescriptor::DataType;
const Dtype dtype = dataType();
switch (dtype) {
switch (dtype)
{
case Dtype::dtype_fl32:
DEBUGTRACE_PRINT("Datatype float32");
format = RTAUDIO_FLOAT32;
@ -237,36 +257,46 @@ public:
unsigned int nFramesPerBlock_copy = nFramesPerBlock;
// Final step: open the stream.
rtaudio.openStream(outParams.get(), inParams.get(), format,
static_cast<us>(samplerate()), &nFramesPerBlock_copy,
mycallback, (void *)this, &streamoptions,
&myerrorcallback);
RtAudioErrorType err = rtaudio.openStream(outParams.get(), inParams.get(), format,
static_cast<us>(samplerate()), &nFramesPerBlock_copy,
mycallback, (void *)this, &streamoptions);
if (err != RTAUDIO_NO_ERROR)
{
throw std::runtime_error(string("Error opening stream: ") + rtaudio.getErrorText());
}
if (nFramesPerBlock_copy != nFramesPerBlock) {
if (nFramesPerBlock_copy != nFramesPerBlock)
{
throw rte(string("Got different number of frames per block back from RtAudio "
"backend: ") + std::to_string(nFramesPerBlock_copy) + ". I do not know what to do.");
"backend: ") +
std::to_string(nFramesPerBlock_copy) + ". I do not know what to do.");
}
}
virtual void start(InDaqCallback inCallback,
OutDaqCallback outCallback) override final {
OutDaqCallback outCallback) override final
{
DEBUGTRACE_ENTER;
assert(!monitorOutput);
if (getStreamStatus().runningOK()) {
if (getStreamStatus().runningOK())
{
throw rte("Stream already running");
}
// Logical XOR
if (inCallback && outCallback) {
if (inCallback && outCallback)
{
throw rte("Either input or output stream possible for RtAudio. "
"Stream duplex mode not provided.");
}
if (neninchannels() > 0) {
if (!inCallback) {
if (neninchannels() > 0)
{
if (!inCallback)
{
throw rte(
"Input callback given, but stream does not provide input data");
@ -274,8 +304,10 @@ public:
_incallback = inCallback;
}
if (nenoutchannels() > 0) {
if (!outCallback) {
if (nenoutchannels() > 0)
{
if (!outCallback)
{
throw rte(
"Output callback given, but stream does not provide output data");
}
@ -283,7 +315,11 @@ public:
}
// Start the stream. Throws on error.
rtaudio.startStream();
const auto err = rtaudio.startStream();
if (err != RTAUDIO_NO_ERROR)
{
throw std::runtime_error(string("Error starting stream: ") + rtaudio.getErrorText());
}
// If we are here, we are running without errors.
StreamStatus status;
@ -293,10 +329,15 @@ public:
StreamStatus getStreamStatus() const override final { return _streamStatus; }
void stop() override final {
void stop() override final
{
DEBUGTRACE_ENTER;
if (getStreamStatus().runningOK()) {
rtaudio.stopStream();
if (getStreamStatus().runningOK())
{
const auto err = rtaudio.stopStream();
if(err != RTAUDIO_NO_ERROR) {
std::cerr << "Error occured while stopping the stream: " << rtaudio.getErrorText() << endl;
}
}
StreamStatus s = _streamStatus;
s.isRunning = false;
@ -305,14 +346,16 @@ public:
}
int streamCallback(void *outputBuffer, void *inputBuffer,
unsigned int nFrames, RtAudioStreamStatus status) {
unsigned int nFrames, RtAudioStreamStatus status)
{
DEBUGTRACE_ENTER;
using se = StreamStatus::StreamError;
int rval = 0;
auto stopWithError = [&](se e) {
auto stopWithError = [&](se e)
{
DEBUGTRACE_PRINT("stopWithError");
StreamStatus stat = _streamStatus;
stat.errorType = e;
@ -321,7 +364,8 @@ public:
rval = 1;
};
switch (status) {
switch (status)
{
case RTAUDIO_INPUT_OVERFLOW:
stopWithError(se::inputXRun);
return 1;
@ -340,14 +384,16 @@ public:
const us neninchannels = this->neninchannels();
const us nenoutchannels = this->nenoutchannels();
const us sw = dtype_descr.sw;
if (nFrames != nFramesPerBlock) {
if (nFrames != nFramesPerBlock)
{
cerr << "RtAudio backend error: nFrames does not match block size!"
<< endl;
stopWithError(se::logicError);
return 1;
}
if (inputBuffer) {
if (inputBuffer)
{
assert(_incallback);
std::vector<byte_t *> ptrs;
ptrs.reserve(neninchannels);
@ -358,8 +404,10 @@ public:
assert(ch_max < ninchannels);
/// Only pass on the pointers of the channels we want
for (us ch = ch_min; ch <= ch_max; ch++) {
if (inchannel_config.at(ch).enabled) {
for (us ch = ch_min; ch <= ch_max; ch++)
{
if (inchannel_config.at(ch).enabled)
{
byte_t *ptr =
static_cast<byte_t *>(inputBuffer) + sw * ch * nFramesPerBlock;
ptrs.push_back(ptr);
@ -368,10 +416,11 @@ public:
DaqData d{nFramesPerBlock, neninchannels, dtype};
d.copyInFromRaw(ptrs);
_incallback(d);
_incallback(d);
}
if (outputBuffer) {
if (outputBuffer)
{
assert(_outcallback);
std::vector<byte_t *> ptrs;
ptrs.reserve(nenoutchannels);
@ -383,8 +432,10 @@ public:
assert(ch_min < noutchannels);
assert(ch_max < noutchannels);
/// Only pass on the pointers of the channels we want
for (us ch = ch_min; ch <= ch_max; ch++) {
if (outchannel_config.at(ch).enabled) {
for (us ch = ch_min; ch <= ch_max; ch++)
{
if (outchannel_config.at(ch).enabled)
{
ptrs.push_back(static_cast<byte_t *>(outputBuffer) +
sw * ch * nFramesPerBlock);
}
@ -394,7 +445,8 @@ public:
_outcallback(d);
// Copy over the buffer
us j = 0;
for (auto ptr : ptrs) {
for (auto ptr : ptrs)
{
d.copyToRaw(j, ptr);
j++;
}
@ -410,17 +462,16 @@ public:
};
std::unique_ptr<Daq> createRtAudioDevice(const DeviceInfo &devinfo,
const DaqConfiguration &config) {
const DaqConfiguration &config)
{
return std::make_unique<RtAudioDaq>(devinfo, config);
}
void myerrorcallback(RtAudioError::Type, const string &errorText) {
cerr << "RtAudio backend stream error: " << errorText << endl;
}
int mycallback(
void *outputBuffer, void *inputBuffer, unsigned int nFrames,
__attribute__((unused)) double streamTime, // Not used parameter streamTime
RtAudioStreamStatus status, void *userData) {
RtAudioStreamStatus status, void *userData)
{
return static_cast<RtAudioDaq *>(userData)->streamCallback(
outputBuffer, inputBuffer, nFrames, status);

View File

@ -1,15 +1,21 @@
/* #define DEBUGTRACE_ENABLED */
// #define DEBUGTRACE_ENABLED
#include "lasp_streammgr.h"
#include "debugtrace.hpp"
#include "lasp_biquadbank.h"
#include "lasp_indatahandler.h"
#include "lasp_thread.h"
#include <algorithm>
#include <assert.h>
#include <algorithm>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
#include "debugtrace.hpp"
#include "lasp_biquadbank.h"
#include "lasp_indatahandler.h"
#include "lasp_thread.h"
using namespace std::literals::chrono_literals;
using std::cerr;
using std::endl;
@ -24,7 +30,7 @@ using rte = std::runtime_error;
std::weak_ptr<StreamMgr> _mgr;
std::mutex _mgr_mutex;
using Lck = std::scoped_lock<std::mutex>;
using Lck = std::scoped_lock<std::recursive_mutex>;
/**
* @brief The only way to obtain a stream manager, can only be called from the
@ -35,11 +41,11 @@ using Lck = std::scoped_lock<std::mutex>;
SmgrHandle StreamMgr::getInstance() {
DEBUGTRACE_ENTER;
std::scoped_lock<std::mutex> lck(_mgr_mutex);
auto mgr = _mgr.lock();
if (!mgr) {
// Double Check Locking Pattern, if two threads would simultaneously
// instantiate the singleton instance.
Lck lck(_mgr_mutex);
auto mgr = _mgr.lock();
if (mgr) {
@ -81,44 +87,75 @@ void StreamMgr::rescanDAQDevices(bool background,
std::function<void()> callback) {
DEBUGTRACE_ENTER;
DEBUGTRACE_PRINT(background);
if (_scanningDevices) {
throw rte("A background device scan is already busy");
}
Lck lck(_mtx);
checkRightThread();
if (_inputStream || _outputStream) {
throw rte("Rescanning DAQ devices only possible when no stream is running");
}
if (!_devices_mtx.try_lock()) {
throw rte("A background DAQ device scan is probably already running");
}
_devices_mtx.unlock();
std::scoped_lock lck(_devices_mtx);
_devices.clear();
if (!background) {
_scanningDevices = true;
rescanDAQDevices_impl(callback);
} else {
DEBUGTRACE_PRINT("Rescanning DAQ devices on different thread...");
_scanningDevices = true;
_pool.push_task(&StreamMgr::rescanDAQDevices_impl, this, callback);
}
}
#if LASP_HAS_PORTAUDIO && LASP_HAS_PA_ALSA
#include <alsa/asoundlib.h>
void empty_handler(const char *file, int line, const char *function, int err,
const char *fmt, ...) {}
// Temporarily set the ALSA eror handler to something that does nothing, to
// prevent ALSA from spitting out all kinds of misconfiguration errors.
class MuteErrHandler {
private:
snd_lib_error_handler_t _default_handler;
public:
explicit MuteErrHandler() {
_default_handler = snd_lib_error;
snd_lib_error_set_handler(empty_handler);
}
~MuteErrHandler() { snd_lib_error_set_handler(_default_handler); }
};
#else
// Does nothin in case of no ALSA
class MuteErrHandler {};
#endif
void StreamMgr::rescanDAQDevices_impl(std::function<void()> callback) {
DEBUGTRACE_ENTER;
std::scoped_lock lck(_devices_mtx);
_devices = DeviceInfo::getDeviceInfo();
assert(!_inputStream && !_outputStream);
Lck lck(_mtx);
// Alsa spits out annoying messages that are not useful
{
MuteErrHandler guard;
_devices = DeviceInfo::getDeviceInfo();
}
if (callback) {
callback();
}
_scanningDevices = false;
}
void StreamMgr::inCallback(const DaqData &data) {
DEBUGTRACE_ENTER;
std::scoped_lock<std::mutex> lck(_inDataHandler_mtx);
Lck lck(_mtx);
assert(_inputFilters.size() == data.nchannels);
if (std::count_if(_inputFilters.cbegin(), _inputFilters.cend(),
[](const auto &a) { return bool(a); }) > 0) {
/// Found a filter in vector of input filters. So we have to apply the
/// filters to each channel.
@ -138,12 +175,13 @@ void StreamMgr::inCallback(const DaqData &data) {
}
}
DEBUGTRACE_PRINT("Calling incallback for handlers (filtered)...");
for (auto &handler : _inDataHandlers) {
handler->inCallback(input_filtered);
}
} else {
/// No input filters
DEBUGTRACE_PRINT("Calling incallback for handlers...");
for (auto &handler : _inDataHandlers) {
handler->inCallback(data);
}
@ -151,12 +189,10 @@ void StreamMgr::inCallback(const DaqData &data) {
}
void StreamMgr::setSiggen(std::shared_ptr<Siggen> siggen) {
DEBUGTRACE_ENTER;
checkRightThread();
std::scoped_lock<std::mutex> lck(_siggen_mtx);
Lck lck(_mtx);
// If not set to nullptr, and a stream is running, we update the signal
// generator by resetting it.
if (isStreamRunningOK(StreamType::output) && siggen) {
@ -179,7 +215,8 @@ void StreamMgr::setSiggen(std::shared_ptr<Siggen> siggen) {
*
* @return
*/
template <typename T> bool fillData(DaqData &data, const vd &signal) {
template <typename T>
bool fillData(DaqData &data, const vd &signal) {
/* DEBUGTRACE_ENTER; */
assert(data.nframes == signal.size());
@ -212,29 +249,28 @@ template <typename T> bool fillData(DaqData &data, const vd &signal) {
return true;
}
void StreamMgr::outCallback(DaqData &data) {
DEBUGTRACE_ENTER;
/* DEBUGTRACE_ENTER; */
std::scoped_lock<std::mutex> lck(_siggen_mtx);
Lck lck(_mtx);
if (_siggen) {
vd signal = _siggen->genSignal(data.nframes);
switch (data.dtype) {
case (DataTypeDescriptor::DataType::dtype_fl32):
fillData<float>(data, signal);
break;
case (DataTypeDescriptor::DataType::dtype_fl64):
fillData<double>(data, signal);
break;
case (DataTypeDescriptor::DataType::dtype_int8):
fillData<int8_t>(data, signal);
break;
case (DataTypeDescriptor::DataType::dtype_int16):
fillData<int16_t>(data, signal);
break;
case (DataTypeDescriptor::DataType::dtype_int32):
fillData<int32_t>(data, signal);
break;
case (DataTypeDescriptor::DataType::dtype_fl32):
fillData<float>(data, signal);
break;
case (DataTypeDescriptor::DataType::dtype_fl64):
fillData<double>(data, signal);
break;
case (DataTypeDescriptor::DataType::dtype_int8):
fillData<int8_t>(data, signal);
break;
case (DataTypeDescriptor::DataType::dtype_int16):
fillData<int16_t>(data, signal);
break;
case (DataTypeDescriptor::DataType::dtype_int32):
fillData<int32_t>(data, signal);
break;
}
} else {
// Set all values to 0.
@ -244,7 +280,17 @@ void StreamMgr::outCallback(DaqData &data) {
StreamMgr::~StreamMgr() {
DEBUGTRACE_ENTER;
checkRightThread();
while (_scanningDevices) {
std::this_thread::sleep_for(10us);
}
#if LASP_DEBUG == 1
{ // Careful, this lock needs to be released to make sure the streams can
// obtain a lock to the stream manager.
Lck lck(_mtx);
checkRightThread();
}
#endif
// Stream manager now handled by shared pointer. Each indata handler gets a
// shared pointer to the stream manager, and stores a weak pointer to it.
// Hence, we do not have to do any cleanup here. It also makes sure that the
@ -257,31 +303,35 @@ StreamMgr::~StreamMgr() {
// virtual methods. This was really a bug.
_inputStream.reset();
_outputStream.reset();
}
void StreamMgr::stopAllStreams() {
DEBUGTRACE_ENTER;
checkRightThread();
{
Lck lck(_mtx);
checkRightThread();
}
// No lock here!
_inputStream.reset();
_outputStream.reset();
}
void StreamMgr::startStream(const DaqConfiguration &config) {
DEBUGTRACE_ENTER;
if (_scanningDevices) {
throw rte("DAQ device scan is busy. Cannot start stream.");
}
Lck lck(_mtx);
checkRightThread();
bool isInput = std::count_if(config.inchannel_config.cbegin(),
config.inchannel_config.cend(),
[](auto &i) { return i.enabled; });
[](auto &i) { return i.enabled; }) > 0;
bool isOutput = std::count_if(config.outchannel_config.cbegin(),
config.outchannel_config.cend(),
[](auto &i) { return i.enabled; });
[](auto &i) { return i.enabled; }) > 0;
// Find the first device that matches with the configuration
std::scoped_lock lck(_devices_mtx);
DeviceInfo *devinfo = nullptr;
// Match configuration to a device in the list of devices
@ -302,34 +352,40 @@ void StreamMgr::startStream(const DaqConfiguration &config) {
bool isDuplex = isInput && isOutput;
if (!isInput && !isOutput) {
throw rte("Neither input, nor output channels enabled for "
"stream. Cannot start.");
throw rte(
"Attempted stream start failed, stream does not have any enabled "
"channels. Please first enable channels in the channel configuration.");
}
if (isInput && _inputStream) {
throw rte("Error: an input stream is already running. Please "
"first stop existing stream");
throw rte(
"Error: an input stream is already running. Please "
"first stop existing stream");
} else if (isOutput && _outputStream) {
throw rte("Error: output stream is already running. Please "
"first stop existing stream");
throw rte(
"Error: output stream is already running. Please "
"first stop existing stream");
} else if (_inputStream) {
if (_inputStream->duplexMode() && isOutput) {
throw rte("Error: output stream is already running (in duplex mode). "
"Please "
"first stop existing stream");
throw rte(
"Error: output stream is already running (in duplex mode). "
"Please "
"first stop existing stream");
}
}
if (_outputStream && isInput && _outputStream->duplexModeForced &&
config.match(*_outputStream)) {
throw rte("This device is already opened for output. If input is also "
"required, please enable duplex mode for this device");
throw rte(
"This device is already opened for output. If input is also "
"required, please enable duplex mode for this device");
}
if (_inputStream && isOutput && _inputStream->duplexModeForced &&
config.match(*_inputStream)) {
throw rte("This device is already opened for input. If output is also "
"required, please enable duplex mode for this device");
throw rte(
"This device is already opened for input. If output is also "
"required, please enable duplex mode for this device");
}
InDaqCallback inCallback;
@ -352,13 +408,13 @@ void StreamMgr::startStream(const DaqConfiguration &config) {
d fs = daq->samplerate();
/// Create input filters
_inputFilters.clear();
/// No input filter for monitor channel, which comes as the first input channel
/// In the list
/// No input filter for monitor channel, which comes as the first input
/// channel In the list
if (config.monitorOutput && devinfo->hasInternalOutputMonitor) {
_inputFilters.push_back(nullptr);
}
for (auto &ch : daq->inchannel_config) {
if (ch.enabled) {
if (ch.digitalHighPassCutOn < 0) {
@ -371,7 +427,7 @@ void StreamMgr::startStream(const DaqConfiguration &config) {
SeriesBiquad::firstOrderHighPass(fs, ch.digitalHighPassCutOn)));
}
}
} // End of input filter creation
} // End of input filter creation
}
if (isOutput) {
@ -398,46 +454,58 @@ void StreamMgr::startStream(const DaqConfiguration &config) {
}
}
void StreamMgr::stopStream(const StreamType t) {
DEBUGTRACE_ENTER;
checkRightThread();
bool resetHandlers = false;
std::unique_ptr<Daq> *streamToStop = nullptr;
if (t == StreamType::input) {
if (!_inputStream) {
throw rte("Input stream is not running");
{ // Mutex locked in this scope
Lck lck(_mtx);
if (t == StreamType::input) {
if (!_inputStream) {
throw rte("Input stream is not running");
}
streamToStop = std::addressof(_inputStream);
resetHandlers = true;
} else {
/// t == output
/// Kill input stream in case that one is a duplex stream
if (_inputStream && _inputStream->duplexMode()) {
streamToStop = std::addressof(_inputStream);
} else {
if (!_outputStream) {
throw rte("Output stream is not running");
}
streamToStop = std::addressof(_outputStream);
} // end else
}
/// Kills input stream
_inputStream.reset();
/// Send reset to all in data handlers
} // End of mutex lock. When stopping stream, mutex should be unlocked.
// If we arrive here, we should have a stream to stop.
assert(streamToStop != nullptr);
streamToStop->reset();
/// Send reset to all in data handlers
if (resetHandlers) {
Lck lck(_mtx);
for (auto &handler : _inDataHandlers) {
handler->reset(nullptr);
}
} else {
/// t == output
/// Kill input stream in case that one is a duplex stream
if (_inputStream && _inputStream->duplexMode()) {
_inputStream.reset();
} else {
if (!_outputStream) {
throw rte("Output stream is not running");
}
_outputStream.reset();
} // end else
}
}
void StreamMgr::addInDataHandler(InDataHandler *handler) {
DEBUGTRACE_ENTER;
Lck lck(_mtx);
checkRightThread();
assert(handler);
std::scoped_lock<std::mutex> lck(_inDataHandler_mtx);
handler->reset(_inputStream.get());
if (std::find(_inDataHandlers.cbegin(), _inDataHandlers.cend(), handler) !=
_inDataHandlers.cend()) {
throw std::runtime_error("Error: handler already added. Probably start() "
"is called more than once on a handler object");
throw std::runtime_error(
"Error: handler already added. Probably start() "
"is called more than once on a handler object");
}
_inDataHandlers.push_back(handler);
DEBUGTRACE_PRINT(_inDataHandlers.size());
@ -445,16 +513,17 @@ void StreamMgr::addInDataHandler(InDataHandler *handler) {
void StreamMgr::removeInDataHandler(InDataHandler &handler) {
DEBUGTRACE_ENTER;
checkRightThread();
std::scoped_lock<std::mutex> lck(_inDataHandler_mtx);
Lck lck(_mtx);
// checkRightThread();
_inDataHandlers.remove(&handler);
DEBUGTRACE_PRINT(_inDataHandlers.size());
}
Daq::StreamStatus StreamMgr::getStreamStatus(const StreamType type) const {
/* DEBUGTRACE_ENTER; */
DEBUGTRACE_ENTER;
Lck lck(_mtx);
checkRightThread();
// Default constructor, says stream is not running, but also no errors
@ -467,7 +536,7 @@ Daq::StreamStatus StreamMgr::getStreamStatus(const StreamType type) const {
}
const Daq *StreamMgr::getDaq(StreamType type) const {
Lck lck(_mtx);
checkRightThread();
if (type == StreamType::input) {

View File

@ -1,19 +1,19 @@
#pragma once
#include "lasp_daq.h"
#include "lasp_siggen.h"
#include "lasp_thread.h"
#include <list>
#include <memory>
#include <mutex>
#include <thread>
#include "lasp_daq.h"
#include "lasp_siggen.h"
#include "lasp_thread.h"
/** \addtogroup device
* @{
*/
class StreamMgr;
class InDataHandler;
class SeriesBiquad;
/**
@ -25,12 +25,15 @@ class SeriesBiquad;
* fact is asserted.
*/
class StreamMgr {
mutable std::recursive_mutex _mtx;
/**
* @brief Storage for streams.
*/
std::unique_ptr<Daq> _inputStream, _outputStream;
std::atomic<bool> _scanningDevices{false};
GlobalThreadPool _pool;
/**
@ -39,22 +42,18 @@ class StreamMgr {
* thread-safety.
*/
std::list<InDataHandler *> _inDataHandlers;
mutable std::mutex _inDataHandler_mtx;
/**
* @brief Signal generator in use to generate output data. Currently
* implemented as to generate the same data for all output channels.
*/
std::shared_ptr<Siggen> _siggen;
std::mutex _siggen_mtx;
/**
* @brief Filters on input stream. For example, a digital high pass filter.
*/
std::vector<std::unique_ptr<SeriesBiquad>> _inputFilters;
mutable std::recursive_mutex _devices_mtx;
/**
* @brief Current storage for the device list
*/
@ -67,9 +66,7 @@ class StreamMgr {
friend class InDataHandler;
friend class Siggen;
public:
public:
~StreamMgr();
enum class StreamType : us {
@ -100,9 +97,10 @@ class StreamMgr {
* @return A copy of the internal stored list of devices
*/
DeviceInfoList getDeviceInfo() const {
std::scoped_lock lck(_devices_mtx);
std::scoped_lock lck(_mtx);
DeviceInfoList d2;
for(const auto& dev: _devices) {
for (const auto &dev : _devices) {
assert(dev != nullptr);
d2.push_back(dev->clone());
}
return d2;
@ -118,9 +116,9 @@ class StreamMgr {
* set to true, the function returns immediately.
* @param callback Function to call when complete.
*/
void
rescanDAQDevices(bool background = false,
std::function<void()> callback = std::function<void()>());
void rescanDAQDevices(
bool background = false,
std::function<void()> callback = std::function<void()>());
/**
* @brief Start a stream based on given configuration.
@ -141,12 +139,12 @@ class StreamMgr {
}
bool isStreamRunning(const StreamType type) const {
switch (type) {
case (StreamType::input):
return bool(_inputStream);
break;
case (StreamType::output):
return bool(_outputStream);
break;
case (StreamType::input):
return bool(_inputStream);
break;
case (StreamType::output):
return bool(_outputStream);
break;
}
return false;
}
@ -193,11 +191,10 @@ class StreamMgr {
*/
void setSiggen(std::shared_ptr<Siggen> s);
private:
private:
void inCallback(const DaqData &data);
void outCallback(DaqData &data);
/**
* @brief Add an input data handler. The handler's inCallback() function is
* called with data when available. This function should *NOT* be called by

View File

@ -68,6 +68,7 @@ void fillUlDaqDeviceInfo(DeviceInfoList &devinfolist) {
}
devinfo.physicalOutputQty = DaqChannel::Qty::Voltage;
devinfo.physicalInputQty = DaqChannel::Qty::Voltage;
devinfo.availableDataTypes.push_back(
DataTypeDescriptor::DataType::dtype_fl64);
@ -79,7 +80,9 @@ void fillUlDaqDeviceInfo(DeviceInfoList &devinfolist) {
devinfo.availableFramesPerBlock = {512, 1024, 2048, 4096, 8192};
devinfo.availableInputRanges = {1.0, 10.0};
devinfo.availableOutputRanges = {10.0};
devinfo.prefInputRangeIndex = 0;
devinfo.prefOutputRangeIndex = 0;
devinfo.ninchannels = 4;
devinfo.noutchannels = 1;
@ -90,6 +93,7 @@ void fillUlDaqDeviceInfo(DeviceInfoList &devinfolist) {
devinfo.hasInternalOutputMonitor = true;
devinfo.hasDuplexMode = true;
devinfo.duplexModeForced = true;
// Finally, this devinfo is pushed back in list

View File

@ -1,14 +1,15 @@
/* #define DEBUGTRACE_ENABLED */
// #define DEBUGTRACE_ENABLED
#include "debugtrace.hpp"
#include "lasp_config.h"
#if LASP_HAS_PORTAUDIO == 1
#include "lasp_portaudiodaq.h"
#include "portaudio.h"
#include <gsl-lite/gsl-lite.hpp>
#include <mutex>
#include <string>
#include "lasp_portaudiodaq.h"
#include "portaudio.h"
using rte = std::runtime_error;
using std::cerr;
using std::endl;
@ -26,7 +27,7 @@ inline void throwIfError(PaError e) {
* @brief Device info, plus PortAudio stuff
*/
class OurPaDeviceInfo : public DeviceInfo {
public:
public:
/**
* @brief Store instance to PaDeviceInfo.
*/
@ -35,17 +36,21 @@ public:
virtual std::unique_ptr<DeviceInfo> clone() const override final {
return std::make_unique<OurPaDeviceInfo>(*this);
}
OurPaDeviceInfo &operator=(const OurPaDeviceInfo &) = delete;
OurPaDeviceInfo(const OurPaDeviceInfo &) = default;
OurPaDeviceInfo(const PaDeviceInfo &o) : DeviceInfo(), _paDevInfo(o) {}
};
void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist) {
DEBUGTRACE_ENTER;
bool shouldPaTerminate = false;
try {
PaError err = Pa_Initialize();
/// PortAudio says that Pa_Terminate() should not be called whenever there
/// is an error in Pa_Initialize(). This is opposite to what most examples
/// of PortAudio show.
throwIfError(err);
shouldPaTerminate = true;
auto fin = gsl::finally([&err] {
DEBUGTRACE_PRINT("Terminating PortAudio instance");
@ -55,6 +60,10 @@ void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist) {
}
});
const PaHostApiIndex apicount = Pa_GetHostApiCount();
if (apicount < 0) {
return;
}
/* const PaDeviceInfo *deviceInfo; */
const int numDevices = Pa_GetDeviceCount();
if (numDevices < 0) {
@ -62,16 +71,51 @@ void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist) {
}
for (us i = 0; i < (us)numDevices; i++) {
/* DEBUGTRACE_PRINT(i); */
bool hasDuplexMode = false;
const PaDeviceInfo *deviceInfo = Pa_GetDeviceInfo(i);
if (!deviceInfo) {
throw rte("No device info struct returned");
}
OurPaDeviceInfo d;
d._paDevInfo = *deviceInfo;
d.api = portaudioApi;
OurPaDeviceInfo d(*deviceInfo);
// We store the name in d.device_name
d._paDevInfo.name = nullptr;
d.device_name = deviceInfo->name;
const PaHostApiInfo *hostapiinfo = Pa_GetHostApiInfo(deviceInfo->hostApi);
if (hostapiinfo == nullptr) {
throw std::runtime_error("Hostapi nullptr!");
}
switch (hostapiinfo->type) {
case paALSA:
// Duplex mode for alsa
hasDuplexMode = true;
d.api = portaudioALSAApi;
break;
case paASIO:
hasDuplexMode = true;
d.api = portaudioASIOApi;
break;
case paDirectSound:
d.api = portaudioDirectSoundApi;
break;
case paMME:
d.api = portaudioWMMEApi;
break;
case paWDMKS:
d.api = portaudioWDMKS;
break;
case paWASAPI:
d.api = portaudioWASAPIApi;
break;
case paPulseAudio:
d.api = portaudioPulseApi;
break;
default:
throw rte("Unimplemented portaudio API!");
break;
}
d.availableDataTypes = {DataTypeDescriptor::DataType::dtype_int16,
DataTypeDescriptor::DataType::dtype_int32,
DataTypeDescriptor::DataType::dtype_fl32};
@ -87,15 +131,27 @@ void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist) {
d.prefFramesPerBlockIndex = 2;
d.availableInputRanges = {1.0};
// d.prefInputRangeIndex = 0; // Constructor-defined
d.availableOutputRanges = {1.0};
// d.prefOutputRangeIndex = 0; // Constructor-defined
d.ninchannels = deviceInfo->maxInputChannels;
d.noutchannels = deviceInfo->maxOutputChannels;
// Duplex mode, only for ALSA devices
d.hasDuplexMode = hasDuplexMode;
devinfolist.push_back(std::make_unique<OurPaDeviceInfo>(d));
}
}
catch (rte &e) {
if (shouldPaTerminate) {
PaError err = Pa_Terminate();
if (err != paNoError) {
cerr << "Error terminating PortAudio. Do not know what to do." << endl;
}
}
cerr << "PortAudio backend error: " << e.what() << std::endl;
return;
}
@ -121,14 +177,13 @@ static int rawPaCallback(const void *inputBuffer, void *outputBuffer,
PaStreamCallbackFlags statusFlags, void *userData);
class PortAudioDaq : public Daq {
bool _shouldPaTerminate = false;
PaStream *_stream = nullptr;
std::atomic<StreamStatus::StreamError> _streamError =
StreamStatus::StreamError::noError;
InDaqCallback _incallback;
OutDaqCallback _outcallback;
public:
public:
PortAudioDaq(const OurPaDeviceInfo &devinfo_gen,
const DaqConfiguration &config);
@ -158,7 +213,7 @@ public:
std::unique_ptr<Daq> createPortAudioDevice(const DeviceInfo &devinfo,
const DaqConfiguration &config) {
DEBUGTRACE_ENTER;
const OurPaDeviceInfo *_info =
dynamic_cast<const OurPaDeviceInfo *>(&devinfo);
if (_info == nullptr) {
@ -178,119 +233,126 @@ static int rawPaCallback(const void *inputBuffer, void *outputBuffer,
PortAudioDaq::PortAudioDaq(const OurPaDeviceInfo &devinfo_gen,
const DaqConfiguration &config)
: Daq(devinfo_gen, config) {
DEBUGTRACE_ENTER;
PaError err = Pa_Initialize();
/// PortAudio says that Pa_Terminate() should not be called whenever there
/// is an error in Pa_Initialize(). This is opposite to what most examples
/// of PortAudio show.
throwIfError(err);
bool shouldPaTerminate = false;
try {
PaError err = Pa_Initialize();
/// PortAudio says that Pa_Terminate() should not be called whenever there
/// is an error in Pa_Initialize(). This is opposite to what most examples
/// of PortAudio show.
throwIfError(err);
// OK, Pa_Initialize successfully finished, it means we have to clean up with
// Pa_Terminate in the destructor.
_shouldPaTerminate = true;
// OK, Pa_Initialize successfully finished, it means we have to clean up
// with Pa_Terminate in the destructor.
shouldPaTerminate = true;
// Going to find the device in the list. If its there, we have to retrieve
// the index, as this is required in the PaStreamParameters struct
int devindex = -1;
for (int i = 0; i < Pa_GetDeviceCount(); i++) {
bool ok = true;
const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
if (!info) {
throw rte("No device structure returned from PortAudio");
// Going to find the device in the list. If its there, we have to retrieve
// the index, as this is required in the PaStreamParameters struct
int devindex = -1;
for (int i = 0; i < Pa_GetDeviceCount(); i++) {
// DEBUGTRACE_PRINT(i);
bool ok = true;
const PaDeviceInfo *info = Pa_GetDeviceInfo(i);
if (!info) {
throw rte("No device structure returned from PortAudio");
}
ok &= string(info->name) == devinfo_gen.device_name;
ok &= info->hostApi == devinfo_gen._paDevInfo.hostApi;
ok &= info->maxInputChannels == devinfo_gen._paDevInfo.maxInputChannels;
ok &= info->maxOutputChannels == devinfo_gen._paDevInfo.maxOutputChannels;
ok &= info->defaultSampleRate == devinfo_gen._paDevInfo.defaultSampleRate;
if (ok) {
devindex = i;
}
}
ok &= string(info->name) == devinfo_gen._paDevInfo.name;
ok &= info->hostApi == devinfo_gen._paDevInfo.hostApi;
ok &= info->maxInputChannels == devinfo_gen._paDevInfo.maxInputChannels;
ok &= info->maxOutputChannels == devinfo_gen._paDevInfo.maxOutputChannels;
ok &= info->defaultSampleRate == devinfo_gen._paDevInfo.defaultSampleRate;
if (ok) {
devindex = i;
if (devindex < 0) {
throw rte(string("Device not found: ") + string(devinfo_gen.device_name));
}
}
if (devindex < 0) {
throw rte(string("Device not found: ") + string(devinfo_gen.device_name));
}
using Dtype = DataTypeDescriptor::DataType;
const Dtype dtype = dataType();
// Sample format is bit flag
PaSampleFormat format = paNonInterleaved;
switch (dtype) {
case Dtype::dtype_fl32:
DEBUGTRACE_PRINT("Datatype float32");
format |= paFloat32;
break;
case Dtype::dtype_fl64:
DEBUGTRACE_PRINT("Datatype float64");
throw rte("Invalid data type specified for DAQ stream.");
break;
case Dtype::dtype_int8:
DEBUGTRACE_PRINT("Datatype int8");
format |= paInt8;
break;
case Dtype::dtype_int16:
DEBUGTRACE_PRINT("Datatype int16");
format |= paInt16;
break;
case Dtype::dtype_int32:
DEBUGTRACE_PRINT("Datatype int32");
format |= paInt32;
break;
default:
throw rte("Invalid data type specified for DAQ stream.");
break;
using Dtype = DataTypeDescriptor::DataType;
const Dtype dtype = dataType();
// Sample format is bit flag
PaSampleFormat format = paNonInterleaved;
switch (dtype) {
case Dtype::dtype_fl32:
DEBUGTRACE_PRINT("Datatype float32");
format |= paFloat32;
break;
case Dtype::dtype_fl64:
DEBUGTRACE_PRINT("Datatype float64");
throw rte("Invalid data type specified for DAQ stream.");
break;
case Dtype::dtype_int8:
DEBUGTRACE_PRINT("Datatype int8");
format |= paInt8;
break;
case Dtype::dtype_int16:
DEBUGTRACE_PRINT("Datatype int16");
format |= paInt16;
break;
case Dtype::dtype_int32:
DEBUGTRACE_PRINT("Datatype int32");
format |= paInt32;
break;
default:
throw rte("Invalid data type specified for DAQ stream.");
break;
}
std::unique_ptr<PaStreamParameters> instreamParams;
std::unique_ptr<PaStreamParameters> outstreamParams;
if (neninchannels() > 0) {
instreamParams = std::make_unique<PaStreamParameters>(PaStreamParameters(
{.device = devindex,
.channelCount = (int)getHighestEnabledInChannel() + 1,
.sampleFormat = format,
.suggestedLatency = framesPerBlock() / samplerate(),
.hostApiSpecificStreamInfo = nullptr}));
}
if (nenoutchannels() > 0) {
outstreamParams = std::make_unique<PaStreamParameters>(PaStreamParameters(
{.device = devindex,
.channelCount = (int)getHighestEnabledOutChannel() + 1,
.sampleFormat = format,
.suggestedLatency = framesPerBlock() / samplerate(),
.hostApiSpecificStreamInfo = nullptr}));
}
// Next step: check whether we are OK
err = Pa_IsFormatSupported(instreamParams.get(), outstreamParams.get(),
samplerate());
throwIfError(err);
err = Pa_OpenStream(&_stream, // stream
instreamParams.get(), // inputParameters
outstreamParams.get(), // outputParameters
samplerate(), // yeah,
framesPerBlock(), // framesPerBuffer
paNoFlag, // streamFlags
rawPaCallback, this);
throwIfError(err);
assert(_stream);
} catch (rte &e) {
if (shouldPaTerminate) {
PaError err = Pa_Terminate();
if (err != paNoError) {
cerr << "Error terminating PortAudio. Do not know what to do." << endl;
}
}
throw;
}
std::unique_ptr<PaStreamParameters> instreamParams;
std::unique_ptr<PaStreamParameters> outstreamParams;
if (neninchannels() > 0) {
instreamParams = std::make_unique<PaStreamParameters>(
PaStreamParameters({.device = devindex,
.channelCount = (int)neninchannels(),
.sampleFormat = format,
.suggestedLatency = framesPerBlock() / samplerate(),
.hostApiSpecificStreamInfo = nullptr}));
}
if (nenoutchannels() > 0) {
outstreamParams = std::make_unique<PaStreamParameters>(
PaStreamParameters({.device = devindex,
.channelCount = (int)nenoutchannels(),
.sampleFormat = format,
.suggestedLatency = framesPerBlock() / samplerate(),
.hostApiSpecificStreamInfo = nullptr}));
}
// Next step: check whether we are OK
err = Pa_IsFormatSupported(instreamParams.get(), outstreamParams.get(),
samplerate());
throwIfError(err);
err = Pa_OpenStream(&_stream, // stream
instreamParams.get(), // inputParameters
outstreamParams.get(), // outputParameters
samplerate(), // yeah,
framesPerBlock(), // framesPerBuffer
paNoFlag, // streamFlags
rawPaCallback, this);
throwIfError(err);
}
void PortAudioDaq::start(InDaqCallback inCallback, OutDaqCallback outCallback) {
DEBUGTRACE_ENTER;
assert(_stream);
if (Pa_IsStreamActive(_stream)) {
throw rte("Stream is already running");
}
// Logical XOR
if (inCallback && outCallback) {
throw rte("Either input or output stream possible for RtAudio. "
"Stream duplex mode not provided.");
}
if (neninchannels() > 0) {
if (!inCallback) {
throw rte(
@ -314,52 +376,77 @@ void PortAudioDaq::start(InDaqCallback inCallback, OutDaqCallback outCallback) {
void PortAudioDaq::stop() {
DEBUGTRACE_ENTER;
assert(_stream);
if (Pa_IsStreamStopped(_stream)) {
if (Pa_IsStreamStopped(_stream) > 1) {
throw rte("Stream is already stopped");
}
PaError err = Pa_StopStream(_stream);
throwIfError(err);
}
Daq::StreamStatus PortAudioDaq::getStreamStatus() const {
DEBUGTRACE_ENTER;
// Stores an error type and whether the
Daq::StreamStatus status;
// Copy over atomic flag.
status.errorType = _streamError;
// Check if stream is still running.
if (_stream) {
if (Pa_IsStreamActive(_stream)) {
status.isRunning = true;
using StreamError = Daq::StreamStatus::StreamError;
Daq::StreamStatus::StreamError errortype = _streamError.load();
PaError err = Pa_IsStreamStopped(_stream);
if (err > 1) {
// Stream is stopped due to an error in the callback. The exact error type
// is filled in in the if-statement above
return status;
} else if (err == 0) {
// Still running
status.isRunning = true;
} else if (err < 0) {
// Stream encountered an error.
switch (err) {
case paInternalError:
errortype = StreamError::driverError;
break;
case paDeviceUnavailable:
errortype = StreamError::driverError;
break;
case paInputOverflowed:
errortype = StreamError::inputXRun;
break;
case paOutputUnderflowed:
errortype = StreamError::outputXRun;
break;
default:
errortype = StreamError::driverError;
cerr << "Portaudio backend error:" << Pa_GetErrorText(err) << endl;
break;
}
}
status.errorType = errortype;
return status;
}
PortAudioDaq::~PortAudioDaq() {
DEBUGTRACE_ENTER;
PaError err;
if (_stream) {
if (Pa_IsStreamActive(_stream)) {
stop();
}
err = Pa_CloseStream(_stream);
_stream = nullptr;
if (err != paNoError) {
cerr << "Error closing PortAudio stream. Do not know what to do." << endl;
}
assert(_shouldPaTerminate);
assert(_stream);
if (Pa_IsStreamActive(_stream)) {
// Stop the stream first
stop();
}
if (_shouldPaTerminate) {
err = Pa_Terminate();
if (err != paNoError) {
cerr << "Error terminating PortAudio. Do not know what to do." << endl;
}
err = Pa_CloseStream(_stream);
_stream = nullptr;
if (err != paNoError) {
cerr << "Error closing PortAudio stream. Do not know what to do." << endl;
}
err = Pa_Terminate();
if (err != paNoError) {
cerr << "Error terminating PortAudio. Do not know what to do." << endl;
}
}
int PortAudioDaq::memberPaCallback(const void *inputBuffer, void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags) {
DEBUGTRACE_ENTER;
typedef Daq::StreamStatus::StreamError se;
if (statusFlags & paPrimingOutput) {

View File

@ -16,6 +16,7 @@ set(lasp_dsp_files
lasp_threadedindatahandler.cpp
lasp_ppm.cpp
lasp_clip.cpp
lasp_freqsmooth.cpp
)

View File

@ -2,12 +2,9 @@
#include "lasp_avpowerspectra.h"
#include "debugtrace.hpp"
#include "lasp_mathtypes.h"
#include <cmath>
#include <optional>
#include <stdexcept>
using rte = std::runtime_error;
using std::cerr;
using std::endl;
PowerSpectra::PowerSpectra(const us nfft, const Window::WindowType w)
: PowerSpectra(Window::create(w, nfft)) {}

View File

@ -3,8 +3,6 @@
#include "lasp_mathtypes.h"
#include "lasp_timebuffer.h"
#include "lasp_window.h"
#include <memory>
#include <optional>
/** \defgroup dsp Digital Signal Processing utilities
* These are classes and functions used for processing raw signal data, to

View File

@ -2,7 +2,6 @@
#include "lasp_biquadbank.h"
#include "debugtrace.hpp"
#include "lasp_thread.h"
#include <cassert>
#include <vector>
using std::cerr;

View File

@ -0,0 +1,133 @@
// #define DEBUGTRACE_ENABLED
#include "lasp_freqsmooth.h"
#include <cassert>
#include "debugtrace.hpp"
using rte = std::runtime_error;
vd freqSmooth(const vd& freq, const vd& X, const unsigned w,
bool power_correct) {
DEBUGTRACE_ENTER;
if (freq.size() < 2) {
throw rte("Invalid frequency size. Should be > 2");
}
if (freq.size() != X.size()) {
throw rte("Sizes of freq and X do not match");
}
if (freq.size() > std::numeric_limits<long>::max() / 2) {
throw rte("Frequency size limit for smoothing is 2^30");
}
if (w == 0) {
throw rte("Invalid number of octaves");
}
const us Nfreq = freq.size();
// Smoothing width in unit of number of octaves
const d Delta = 1 / d(w);
// Minimum frequency and maximum frequency to smooth on (frequency range that
// is interpolated to a log scale)
d freq_min;
const d freq_max = freq(Nfreq - 1);
const bool firstFreqEqZero = (d_abs(freq(0)) < 1e-15);
// AC-signal power
d ac_pwr;
if (firstFreqEqZero) {
freq_min = freq(1);
if (power_correct) {
ac_pwr = arma::sum(X.subvec(1, Nfreq - 1));
}
} else {
freq_min = freq(0);
if (power_correct) {
ac_pwr = arma::sum(X);
}
}
DEBUGTRACE_PRINT(freq_min);
DEBUGTRACE_PRINT(freq_max);
const vd freq_log =
arma::logspace(d_log10(freq_min), d_log10(freq_max), 10 * Nfreq);
DEBUGTRACE_PRINT("freq_log = ");
const long Nfreq_sm = freq_log.size();
// Interpolate X to logscale
vd X_log;
DEBUGTRACE_PRINT("X_log = :");
arma::interp1(freq, X, freq_log, X_log, "*linear");
// First and last point are not interpolated well, could be minimally out of
// the interpolation range, due to roundoff errors. Armadillo sets these
// points to nan, so we have to manually "interpolate" them.
X_log(Nfreq_sm - 1) = X(X.size() - 1);
if (firstFreqEqZero) {
X_log(0) = X(1);
} else {
X_log(0) = X(0);
}
// Allocate space for smoothed X on log scale
vd Xsm_log(freq_log.size());
const d beta = d_log10(Nfreq_sm) / d_log10(2) / (Nfreq_sm - 1);
// int rounds down
const long mu = int(Delta / d(2) / beta);
DEBUGTRACE_PRINT(mu);
// Long is at least 32 bits. So +/- 2M points length
for (long k = 0; k < Nfreq_sm; k++) {
// const d fcur = freq_log(k);
long idx_start = std::max(k - mu, 0l);
long idx_stop = std::min(k + mu, Nfreq_sm - 1);
// Make window smaller at the sides (close to the end of the array)
if (idx_start == 0 || idx_stop == Nfreq_sm - 1) {
const long mu_edge = std::min(k - idx_start, idx_stop - k);
idx_start = k - mu_edge;
idx_stop = k + mu_edge;
}
assert(idx_stop < Nfreq_sm);
assert(idx_start < Nfreq_sm);
DEBUGTRACE_PRINT(idx_start)
DEBUGTRACE_PRINT(idx_stop);
Xsm_log(k) = arma::mean(X_log.subvec(idx_start, idx_stop));
}
DEBUGTRACE_PRINT("Xsm_log:");
// std::cerr << Xsm_log << std::endl;
// Back-interpolate to a linear scale, and be wary of nans at the start end
// and range. Also interpolates power
vd Xsm(Nfreq);
if (firstFreqEqZero) {
vd Xsm_gt0;
arma::interp1(freq_log, Xsm_log, freq.subvec(1, Nfreq - 1), Xsm_gt0,
"*linear");
Xsm(0) = X(0);
Xsm.subvec(1, Nfreq - 1) = Xsm_gt0;
Xsm(1) = Xsm_log(1);
Xsm(Nfreq - 1) = Xsm_log(Nfreq_sm - 1);
// Final step: power-correct smoothed spectrum
if (power_correct) {
d new_acpwr = arma::sum(Xsm.subvec(1, Nfreq - 1));
Xsm.subvec(1, Nfreq - 1) *= ac_pwr / new_acpwr;
}
} else {
arma::interp1(freq_log, Xsm_log, freq, Xsm, "*linear");
Xsm(0) = X(0);
Xsm(Nfreq - 1) = Xsm_log(Nfreq_sm - 1);
// Final step: power-correct smoothed spectrum
if (power_correct) {
d new_acpwr = arma::sum(Xsm);
Xsm *= ac_pwr / new_acpwr;
}
}
return Xsm;
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <memory>
#include <vector>
#include "lasp_mathtypes.h"
#include "lasp_types.h"
/**
* \addtogroup dsp
* @{
*/
/**
* @brief Apply frequency domain smoothing to a Frequency domain (single
* sided)signal power spectrum
*
* @param freq Frequency range
* @param X Signal pwr
* @param w Parameter determining the smoothing with. 1 = 1/1 octave, 3 = 1/3th
* octave and so on
* @param power_correct Apply a correction to the whole spectrum to make the
* signal power equal to the unsmoothed signal power.
* @return vd Smoothed spectrum
*/
vd freqSmooth(const vd& freq, const vd& X, const unsigned w,
bool power_correct = false);
/** @} */

View File

@ -37,7 +37,7 @@ void RtAps::inCallback(const DaqData &data) {
cerr << "**** Error: sensitivity size does not match! *****" << endl;
return;
}
fltdata.each_row() %= _sens.as_row();
fltdata.each_row() /= _sens.as_row();
if (_filterPrototype) {

View File

@ -4,12 +4,9 @@
//
// Description: Real Time Signal Viewer.
#pragma once
#include "lasp_avpowerspectra.h"
#include "lasp_filter.h"
#include "lasp_mathtypes.h"
#include "lasp_threadedindatahandler.h"
#include "lasp_timebuffer.h"
#include <memory>
#include <mutex>
/**

View File

@ -10,12 +10,12 @@ using rte = std::runtime_error;
inline d level_amp(d level_dB) { return pow(10, level_dB / 20); }
using mutexlock = std::scoped_lock<std::mutex>;
using slock = std::scoped_lock<std::recursive_mutex>;
vd Siggen::genSignal(const us nframes) {
DEBUGTRACE_ENTER;
mutexlock lck(_mtx);
slock lck(_mtx);
DEBUGTRACE_PRINT(nframes);
vd signal(nframes, arma::fill::value(_dc_offset));
@ -52,7 +52,7 @@ vd Siggen::genSignal(const us nframes) {
return signal;
}
void Siggen::setInterruptPeriod(const d newPeriod) {
mutexlock lck(_mtx);
slock lck(_mtx);
if (newPeriod == 0) {
throw rte("Interruption period cannot be 0");
}
@ -65,7 +65,7 @@ void Siggen::setInterruptPeriod(const d newPeriod) {
void Siggen::setFilter(const std::string &name,
std::shared_ptr<Filter> filter) {
DEBUGTRACE_ENTER;
mutexlock lck(_mtx);
slock lck(_mtx);
if (filter) {
_filters[name] = filter;
} else if (_filters.find(name) != _filters.end()) {
@ -74,17 +74,17 @@ void Siggen::setFilter(const std::string &name,
}
void Siggen::setDCOffset(const d offset) {
DEBUGTRACE_ENTER;
mutexlock lck(_mtx);
slock lck(_mtx);
_dc_offset = offset;
}
void Siggen::setLevel(const d level, bool dB) {
DEBUGTRACE_ENTER;
mutexlock lck(_mtx);
slock lck(_mtx);
_level_linear = dB ? level_amp(level) : level;
}
void Siggen::reset(const d newFs) {
DEBUGTRACE_ENTER;
mutexlock lck(_mtx);
slock lck(_mtx);
_fs = newFs;
for (auto &f : _filters) {
assert(f.second);

View File

@ -27,7 +27,7 @@ private:
bool _muted = false;
protected:
std::mutex _mtx;
mutable std::recursive_mutex _mtx;
d _fs = 0;
/**
* @brief Interuption of period the signal. If set, the signal will be

View File

@ -7,11 +7,14 @@
//////////////////////////////////////////////////////////////////////
/* #define DEBUGTRACE_ENABLED */
#include "lasp_siggen_impl.h"
#include "debugtrace.hpp"
#include "lasp_mathtypes.h"
#include <cassert>
#include "debugtrace.hpp"
#include "lasp_mathtypes.h"
using rte = std::runtime_error;
using slock = std::scoped_lock<std::recursive_mutex>;
DEBUGTRACE_VARIABLES;
@ -30,6 +33,7 @@ Sine::Sine(const d freq) : omg(2 * arma::datum::pi * freq) { DEBUGTRACE_ENTER; }
vd Sine::genSignalUnscaled(const us nframes) {
/* DEBUGTRACE_ENTER; */
slock lck(_mtx);
const d pi = arma::datum::pi;
vd phase_vec =
arma::linspace(phase, phase + omg * (nframes - 1) / _fs, nframes);
@ -41,8 +45,8 @@ vd Sine::genSignalUnscaled(const us nframes) {
}
vd Periodic::genSignalUnscaled(const us nframes) {
vd res(nframes);
slock lck(_mtx);
if (_signal.size() == 0) {
throw rte("No signal defined while calling");
}
@ -74,15 +78,15 @@ Sweep::Sweep(const d fl, const d fu, const d Ts, const d Tq, const us flags)
}
void Sweep::resetImpl() {
DEBUGTRACE_ENTER;
slock lck(_mtx);
_cur_pos = 0;
bool forward_sweep = flags & ForwardSweep;
bool backward_sweep = flags & BackwardSweep;
const d Dt = 1 / _fs; // Deltat
const d Dt = 1 / _fs; // Deltat
// Estimate N, the number of samples in the sweep part (non-quiescent part):
const us Ns = (us)(Ts * _fs);
@ -166,7 +170,6 @@ void Sweep::resetImpl() {
/* dVARTRACE(15, phase); */
}
} else if (flags & LogSweep) {
DEBUGTRACE_PRINT("Log sweep");
if (forward_sweep || backward_sweep) {
/* Forward or backward sweep */
@ -194,7 +197,6 @@ void Sweep::resetImpl() {
phase += 2 * number_pi * Dt * fn;
}
} else {
DEBUGTRACE_PRINT("Continuous sweep");
const us Nf = Ns / 2;
@ -249,17 +251,15 @@ void Sweep::resetImpl() {
/* dbgassert(fn >= 0, "BUG"); */
phase += 2 * number_pi * Dt * fn;
while (phase > 2 * number_pi)
phase -= 2 * number_pi;
while (phase > 2 * number_pi) phase -= 2 * number_pi;
/* dVARTRACE(17, phase); */
}
/* This should be a very small number!! */
DEBUGTRACE_PRINT(phase);
}
} // End of log sweep
} // End of log sweep
else {
// Either log or linear sweep had to be given as flags.
assert(false);
}
}

View File

@ -7,7 +7,7 @@
*/
/**
* \addtogroup siggen
* \addtogroup siggen
* @{
*/
@ -18,8 +18,8 @@ class Noise : public Siggen {
d level_linear;
virtual vd genSignalUnscaled(const us nframes) override;
void resetImpl() override;
public:
public:
/**
* @brief Constructs a noise generator. If no filter is used, the output will
* be white noise. By default, the output will be standard deviation = 1
@ -28,7 +28,6 @@ class Noise : public Siggen {
*/
Noise();
~Noise() = default;
};
/**
@ -37,13 +36,12 @@ class Noise : public Siggen {
class Sine : public Siggen {
d phase = 0;
d omg;
protected:
void resetImpl() override final { phase=0; }
protected:
void resetImpl() override final { phase = 0; }
virtual vd genSignalUnscaled(const us nframes) override final;
public:
public:
/**
* @brief Create a sine wave generator
*
@ -51,7 +49,7 @@ class Sine : public Siggen {
*/
Sine(const d freq_Hz);
~Sine() = default;
void setFreq(const d newFreq) { omg = 2*arma::datum::pi*newFreq; } ;
void setFreq(const d newFreq) { omg = 2 * arma::datum::pi * newFreq; };
};
/**
@ -86,8 +84,7 @@ class Sweep : public Periodic {
void resetImpl() override;
public:
public:
static constexpr int ForwardSweep = 1 << 0;
static constexpr int BackwardSweep = 1 << 1;
static constexpr int LinearSweep = 1 << 2;
@ -103,11 +100,11 @@ class Sweep : public Periodic {
* avoid temporal aliasing in case of measuring impulse responses.
* @param[in] sweep_flags: Sweep period [s]
*/
Sweep(const d fl, const d fu, const d Ts, const d Tq,
const us sweep_flags);
Sweep(const d fl, const d fu, const d Ts, const d Tq, const us sweep_flags);
~Sweep() = default;
};
/** @} */
/** @} */

View File

@ -1,13 +1,15 @@
/* #define DEBUGTRACE_ENABLED */
// #define DEBUGTRACE_ENABLED
#include "lasp_threadedindatahandler.h"
#include "debugtrace.hpp"
#include "lasp_daqdata.h"
#include "lasp_thread.h"
#include <future>
#include <optional>
#include <queue>
#include <thread>
#include "debugtrace.hpp"
#include "lasp_daqdata.h"
#include "lasp_thread.h"
using namespace std::literals::chrono_literals;
using lck = std::scoped_lock<std::mutex>;
using rte = std::runtime_error;
@ -20,26 +22,26 @@ class SafeQueue {
std::mutex _mtx;
std::atomic<uint32_t> _contents{0};
public:
public:
void push(const DaqData &d) {
DEBUGTRACE_ENTER;
lck lock(_mtx);
_queue.push(d);
_contents++;
assert(_contents == _queue.size());
assert(_contents == _queue.size());
}
DaqData pop() {
DEBUGTRACE_ENTER;
if (empty()) {
throw rte("BUG: Pop on empty queue");
}
}
lck lock(_mtx);
/* DaqData d(std::move(_queue.front())); */
DaqData d(_queue.front());
_queue.pop();
_contents--;
assert(_contents == _queue.size());
assert(_contents == _queue.size());
return d;
}
/**
@ -52,58 +54,75 @@ public:
};
ThreadedInDataHandlerBase::ThreadedInDataHandlerBase(SmgrHandle mgr,
InCallbackType cb,
InResetType reset)
: _indatahandler(
mgr,
std::bind(&ThreadedInDataHandlerBase::_inCallbackFromInDataHandler, this,
_1),
reset),
_queue(std::make_unique<SafeQueue>()), inCallback(cb) {
InCallbackType cb,
ResetCallbackType reset)
: _queue(std::make_unique<SafeQueue>()),
inCallback(cb),
resetCallback(reset),
_smgr(mgr) {
DEBUGTRACE_ENTER;
}
void ThreadedInDataHandlerBase::startThread() {
DEBUGTRACE_ENTER;
_thread_can_safely_run = true;
_indatahandler.start();
if (_indatahandler) {
throw rte("BUG: ThreadedIndataHandler already started");
}
SmgrHandle smgr = _smgr.lock();
if (!smgr) {
cerr << "Stream manager destructed" << endl;
return;
}
_indatahandler = std::make_unique<InDataHandler>(
smgr,
std::bind(&ThreadedInDataHandlerBase::_inCallbackFromInDataHandler, this,
_1),
resetCallback);
_thread_allowed_to_run = true;
_indatahandler->start();
}
void ThreadedInDataHandlerBase::_inCallbackFromInDataHandler(
const DaqData &daqdata) {
DEBUGTRACE_ENTER;
std::scoped_lock lck(_mtx);
// Early return in case object is under DESTRUCTION
if (!_thread_can_safely_run)
return;
if (!_thread_allowed_to_run) return;
_queue->push(daqdata);
if (!_thread_running) {
DEBUGTRACE_PRINT("Pushing new thread in pool");
_thread_running = true;
_pool.push_task(&ThreadedInDataHandlerBase::threadFcn, this);
}
}
void ThreadedInDataHandlerBase::stopThread() {
DEBUGTRACE_ENTER;
// Make sure inCallback is no longer called
_thread_can_safely_run = false;
_indatahandler.stop();
if (!_indatahandler) {
throw rte("BUG: ThreadedIndataHandler not running");
}
std::scoped_lock lck(_mtx);
// Stop the existing thread
_thread_allowed_to_run = false;
// Make sure no new data arrives
_indatahandler->stop();
_indatahandler.reset();
DEBUGTRACE_PRINT("Indatahandler stopped. Waiting for thread to finish...");
// Then wait in steps for the thread to stop running.
while (_thread_running) {
std::this_thread::sleep_for(10us);
}
DEBUGTRACE_PRINT("Thread stopped");
// Kill the handler
DEBUGTRACE_PRINT("Handler resetted");
}
ThreadedInDataHandlerBase::~ThreadedInDataHandlerBase() {
DEBUGTRACE_ENTER;
if (_thread_can_safely_run) {
if (_thread_allowed_to_run) {
stopThread();
cerr << "*** BUG: InDataHandlers have not been all stopped, while "
"StreamMgr destructor is called. This is a misuse BUG."
@ -113,12 +132,9 @@ ThreadedInDataHandlerBase::~ThreadedInDataHandlerBase() {
}
void ThreadedInDataHandlerBase::threadFcn() {
DEBUGTRACE_ENTER;
_thread_running = true;
while (!_queue->empty() && _thread_can_safely_run) {
while (!_queue->empty() && _thread_allowed_to_run) {
// Call inCallback_threaded
inCallback(_queue->pop());
}

View File

@ -29,21 +29,27 @@ class ThreadedInDataHandlerBase {
* @brief The queue used to push elements to the handling thread.
*/
InDataHandler _indatahandler;
std::unique_ptr<SafeQueue> _queue;
mutable std::recursive_mutex _mtx;
std::atomic<bool> _thread_running{false};
std::atomic<bool> _thread_can_safely_run{false};
GlobalThreadPool _pool;
/**
* @brief Function pointer that is called when new DaqData arrives.
*/
const InCallbackType inCallback;
/**
* @brief Function pointer that is called when reset() is called.
*/
const ResetCallbackType resetCallback;
std::weak_ptr<StreamMgr> _smgr;
std::unique_ptr<InDataHandler> _indatahandler;
std::atomic<bool> _thread_running{false};
std::atomic<bool> _thread_allowed_to_run{false};
GlobalThreadPool _pool;
void threadFcn();
@ -58,7 +64,7 @@ class ThreadedInDataHandlerBase {
void _inCallbackFromInDataHandler(const DaqData &daqdata);
public:
ThreadedInDataHandlerBase(SmgrHandle mgr, InCallbackType cb, InResetType reset);
ThreadedInDataHandlerBase(SmgrHandle mgr, InCallbackType cb, ResetCallbackType reset);
~ThreadedInDataHandlerBase();
/**
* @brief This method should be called from the derived class' constructor,

View File

@ -29,6 +29,9 @@ void init_deviceinfo(py::module& m) {
devinfo.def_readonly("availableInputRanges",
&DeviceInfo::availableInputRanges);
devinfo.def_readonly("prefInputRangeIndex", &DeviceInfo::prefInputRangeIndex);
devinfo.def_readonly("availableOutputRanges",
&DeviceInfo::availableOutputRanges);
devinfo.def_readonly("prefOutputRangeIndex", &DeviceInfo::prefOutputRangeIndex);
devinfo.def_readonly("ninchannels", &DeviceInfo::ninchannels);
devinfo.def_readonly("noutchannels", &DeviceInfo::noutchannels);
@ -36,7 +39,10 @@ void init_deviceinfo(py::module& m) {
devinfo.def_readonly("hasInputACCouplingSwitch",
&DeviceInfo::hasInputACCouplingSwitch);
devinfo.def_readonly("hasDuplexMode", &DeviceInfo::hasDuplexMode);
devinfo.def_readonly("duplexModeForced", &DeviceInfo::duplexModeForced);
devinfo.def_readonly("hasInternalOutputMonitor", &DeviceInfo::hasInternalOutputMonitor);
devinfo.def_readonly("physicalInputQty", &DeviceInfo::physicalInputQty);
devinfo.def_readonly("physicalOutputQty", &DeviceInfo::physicalOutputQty);
}

View File

@ -1,13 +1,16 @@
#include <pybind11/pybind11.h>
#include <iostream>
#include "arma_npy.h"
#include "lasp_avpowerspectra.h"
#include "lasp_biquadbank.h"
#include "lasp_fft.h"
#include "lasp_filter.h"
#include "lasp_freqsmooth.h"
#include "lasp_slm.h"
#include "lasp_streammgr.h"
#include "lasp_window.h"
#include <iostream>
#include <pybind11/pybind11.h>
using std::cerr;
using std::endl;
@ -27,7 +30,6 @@ using rte = std::runtime_error;
*/
void init_dsp(py::module &m) {
py::class_<Fft> fft(m, "Fft");
fft.def(py::init<us>());
fft.def("fft", [](Fft &f, dpyarray dat) {
@ -114,9 +116,10 @@ void init_dsp(py::module &m) {
aps.def("compute", [](AvPowerSpectra &aps, dpyarray timedata) {
std::optional<ccube> res;
dmat timedata_mat = NpyToMat<d, false>(timedata);
{
py::gil_scoped_release release;
res = aps.compute(NpyToMat<d, false>(timedata));
res = aps.compute(timedata_mat);
}
return CubeToNpy<c>(res.value_or(ccube(0, 0, 0)));
@ -151,5 +154,12 @@ void init_dsp(py::module &m) {
slm.def("Lmax", [](const SLM &slm) { return ColToNpy<d>(slm.Lmax()); });
slm.def("Lpeak", [](const SLM &slm) { return ColToNpy<d>(slm.Lpeak()); });
slm.def_static("suggestedDownSamplingFac", &SLM::suggestedDownSamplingFac);
// Frequency smoother
m.def("freqSmooth", [](dpyarray freq, dpyarray X, unsigned w) {
vd freqa = NpyToCol<d, false>(freq);
vd Xa = NpyToCol<d, false>(X);
return ColToNpy(freqSmooth(freqa, Xa, w));
});
}
/** @} */

View File

@ -1,4 +1,10 @@
/* #define DEBUGTRACE_ENABLED */
// #define DEBUGTRACE_ENABLED
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <armadillo>
#include <atomic>
#include "arma_npy.h"
#include "debugtrace.hpp"
#include "lasp_clip.h"
@ -9,14 +15,12 @@
#include "lasp_rtsignalviewer.h"
#include "lasp_streammgr.h"
#include "lasp_threadedindatahandler.h"
#include <armadillo>
#include <atomic>
#include <chrono>
#include <pybind11/pybind11.h>
using namespace std::literals::chrono_literals;
using std::cerr;
using std::endl;
using rte = std::runtime_error;
using Lck = std::scoped_lock<std::recursive_mutex>;
namespace py = pybind11;
@ -48,17 +52,17 @@ py::array_t<T> getPyArrayNoCpy(const DaqData &d) {
*/
return py::array_t<T>(
py::array::ShapeContainer({d.nframes, d.nchannels}), // Shape
py::array::ShapeContainer({d.nframes, d.nchannels}), // Shape
py::array::StridesContainer( // Strides
py::array::StridesContainer( // Strides
{sizeof(T),
sizeof(T) * d.nframes}), // Strides (in bytes) for each index
sizeof(T) * d.nframes}), // Strides (in bytes) for each index
reinterpret_cast<T *>(
const_cast<DaqData &>(d).raw_ptr()), // Pointer to buffer
const_cast<DaqData &>(d).raw_ptr()), // Pointer to buffer
dummyDataOwner // As stated above, now Numpy does not take ownership of
// the data pointer.
dummyDataOwner // As stated above, now Numpy does not take ownership of
// the data pointer.
);
}
@ -81,17 +85,17 @@ py::array_t<d> dmat_to_ndarray(const DaqData &d) {
*/
return py::array_t<T>(
py::array::ShapeContainer({d.nframes, d.nchannels}), // Shape
py::array::ShapeContainer({d.nframes, d.nchannels}), // Shape
py::array::StridesContainer( // Strides
py::array::StridesContainer( // Strides
{sizeof(T),
sizeof(T) * d.nframes}), // Strides (in bytes) for each index
sizeof(T) * d.nframes}), // Strides (in bytes) for each index
reinterpret_cast<T *>(
const_cast<DaqData &>(d).raw_ptr()), // Pointer to buffer
const_cast<DaqData &>(d).raw_ptr()), // Pointer to buffer
dummyDataOwner // As stated above, now Numpy does not take ownership of
// the data pointer.
dummyDataOwner // As stated above, now Numpy does not take ownership of
// the data pointer.
);
}
@ -104,9 +108,11 @@ class PyIndataHandler : public ThreadedInDataHandler<PyIndataHandler> {
/**
* @brief The callback functions that is called.
*/
py::function cb, reset_callback;
py::object _cb, _reset_callback;
std::atomic<bool> _done{false};
std::recursive_mutex _mtx;
public:
public:
/**
* @brief Initialize PyIndataHandler
*
@ -117,19 +123,26 @@ public:
* is called, when a stream stops, this pointer / handle will dangle.
*/
PyIndataHandler(SmgrHandle mgr, py::function cb, py::function reset_callback)
: ThreadedInDataHandler(mgr), cb(cb), reset_callback(reset_callback) {
: ThreadedInDataHandler(mgr),
_cb(py::weakref(cb)),
_reset_callback(py::weakref(reset_callback)) {
DEBUGTRACE_ENTER;
// cerr << "Thread ID: " << std::this_thread::get_id() << endl;
/// Start should be called externally, as at constructor time no virtual
/// functions should be called.
py::gil_scoped_release release;
if (_cb().is_none() || _reset_callback().is_none()) {
throw rte("cb or reset_callback is none!");
}
startThread();
}
~PyIndataHandler() {
DEBUGTRACE_ENTER;
// cerr << "Thread ID: " << std::this_thread::get_id() << endl;
/// Callback cannot be called, which results in a deadlock on the GIL
/// without this release.
py::gil_scoped_release release;
DEBUGTRACE_PRINT("Gil released");
_done = true;
stopThread();
}
/**
@ -137,81 +150,123 @@ public:
*
* @param daq Daq device, or nullptr in case no input stream is running.
*/
void reset(const Daq *daq) {
void reset(const Daq *daqi) {
DEBUGTRACE_ENTER;
try {
py::gil_scoped_acquire acquire;
if (daq) {
reset_callback(daq);
} else {
reset_callback(py::none());
// cerr << "Thread ID: " << std::this_thread::get_id() << endl;
if (_done) return;
{
try {
py::object reset_callback = _reset_callback();
if (reset_callback.is_none()) {
DEBUGTRACE_PRINT("reset_callback is none, weakref killed");
_done = true;
return;
}
if (daqi != nullptr) {
assert(reset_callback);
reset_callback(daqi);
} else {
assert(reset_callback);
reset_callback(py::none());
}
} catch (py::error_already_set &e) {
cerr << "*************** Error calling reset callback!\n";
cerr << e.what() << endl;
cerr << "*************** \n";
/// Throwing a runtime error here does not work out one way or another.
/// Therefore, it is better to dive out and prevent undefined behaviour
abort();
/* throw std::runtime_error(e.what()); */
} catch (std::exception &e) {
cerr << "Caught unknown exception in reset callback:" << e.what()
<< endl;
abort();
}
} catch (py::error_already_set &e) {
cerr << "*************** Error calling reset callback!\n";
cerr << e.what() << endl;
cerr << "*************** \n";
/// Throwing a runtime error here does not work out one way or another.
/// Therefore, it is better to dive out and prevent undefined behaviour
abort();
/* throw std::runtime_error(e.what()); */
} catch (std::exception &e) {
cerr << "Caught unknown exception in reset callback:" << e.what() << endl;
abort();
}
}
} // end of GIL scope
} // end of function reset()
/**
* @brief Calls the Python callback method / function with a Numpy array of
* stream data.
*/
void inCallback(const DaqData &d) {
/* DEBUGTRACE_ENTER; */
DEBUGTRACE_ENTER;
// cerr << "=== Enter incallback for thread ID: " << std::this_thread::get_id() << endl;
using DataType = DataTypeDescriptor::DataType;
try {
py::gil_scoped_acquire acquire;
py::object bool_val;
switch (d.dtype) {
case (DataType::dtype_int8): {
bool_val = cb(getPyArrayNoCpy<int8_t>(d));
} break;
case (DataType::dtype_int16): {
bool_val = cb(getPyArrayNoCpy<int16_t>(d));
} break;
case (DataType::dtype_int32): {
bool_val = cb(getPyArrayNoCpy<int32_t>(d));
} break;
case (DataType::dtype_fl32): {
bool_val = cb(getPyArrayNoCpy<float>(d));
} break;
case (DataType::dtype_fl64): {
bool_val = cb(getPyArrayNoCpy<double>(d));
} break;
default:
throw std::runtime_error("BUG");
} // End of switch
bool res = bool_val.cast<bool>();
} catch (py::error_already_set &e) {
cerr << "ERROR: Python raised exception from callback function: ";
cerr << e.what() << endl;
abort();
} catch (py::cast_error &e) {
cerr << e.what() << endl;
cerr << "ERROR: Python callback does not return boolean value." << endl;
abort();
} catch (std::exception &e) {
cerr << "Caught unknown exception in Python callback:" << e.what()
<< endl;
abort();
if (_done) {
DEBUGTRACE_PRINT("Early stop, done");
return;
}
}
{
DEBUGTRACE_PRINT("================ TRYING TO OBTAIN GIL in inCallback...");
py::gil_scoped_acquire acquire;
try {
py::object py_bool;
py::object cb = _cb();
if (cb.is_none()) {
DEBUGTRACE_PRINT("cb is none, weakref killed");
_done = true;
return;
}
switch (d.dtype) {
case (DataType::dtype_int8): {
py_bool = cb(getPyArrayNoCpy<int8_t>(d));
} break;
case (DataType::dtype_int16): {
py_bool = cb(getPyArrayNoCpy<int16_t>(d));
} break;
case (DataType::dtype_int32): {
py_bool = cb(getPyArrayNoCpy<int32_t>(d));
} break;
case (DataType::dtype_fl32): {
py_bool = cb(getPyArrayNoCpy<float>(d));
} break;
case (DataType::dtype_fl64): {
py_bool = cb(getPyArrayNoCpy<double>(d));
} break;
default:
throw std::runtime_error("BUG");
} // End of switch
bool res = py_bool.cast<bool>();
if (res == false) {
DEBUGTRACE_PRINT("Setting callbacks to None")
_done = true;
// By doing this, we remove the references, but in the mean time this
// might also trigger removing Python objects. Including itself, as
// there is no reference to it anymore. The consequence is that the
// current object might be destroyed from this thread. However, if we
// do not remove these references and in lasp_record.py finish() is
// not called, we end up with not-garbage collected recordings in
// memory. This is also not good. How can we force Python to not yet
// destroy this object?
// cb.reset();
// reset_callback.reset();
}
} catch (py::error_already_set &e) {
cerr << "ERROR (BUG): Python raised exception from callback function: ";
cerr << e.what() << endl;
abort();
} catch (py::cast_error &e) {
cerr << e.what() << endl;
cerr << "ERROR (BUG): Python callback does not return boolean value."
<< endl;
abort();
} catch (std::exception &e) {
cerr << "Caught unknown exception in Python callback:" << e.what()
<< endl;
abort();
}
} // End of scope in which the GIL is acquired
// cerr << "=== LEAVE incallback for thread ID: " << std::this_thread::get_id() << endl;
} // End of function inCallback()
};
void init_datahandler(py::module &m) {
/// The C++ class is PyIndataHandler, but for Python, it is called
/// InDataHandler
py::class_<PyIndataHandler> pyidh(m, "InDataHandler");
@ -244,29 +299,29 @@ void init_datahandler(py::module &m) {
cval = clip.getCurrentValue();
}
return ColToNpy<arma::uword>(cval); // something goes wrong here
return ColToNpy<arma::uword>(cval); // something goes wrong here
});
/// Real time Aps
///
py::class_<RtAps> rtaps(m, "RtAps");
rtaps.def(py::init<SmgrHandle, // StreamMgr
Filter *const, // FreqWeighting filter
const us, // Nfft
const Window::WindowType, // Window
const d, // Overlap percentage 0<=o<100
rtaps.def(py::init<SmgrHandle, // StreamMgr
Filter *const, // FreqWeighting filter
const us, // Nfft
const Window::WindowType, // Window
const d, // Overlap percentage 0<=o<100
const d // Time constant
const d // Time constant
>(),
py::arg("streammgr"), // StreamMgr
py::arg("streammgr"), // StreamMgr
py::arg("preFilter").none(true),
/// Below list of arguments *SHOULD* be same as for
/// AvPowerSpectra constructor!
py::arg("nfft") = 2048, //
py::arg("windowType") = Window::WindowType::Hann, //
py::arg("overlap_percentage") = 50.0, //
py::arg("time_constant") = -1 //
py::arg("nfft") = 2048, //
py::arg("windowType") = Window::WindowType::Hann, //
py::arg("overlap_percentage") = 50.0, //
py::arg("time_constant") = -1 //
);
rtaps.def("getCurrentValue", [](RtAps &rt) {
@ -281,10 +336,10 @@ void init_datahandler(py::module &m) {
/// Real time Signal Viewer
///
py::class_<RtSignalViewer> rtsv(m, "RtSignalViewer");
rtsv.def(py::init<SmgrHandle, // StreamMgr
const d, // Time history
const us, // Resolution
const us // Channel number
rtsv.def(py::init<SmgrHandle, // StreamMgr
const d, // Time history
const us, // Resolution
const us // Channel number
>());
rtsv.def("getCurrentValue", [](RtSignalViewer &rt) {

View File

@ -5,7 +5,7 @@ requires-python = ">=3.10"
description = "Library for Acoustic Signal Processing"
license = { "file" = "LICENSE" }
authors = [{ "name" = "J.A. de Jong", "email" = "j.a.dejong@ascee.nl" }]
version = "1.0.4"
version = "1.6.0"
keywords = ["DSP", "DAQ", "Signal processing"]
@ -22,34 +22,40 @@ classifiers = [
]
urls = { "Documentation" = "https://lasp.ascee.nl" }
dependencies = ["scipy", "numpy", "matplotlib>=3.7.2", "appdirs",
"dataclasses_json", "h5py"]
dependencies = [
"scipy",
"matplotlib>=3.7.2",
"appdirs",
"dataclasses_json",
"h5py",
]
[build-system] # How pip and other frontends should build this project
requires = ["py-build-cmake~=0.1.8", "pybind11" ]
requires = ["py-build-cmake~=0.1.8", "pybind11"]
build-backend = "py_build_cmake.build"
[tool.py-build-cmake.module] # Where to find the Python module to package
directory = "python_src"
[tool.py-build-cmake.sdist] # What to include in source distributions
include = ["CMakeLists.txt", "cmake", "cpp_src", "python_src", "img", "scripts",
"third_party"]
include = [
"CMakeLists.txt",
"cmake",
"cpp_src",
"python_src",
"img",
"scripts",
"third_party",
]
[tool.py-build-cmake.cmake] # How to build the CMake project
build_type = "Release"
source_path = "."
build_args = ["-j12"]
options = { "LASP_HAS_PORTAUDIO" = "ON", "LASP_HAS_RTAUDIO" = "OFF" }
build_args = ["-j"]
install_components = ["python_modules"]
[tool.py-build-cmake.editable]
# This might not work properly on Windows. Comment this out when testing on
# Windows.
mode = "symlink"
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "v$version"
version_scheme = "semver"
version_provider = "pep621"
update_changelog_on_bump = true

View File

@ -209,6 +209,7 @@ class FilterBankDesigner:
Returns:
h: Linear filter transfer function [-]
"""
fs = self.fs
fir = self.createFirFilter(fs, x)
# Decimated sampling frequency [Hz]

View File

@ -33,7 +33,7 @@ class Atomic:
def checkType(self, val):
if not (type(val) == bool or type(val) == int):
raise RuntimeError("Invalid type for Atomic")
raise ValueError("Invalid type for Atomic")
def __iadd__(self, toadd):
self.checkType(toadd)

View File

@ -62,8 +62,9 @@ class Qty:
name: str
# I.e.: Pascal
unit_name: str
# I.e.: Pa
# I.e.: -, Pa, V
unit_symb: str
# I.e.: ('dB SPL') <== tuple of possible level units
level_unit: object
# Contains a tuple of possible level names, including its reference value.
@ -92,6 +93,18 @@ class Qty:
"""
return self.cpp_enum.value
@property
def unit_symb_eq(self):
"""Unit symbol to be used in equations
Returns:
String: V, Pa, 1,
"""
if self.unit_symb != '-':
return self.unit_symb
else:
return '1'
@unique

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
"""!
Author: J.A. de Jong - ASCEE
@ -11,18 +10,21 @@ The ASCEE hdf5 measurement file format contains the following fields:
- Attributes:
'version': If not given, version 1 is assumed. For version 1, measurement data
is assumed to be acoustic data.
'samplerate': The audio data sample rate in Hz.
'nchannels': The number of audio channels in the file
'LASP_VERSION_MAJOR': int The major version of LASP which which the recording has been performed.
'LASP_VERSION_MINOR': int The minor version of LASP which which the recording has been performed.
'samplerate': The audio data sample rate in Hz [float]
'nchannels': The number of audio channels in the file List[float]
'sensitivity': (Optionally) the stored sensitivity of the record channels.
This can be a single value, or a list of sensitivities for
each channel. Both representations are allowed.
each channel. Both representations are allowed. List[float]
For measurement files of LASP < v1.0
'qtys' : (Optionally): list of quantities that is recorded for each channel',
if this array is not found. Quantities are defaulted to 'Number / Full scale'
'type_int': A specified measurement type that can be used programmatically. It can be read out as an enumeration variant of type "MeasurementType". See code below of implemented measurement types.
For measurement files of LASP >= 1.0
- Datasets:
@ -32,7 +34,7 @@ block index, the second axis the sample number and the third axis is the
channel number. The data type is either int16, int32 or float64 / float32. If
raw data is stored as integer values (int16, int32), the actual values should
be pre-scaled by its maximum positive number (2**(nb-1) - 1), such that the
corresponding 'number' lies between -1.0 and 1.0.
corresponding 'number' lies between -1.0 and 1.0. To stay backwards-compatible, the dataset is always called 'audio' despite it being possible that other types of data is stored in the dataset (such as voltages, accelerations etc).
'video': 4-dimensional array of video frames. The first index is the frame
number, the second the x-value of the pixel and the third is the
@ -44,10 +46,16 @@ The video dataset can possibly be not present in the data.
"""
__all__ = ["Measurement", "scaleBlockSens"]
__all__ = ["Measurement", "scaleBlockSens", "MeasurementType"]
from contextlib import contextmanager
from weakref import WeakValueDictionary
import h5py as h5
import uuid
import pathlib
import glob
import itertools
import numpy as np
from enum import Enum, unique
from .lasp_config import LASP_NUMPY_FLOAT_TYPE
from scipy.io import wavfile
import os, time, wave, logging
@ -57,6 +65,45 @@ from .lasp_cpp import Window, DaqChannel, AvPowerSpectra
from typing import List
from functools import lru_cache
# Measurement file extension
MEXT = 'h5'
DOTMEXT = f'.{MEXT}'
@unique
class MeasurementType(Enum):
"""
Measurement flags related to the measurement. Stored as bit flags in the measurement file. This is for possible changes in the API later.
"""
# Not specific measurement type
NotSpecific = 0
# Measurement serves as an insertion loss reference measurement
ILReference = 1 << 0
# Measurement is general calibration measurement (to calibrate sensor in a certain way)
CALGeneral = 1 << 1
# Measurement serves as impedance tube calibration (short tube case / ref. plane at origin)
muZCalShort = 1 << 2
# Measurement serves as impedance tube calibration (long tube case)
muZCalLong = 1 << 3
# Series impedance reference
muZSeriesImpedanceRef = 1 << 4
def __str__(self):
match self:
case MeasurementType.NotSpecific: return '-'
case MeasurementType.ILReference: return 'Insertion loss reference'
case MeasurementType.CALGeneral: return 'General calibration'
case MeasurementType.muZCalShort: return 'ASCEE μZ short length calibration'
case MeasurementType.muZCalLong: return 'ASCEE μZ long length calibration'
case MeasurementType.muZSeriesImpedanceRef: return 'ASCEE μZ series impedance empty reference'
case _: raise ValueError("Not a MeasurementType")
def getSampWidth(dtype):
"""Returns the width of a single sample in **bytes**.
@ -92,7 +139,7 @@ def scaleBlockSens(block, sens):
fac = 2 ** (8 * sw - 1) - 1
else:
fac = 1.0
return block.astype(LASP_NUMPY_FLOAT_TYPE) / fac / sens[np.newaxis, :]
return block.astype(LASP_NUMPY_FLOAT_TYPE) / fac / sens[np.newaxis,:]
class IterRawData:
@ -165,7 +212,7 @@ class IterRawData:
# print(f'block: {block}, starto: {start_offset}, stopo {stop_offset}')
self.i += 1
return fa[block, start_offset:stop_offset, :][:, self.channels]
return fa[block, start_offset:stop_offset,:][:, self.channels]
class IterData(IterRawData):
@ -188,17 +235,19 @@ class Measurement:
"""Provides access to measurement data stored in the h5 measurement file
format."""
# Store a dict of open measurements, with uuid string as a key. We store them as a weak ref.
uuid_s = WeakValueDictionary()
def __init__(self, fn):
"""Initialize a Measurement object based on the filename."""
if ".h5" not in fn:
fn += ".h5"
# Add extension if tried to open without exension
if DOTMEXT not in fn:
fn += DOTMEXT
# Full filepath
self.fn = fn
# Base filename
self.fn_base = os.path.split(fn)[1]
# Open the h5 file in read-plus mode, to allow for changing the
# measurement comment.
with h5.File(fn, "r") as f:
@ -222,9 +271,39 @@ class Measurement:
self.version_major = f.attrs["LASP_VERSION_MAJOR"]
self.version_minor = f.attrs["LASP_VERSION_MINOR"]
except KeyError:
# No version information stored
self.version_major = 0
self.version_minor = 1
try:
# Try to catch UUID (Universally Unique IDentifier)
self._UUID = f.attrs['UUID']
# Flag indicating we have to add a new UUID
create_new_uuid = False
except KeyError:
create_new_uuid = True
try:
# UUID of the reference measurement. Should be stored as
# a lists of tuples, where each tuple is a combination of (<MeasurementType.value>, <uuid_string>, <last_filename>).
# The last filename is a filename that *probably* is the reference measurement with
# given UUID. If it is not, we will search for it in the same directory as `this` measurement.
# If we cannot find it there, we will give up, and remove the field corresponding to this reference measurement type.
refMeas_list = f.attrs['refMeas']
# Build a tuple string from it
self._refMeas = {}
for (key, val, name) in refMeas_list:
self._refMeas[MeasurementType(int(key))] = (val, name)
except KeyError:
self._refMeas = {}
try:
self._type_int = f.attrs['type_int']
except KeyError:
self._type_int = 0
# Due to a previous bug, the channel names were not stored
# consistently, i.e. as 'channel_names' and later camelcase.
try:
@ -282,20 +361,202 @@ class Measurement:
f"Physical quantity data not available in measurement file. Assuming {SIQtys.default}"
)
def setAttribute(self, atrname, value):
if create_new_uuid:
# Create and store a random UUID based on *now* and store it forever
# inside of the Measurement file
self.genNewUUID()
else:
if self.UUID in Measurement.uuid_s.keys():
raise RuntimeError(f"Measurement '{self.name}' is already opened. Cannot open measurement twice. Note: this error can happen when measurements are manually copied.")
# Store weak reference to 'self' in list of UUID's. They are removed when no file is open anymore
Measurement.uuid_s[self._UUID] = self
def rename(self, newname: str):
"""
Try to rename the measurement file.
Args:
newname: New name, with or without extension
"""
_ , ext = os.path.splitext(newname)
# Add proper extension if new name is given without extension.
if ext != DOTMEXT:
newname = newname + DOTMEXT
# Folder, Base filename + extension
folder, _ = os.path.split(self.fn)
newname_full = str(pathlib.Path(folder) / newname)
os.rename(self.fn, newname_full)
self.fn = newname_full
def genNewUUID(self):
"""
Create new UUID for measurement and store in file.
"""
self.setAttribute('UUID', str(uuid.uuid1()))
@property
def UUID(self):
"""
Universally unique identifier
"""
return self._UUID
def getRefMeas(self, mtype: MeasurementType):
"""
Return corresponding reference measurement, if configured and can be found. If the reference
measurement is currently not open, it tries to open it by traversing other measurement
files in the current directory. Throws a runtime error in case the reference measurement cannot be found.
Throws a ValueError when the reference measurement is not configured.
"""
# See if we can find the UUID for the required measurement type
try:
required_uuid, possible_name = self._refMeas[mtype]
except KeyError:
raise ValueError(f"No reference measurement configured for '{self.name}'")
m = None
# Try to find it in the dictionary of of open measurements
if required_uuid in Measurement.uuid_s.keys():
m = Measurement.uuid_s[required_uuid]
logging.debug(f'Returned reference measurement {m.name} from list of open measurements')
# Not found in list of openend measurements. See if we can open it using its last stored file name we know of
if m is None:
try:
m = Measurement(possible_name)
if m.UUID == required_uuid:
logging.info(f'Opened reference measurement {m.name} by name')
except Exception as e:
logging.error(f'Could not find reference measurement using file name: {possible_name}')
# Last resort, see if we can find the right measurement in the same folder
if m is None:
try:
folder, _ = os.path.split(self.fn)
m = Measurement.fromFolderWithUUID(required_uuid, folder, skip=[self.name])
logging.info('Found reference measurement in folder with correct UUID. Updating name of reference measurement')
# Update the measurement file name in the list, such that next time it
# can be opened just by its name.
self.setRefMeas(m)
except:
logging.error("Could not find the reference measurement. Is it deleted?")
# Well, we found it. Now make sure the reference measurement actually has the right type (User could have marked it as a NotSpecific for example in the mean time).
if m is not None:
if m.measurementType() != mtype:
m.removeRefMeas(mtype)
raise RuntimeError(f"Reference measurement for {self.name} is not a proper reference (anymore).")
# Whow, we passed all security checks, here we go!
return m
else:
# Nope, not there.
raise RuntimeError(f"Could not find the reference measurement for '{self.name}'. Is it deleted?")
def removeRefMeas(self, mtype: MeasurementType):
"""
Remove an existing reference measurement of specified type from this measurement. Silently ignores this
action if no reference measurement of this type is configured.
"""
try:
del self._refMeas[mtype]
self.__storeReafMeas()
except KeyError:
pass
def __storeReafMeas(self):
"""
Internal method that syncs the dictionary of reference methods with the backing HDF5 file
"""
with self.file("r+") as f:
# Update attribute in file. Stored as a flat list of string tuples:
# [(ref_value1, uuid_1, name_1), (ref_value2, uuid_2, name_2), ...]
reflist = list((str(key.value), val1, val2) for key, (val1, val2) in self._refMeas.items())
# print(reflist)
f.attrs['refMeas'] = reflist
def setRefMeas(self, m: Measurement):
"""
Set a reference measurement for the given measurement. If this measurement is already
a reference measurement, the previous reference measurement type is overwritten, such that there is
only one measurement that is the reference of a certain 'MeasurementType'
"""
mtype = m.measurementType()
if mtype == MeasurementType.NotSpecific:
raise ValueError('Measurement to be set as reference is not a reference measurement')
self._refMeas[mtype] = (m.UUID, m.name)
self.__storeReafMeas()
@staticmethod
def fromFolderWithUUID(uuid_str: str, folder: str='', skip=[]):
"""
Returns Measurement object from a given UUID string. It first tries to find whether there
is an uuid in the static list of weak references. If not, it will try to open files in
the current file path.
"""
for fn in glob.glob(str(pathlib.Path(folder)) + f'/*{DOTMEXT}'):
# Do not try to open this file in case it is in the 'skip' list.
if len(list(filter(lambda a: a in fn, skip))) > 0:
continue
try:
m = Measurement(fn)
if m.UUID == uuid_str:
# Update 'last_fn' attribute in dict of stored reference measurements
return m
except Exception as e:
logging.error(f'Possible measurement file {fn} returned error {e} when opening.')
raise RuntimeError(f'Measurement with UUID {uuid_str} could not be found.')
def setAttribute(self, attrname: str, value):
"""
Set an attribute in the measurement file, and keep a local copy in
memory for efficient accessing.
Args:
attrname: name of attribute, a string
value: the value. Should be anything that can be stored as an attribute in HDF5.
"""
with self.file("r+") as f:
# Update comment attribute in the file
f.attrs[atrname] = value
setattr(self, "_" + atrname, value)
f.attrs[attrname] = value
setattr(self, "_" + attrname, value)
def isType(self, type_: MeasurementType):
"""
Returns True when a measurement is flagged as being of a certaint "MeasurementType"
"""
if (type_.value & self._type_int):
return True
elif type_.value == self._type_int == 0:
return True
return False
def setType(self, type_: MeasurementType):
"""
Set the measurement type to given type
"""
self.setAttribute('type_int', type_.value)
def measurementType(self):
"""
Returns type of measurement
"""
return MeasurementType(self._type_int)
@property
def name(self):
"""Returns filename base without extension."""
return os.path.splitext(self.fn_base)[0]
_, fn = os.path.split(self.fn)
return os.path.splitext(fn)[0]
@property
def channelNames(self):
@ -303,12 +564,18 @@ class Measurement:
@channelNames.setter
def channelNames(self, newchnames):
"""
Returns list of the names of the channels
"""
if len(newchnames) != self.nchannels:
raise RuntimeError("Invalid length of new channel names")
self.setAttribute("channelNames", newchnames)
@property
def channelConfig(self):
"""
Returns list of current channel configuration data.
"""
chcfg = []
for chname, sens, qty in zip(self.channelNames, self.sensitivity, self.qtys):
ch = DaqChannel()
@ -321,6 +588,18 @@ class Measurement:
@channelConfig.setter
def channelConfig(self, chcfg: List[DaqChannel]):
"""
Set new channel configuration from list of DaqChannel objects.
Use cases:
- Update channel types, sensitivities etc.
Args:
chchfg: New channel configuration
"""
if len(chcfg) != self.nchannels:
raise RuntimeError("Invalid number of channels")
chname = []
sens = []
qtys = []
@ -429,13 +708,13 @@ class Measurement:
Nblock = block.shape[0]
sum_ += np.sum(block, axis=0)
N += Nblock
meansquare += np.sum(block**2, axis=0) / self.N
meansquare += np.sum(block ** 2, axis=0) / self.N
avg = sum_ / N
# In fact, this is not the complete RMS, as in includes the DC
# If p = p_dc + p_osc, then rms(p_osc) = sqrt(ms(p)-ms(p_dc))
if substract_average:
meansquare -= avg**2
meansquare -= avg ** 2
rms = np.sqrt(meansquare)
return rms
@ -540,7 +819,7 @@ class Measurement:
signal = self.data(channels)
# Estimate noise power in block
blocks = [signal[i * N : (i + 1) * N] for i in range(Nblocks)]
blocks = [signal[i * N: (i + 1) * N] for i in range(Nblocks)]
if noiseCorrection:
# The difference between the measured signal in the previous block and
@ -711,6 +990,28 @@ class Measurement:
wavfile.write(fn, int(self.samplerate), data.astype(newtype))
@staticmethod
def fromFile(fn):
"""
Try to open measurement from a given file name. First checks
whether the measurement is already open. Otherwise it might
happen that a Measurement object is created twice for the same backing file, which we do not allow.
"""
# See if the base part of the filename is referring to a file that is already open
with h5.File(fn, 'r') as f:
try:
theuuid = f.attrs['UUID']
except KeyError:
# No UUID stored in measurement. This is an old measurement that did not have UUID's
# We create a new UUID here such that the file is opened from the filesystem
# anyhow.
theuuid = str(uuid.uuid1())
if theuuid in Measurement.uuid_s.keys():
return Measurement.uuid_s[theuuid]
return Measurement(fn)
@staticmethod
def fromtxt(
fn,
@ -744,10 +1045,11 @@ class Measurement:
raise ValueError(f"File {fn} does not exist.")
if timestamp is None:
timestamp = os.path.getmtime(fn)
if mfn is None:
mfn = os.path.splitext(fn)[0] + ".h5"
mfn = os.path.splitext(fn)[0] + DOTMEXT
else:
mfn = os.path.splitext(mfn)[0] + ".h5"
mfn = os.path.splitext(mfn)[0] + DOTMEXT
dat = np.loadtxt(fn, skiprows=skiprows, delimiter=delimiter)
if firstcoltime:
@ -788,8 +1090,8 @@ class Measurement:
sensitivity,
mfn,
timestamp=None,
qtys: List[SIQtys] = None,
channelNames: List[str] = None,
qtys: List[SIQtys]=None,
channelNames: List[str]=None,
force=False,
) -> Measurement:
"""
@ -818,8 +1120,9 @@ class Measurement:
force: If True, overwrites existing files with specified `mfn`
name.
"""
if os.path.splitext(mfn)[1] != ".h5":
mfn += ".h5"
if os.path.splitext(mfn)[1] != DOTMEXT:
mfn += DOTMEXT
if os.path.exists(mfn) and not force:
raise ValueError(f"File {mfn} already exist.")
if timestamp is None:
@ -850,8 +1153,6 @@ class Measurement:
if len(qtys) != nchannels:
raise RuntimeError("Illegal length of qtys list given")
qtyvals = [qty.value for qty in qtys]
with h5.File(mfn, "w") as hf:
hf.attrs["samplerate"] = samplerate
hf.attrs["sensitivity"] = sensitivity
@ -860,7 +1161,7 @@ class Measurement:
hf.attrs["nchannels"] = nchannels
# Add physical quantity indices
hf.attrs["qtys_enum_idx"] = [qtyval.toInt() for qtyval in qtyvals]
hf.attrs['qtys_enum_idx'] = [qty.toInt() for qty in qtys]
# Add channel names in case given
if channelNames is not None:
@ -916,3 +1217,4 @@ class Measurement:
ad[0] = data
return Measurement(newfn)

View File

@ -3,9 +3,11 @@ Provides class MeasurementSet, a class used to perform checks and adjustments
on a group of measurements at the same time.
"""
__all__ = ['MeasurementSet']
from .lasp_measurement import Measurement
__all__ = ["MeasurementSet"]
from .lasp_measurement import Measurement, MeasurementType
from typing import List
import time
class MeasurementSet(list):
@ -14,7 +16,8 @@ class MeasurementSet(list):
is used to operate on multiple measurements at once.
"""
def __init__(self, mlist: List[Measurement] =[]):
def __init__(self, mlist: List[Measurement] = []):
"""
Initialize a measurement set
@ -23,10 +26,65 @@ class MeasurementSet(list):
"""
if any([not isinstance(i, Measurement) for i in mlist]):
raise TypeError('Object in list should be of Measurement type')
raise TypeError("Object in list should be of Measurement type")
# Sort by time stamp, otherwise the order is random
mlist.sort(key=lambda x: x.time, reverse=True)
super().__init__(mlist)
def getNewestReferenceMeasurement(self, mtype: MeasurementType):
"""Return the newest (in time) measurement in the current list of a certain type. Returns None in case no measurement could be found.
Args:
mtype (MeasurementType): The type required.
"""
mnewest = None
for m in self:
if m.measurementType() == mtype:
if mnewest is None:
mnewest = m
else:
if mnewest.time < m.time:
mnewest = m
return mnewest
def getReferenceMeasurements(self, mtype: MeasurementType):
"""Get all available reference measurements of a certain type in the
current set.
Args:
mtype (MeasurementType): The type of which to list
Returns:
a new measurement set including all measurements of a certain type
"""
return [m for m in self if m.measurementType() == mtype]
def getNewestReferenceMeasurements(self):
"""Returns a dictionary with newest measurement of each type that is not specific returns None in case no measurement is found."""
newest = {}
for m in self:
mtype = m.measurementType()
if mtype == MeasurementType.NotSpecific:
continue
if not mtype in newest:
newest[mtype] = m
else:
if m.time > newest[mtype].time:
newest[mtype] = m
return newest
def newestReferenceOlderThan(self, secs):
"""Returns a dictionary of references with the newest reference, that is still
older than `secs` seconds."""
curtime = time.time()
newest = self.getNewestReferenceMeasurements()
newest_older_than = {}
for key, m in newest.items():
if curtime - m.time >= secs:
newest_older_than[key] = m
return newest_older_than
def measTimeSame(self):
"""
Returns True if all measurements have the same measurement
@ -50,7 +108,6 @@ class MeasurementSet(list):
"""
return self.measTimeSame() and self.measChannelsSame()
def measChannelsSame(self):
"""
This method is used to check whether a set of measurements can be
@ -63,4 +120,3 @@ class MeasurementSet(list):
return all([first == meas.channelConfig for meas in self])
else:
return False

View File

@ -7,8 +7,15 @@ import dataclasses, logging, os, time, h5py, threading
import numpy as np
from .lasp_atomic import Atomic
from enum import Enum, auto, unique
from .lasp_cpp import InDataHandler, StreamMgr
from .lasp_version import LASP_VERSION_MAJOR, LASP_VERSION_MINOR
import uuid
import logging
logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)
logger.setLevel(logging.INFO)
@dataclasses.dataclass
@ -17,6 +24,16 @@ class RecordStatus:
done: bool = False
class RecordingState(Enum):
"""Enumeration for the recording state"""
Waiting = auto()
Recording = auto()
AllDataStored = auto()
Finished = auto()
Error = auto()
class Recording:
"""
Class used to perform a recording. Recording data can come in from a
@ -48,87 +65,104 @@ class Recording:
startDelay: Optional delay added before the recording is *actually*
started in [s].
"""
logger.debug("__init__()")
ext = ".h5"
if ext not in fn:
fn += ext
self.smgr = streammgr
self.metadata = None
if os.path.exists(fn):
raise RuntimeError("Recording file name already exists / is in use")
self._smgr = streammgr
self._metadata = None
self._recState = RecordingState.Waiting
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
self._startDelay = startDelay
# The amount of seconds (float) that is to be recorded
self.rectime = rectime
self._requiredRecordingLength = rectime
# The file name to store data to
self.fn = fn
self._fn = fn
self.curT_rounded_to_seconds = 0
# Counter of the number of blocks that have been recorded
self._recordedBlocks = 0
# Counter of the number of blocks
self.ablockno = Atomic(0)
# Counter of the overall number of blocks that have passed (including
# the blocks that passed during waiting prior to recording)
self._allBlocks = 0
# Stop flag, set when recording is finished.
self.stop = Atomic(False)
self._stop = Atomic(False)
# Mutex, on who is working with the H5py data
self.file_mtx = threading.Lock()
# Mutex, on who is working with the H5py data and the class settings
self._rec_mutex = threading.RLock()
self.progressCallback = progressCallback
self._progressCallback = progressCallback
try:
# Open the file
self.f = h5py.File(self.fn, "w", "stdio")
self.f.flush()
self._h5file = h5py.File(self._fn, "w", "stdio")
self._h5file.flush()
except Exception as e:
logging.error(f"Error creating measurement file {e}")
logger.error(f"Error creating measurement file {e}")
raise
# This flag is used to delete the file on finish(), and can be used
# when a recording is canceled.
self.deleteFile = False
# when a recording is canceled. It is set to True at start, as the file will be deleted when no data is in it.
self._deleteFile = True
# 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"
)
raise RuntimeError("Stream is not running properly. Cannot start recording")
self.ad = None
# Audio dataset
self._ad = None
logging.debug("Starting record....")
logger.debug("Starting record....")
self.indh = InDataHandler(streammgr, self.inCallback, self.resetCallback)
# In the PyInDataHandler, a weak reference is stored to the python
# methods reset and incallback. One way or another, the weak ref is gone
# on the callback thread. If we store an "extra" ref to this method over
# here, the weak ref stays alive. We do not know whether this is a bug
# or a feature, but in any case storing this extra ref to inCallback
# solves the problem.
self._incalback_cpy = self.inCallback
self._indataHandler = InDataHandler(
streammgr, self._incalback_cpy, self.resetCallback
)
if wait:
logging.debug("Stop recording with CTRL-C")
logger.debug("Stop recording with CTRL-C")
try:
while not self.stop():
while not self._stop():
time.sleep(0.01)
except KeyboardInterrupt:
logging.debug("Keyboard interrupt on record")
logger.debug("Keyboard interrupt on record")
finally:
self.finish()
def curT(self):
"""Return currently recorded time as float"""
def resetCallback(self, daq):
"""
Function called with initial stream data.
"""
with self.file_mtx:
logger.debug(f"resetCallback({daq})")
with self._rec_mutex:
in_ch = daq.enabledInChannels()
blocksize = daq.framesPerBlock()
self.blocksize = blocksize
self.nchannels = daq.neninchannels()
self.fs = daq.samplerate()
self._blocksize = blocksize
self._nchannels = daq.neninchannels()
self._fs = daq.samplerate()
f = self.f
f = self._h5file
f.attrs["LASP_VERSION_MAJOR"] = LASP_VERSION_MAJOR
f.attrs["LASP_VERSION_MINOR"] = LASP_VERSION_MINOR
@ -139,11 +173,12 @@ class Recording:
f.attrs["blocksize"] = blocksize
f.attrs["sensitivity"] = [ch.sensitivity for ch in in_ch]
f.attrs["channelNames"] = [ch.name for ch in in_ch]
f.attrs["UUID"] = str(uuid.uuid1())
# Add the start delay here, as firstFrames() is called right after the
# constructor is called. time.time() returns a floating point
# number of seconds after epoch.
f.attrs["time"] = time.time() + self.startDelay
f.attrs["time"] = time.time() + self._startDelay
# In V2, we do not store JSON metadata anymore, but just an enumeration
# index to a physical quantity.
@ -154,34 +189,6 @@ class Recording:
# f.attrs['qtys'] = [ch.qty.to_json() for ch in in_ch]
f.flush()
def firstFrames(self, adata):
"""
Set up the dataset in which to store the audio data. This will create
the attribute `self.ad`
Args:
adata: Numpy array with data from DAQ
"""
# The array data type cannot
# datatype = daq.dataType()
dtype = np.dtype(adata.dtype)
self.ad = self.f.create_dataset(
"audio",
(1, self.blocksize, self.nchannels),
dtype=dtype,
maxshape=(
None, # This means, we can add blocks
# indefinitely
self.blocksize,
self.nchannels,
),
compression="gzip",
)
self.f.flush()
def inCallback(self, adata):
"""
This method is called when a block of audio data from the stream is
@ -190,18 +197,91 @@ class Recording:
When returning False, it will stop the stream.
"""
if self.stop():
logging.debug("Stop flag set, early return in inCallback")
logger.debug(f"inCallback()")
if self._stop():
logger.debug("Stop flag set, early return in inCallback")
# Stop flag is raised. We do not add any data anymore.
return False
with self._rec_mutex:
self._allBlocks += 1
match self._recState:
case RecordingState.Waiting:
if self._allBlocks * self._blocksize / self._fs > self._startDelay:
self._recState = RecordingState.Recording
case RecordingState.Recording:
if self._ad is None:
self.__addFirstFramesToFile(adata)
else:
self.__addTimeDataToFile(adata)
# Increase the block counter
self._recordedBlocks += 1
recstatus = RecordStatus(curT=self.recordedTime, done=False)
if (
self._requiredRecordingLength is not None
and self.recordedTime >= self._requiredRecordingLength
):
self._recState = RecordingState.AllDataStored
self._stop <<= True
recstatus.done = True
if self._progressCallback is not None:
self._progressCallback(recstatus)
case RecordingState.AllDataStored:
return False
case RecordingState.Finished:
return False
return True
with self.file_mtx:
@property
def recordedTime(self):
"""Return recorded time (not rounded) as float"""
with self._rec_mutex:
if self._ad is None:
return 0.0
return self._recordedBlocks * self._blocksize / self._fs
if self.ad is None:
self.firstFrames(adata)
def __addFirstFramesToFile(self, adata):
"""
Set up the dataset in which to store the audio data. This will create
the attribute `self.ad` and flip around the _deleteFile flag.
self.__addTimeData(adata)
return True
Args:
adata: Numpy array with data from DAQ
"""
with self._rec_mutex:
# The array data type cannot
# datatype = daq.dataType()
dtype = np.dtype(adata.dtype)
assert self._ad is None
self._ad = self._h5file.create_dataset(
"audio",
(1, self._blocksize, self._nchannels),
dtype=dtype,
maxshape=(
None, # This means, we can add blocks
# indefinitely
self._blocksize,
self._nchannels,
),
compression="gzip",
)
self._ad[0, :, :] = adata
self._h5file.flush()
self._deleteFile = False
def setDelete(self, val: bool):
"""
@ -209,8 +289,8 @@ class Recording:
the recording. Typically used for cleaning up after canceling a
recording.
"""
with self.file_mtx:
self.deleteFile = val
with self._rec_mutex:
self._deleteFile = val
def finish(self):
"""
@ -218,88 +298,57 @@ class Recording:
remove the queue from the stream, etc.
"""
logging.debug("Recording::finish()")
logger.debug("Recording::finish()")
self.stop <<= True
self._stop <<= True
with self._rec_mutex:
if self._recState == RecordingState.Finished:
raise RuntimeError("Recording has already finished")
with self.file_mtx:
self.f.flush()
# Remove indata handler, which also should remove callback function
# from StreamMgr. This, however does not have to happen
# instantaneously. For which we have to implement extra mutex
# guards in this class
del self.indh
self.indh = None
del self._indataHandler
self._h5file.flush()
# Remove handle to dataset otherwise the h5 file is not closed
# properly.
del self.ad
self.ad = None
del self._ad
try:
# Close the recording file
self.f.close()
del self.f
self._h5file.close()
del self._h5file
except Exception as e:
logging.error(f"Error closing file: {e}")
logger.error(f"Error closing file: {e}")
logging.debug("Recording ended")
if self.deleteFile:
logger.debug("Recording ended")
if self._deleteFile:
self.__deleteFile()
self._recState = RecordingState.Finished
def __deleteFile(self):
"""
Cleanup the recording file.
"""
try:
os.remove(self.fn)
os.remove(self._fn)
except Exception as e:
logging.error(f"Error deleting file: {self.fn}: {str(e)}")
logger.error(f"Error deleting file: {self._fn}: {str(e)}")
def __addTimeData(self, indata):
def __addTimeDataToFile(self, indata):
"""
Called by handleQueue() and adds new time data to the storage file.
"""
# logging.debug('Recording::__addTimeData()')
with self._rec_mutex:
curT = self.ablockno() * self.blocksize / self.fs
ablockno = self._recordedBlocks
# 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
self.f.flush()
# Add the data to the file, and resize the audio data blocks
self._ad.resize(ablockno + 1, axis=0)
self._ad[ablockno, :, :] = indata
self._h5file.flush()

View File

@ -57,6 +57,9 @@ class SLM:
level_ref_value: Reference value for computing the levels in dB
offset_t: Offset to be added to output time data [s]
"""
if tw == TimeWeighting.ufast:
raise RuntimeError('The current implementation of impulse time weigthing is incorrect, as it does not have the proper decay time constant.')
self.fbdesigner = fbdesigner
if xmin is None and fbdesigner is not None:

View File

@ -20,6 +20,7 @@ from enum import Enum, unique
import copy
import numpy as np
from numpy import log2, pi, sin
from ..lasp_cpp import freqSmooth
@unique
@ -152,7 +153,7 @@ def smoothCalcMatrix(freq, sw: SmoothingWidth):
return Q
def smoothSpectralData(freq, M, sw: SmoothingWidth,
def smoothSpectralData_old(freq, M, sw: SmoothingWidth,
st: SmoothingType = SmoothingType.levels):
"""
Apply fractional octave smoothing to data in the frequency domain.
@ -220,6 +221,45 @@ def smoothSpectralData(freq, M, sw: SmoothingWidth,
return Psm
def smoothSpectralData(freq, M, sw: SmoothingWidth,
st: SmoothingType = SmoothingType.levels):
"""
Apply fractional octave smoothing to data in the frequency domain.
Args:
freq: array of frequencies of data points [Hz] - equally spaced
M: array of data, either power or dB
the smoothing type `st`, the smoothing is applied.
sw: smoothing width
st: smoothing type = data type of input data
Returns:
freq : array frequencies of data points [Hz]
Msm : float smoothed magnitude of data points
"""
# Safety
if st == SmoothingType.ps:
assert np.min(M) >= 0, 'Power spectrum values cannot be negative'
if st == SmoothingType.levels and isinstance(M.dtype, complex):
raise RuntimeError('Decibel input should be real-valued')
# Convert to power
if st == SmoothingType.levels:
P = 10**(M/10)
elif st == SmoothingType.ps:
P = M
else:
raise RuntimeError(f"Incorrect SmoothingType: {st}")
Psm = freqSmooth(freq, P, sw.value[0])
# Convert to original format
if st == SmoothingType.levels:
Psm = 10*np.log10(Psm)
return Psm
# %% Test
if __name__ == "__main__":

View File

@ -1,11 +1,9 @@
#!/bin/bash
#
cmake . -G"Ninja" -DLASP_HAS_ULDAQ=OFF -DPython3_ROOT_DIR=C:\\winpython\\python-3.10.9.amd64
arch=ucrt64
#arch=mingw64
arch=mingw64
# DLL's that are required by lasp_cpp
files_to_cpy="libfftw3-3.dll libgcc_s_seh-1.dll libgfortran-5.dll libgomp-1.dll libopenblas.dll libquadmath-0.dll libstdc++-6.dll libwinpthread-1.dll"
for fn in ${files_to_cpy}; do
cp /c/msys64/${arch}/bin/${fn} src/lasp
cp /c/msys64/${arch}/bin/${fn} python_src/lasp
done

View File

@ -5,8 +5,8 @@
if [ -z $CI ]; then
PACMAN_OPTIONS="--needed --noconfirm"
fi
# arch=mingw-w64-x86_64
arch=mingw-w64-ucrt-x86_64
arch=mingw-w64-x86_64
# arch=mingw-w64-ucrt-x86_64
pacman -S ${PACMAN_OPTIONS} make

@ -1 +1 @@
Subproject commit b6e810f2d33bcc234d67db5277d027949fec82f8
Subproject commit 614f2a9c68998b11010dc1734a77f84fcbd6fa2d

@ -1 +1 @@
Subproject commit cb8d3dcbc6fa74c67f3e236be89b12d5630da141
Subproject commit 88ab584e7bf4358599744cd662cfbc978f41efbf

2
third_party/rtaudio vendored

@ -1 +1 @@
Subproject commit 46b01b5b134f33d8ddc3dab76829d4b1350e0522
Subproject commit b4f04903312e0e0efffbe77655172e0f060dc085