diff --git a/.cmake-format.py b/.cmake-format.py new file mode 100644 index 0000000..1f3853f --- /dev/null +++ b/.cmake-format.py @@ -0,0 +1,242 @@ +# ---------------------------------- +# Options affecting listfile parsing +# ---------------------------------- +with section("parse"): + + # Specify structure for custom cmake functions + additional_commands = { 'foo': { 'flags': ['BAR', 'BAZ'], + 'kwargs': {'DEPENDS': '*', 'HEADERS': '*', 'SOURCES': '*'}}} + + # Override configurations per-command where available + override_spec = {} + + # Specify variable tags. + vartags = [] + + # Specify property tags. + proptags = [] + +# ----------------------------- +# Options affecting formatting. +# ----------------------------- +with section("format"): + + # Disable formatting entirely, making cmake-format a no-op + disable = False + + # How wide to allow formatted cmake files + # line_width = 80 + line_width = 72 + + # How many spaces to tab for indent + tab_size = 2 + + # If true, lines are indented using tab characters (utf-8 0x09) instead of + # space characters (utf-8 0x20). In cases where the layout would + # require a fractional tab character, the behavior of the fractional + # indentation is governed by + use_tabchars = False + + # If is True, then the value of this variable indicates how + # fractional indentions are handled during whitespace replacement. If set to + # 'use-space', fractional indentation is left as spaces (utf-8 0x20). If set + # to `round-up` fractional indentation is replaced with a single tab character + # (utf-8 0x09) effectively shifting the column to the next tabstop + fractional_tab_policy = 'use-space' + + # If an argument group contains more than this many sub-groups (parg or kwarg + # groups) then force it to a vertical layout. + max_subgroups_hwrap = 2 + + # If a positional argument group contains more than this many arguments, then + # force it to a vertical layout. + max_pargs_hwrap = 6 + + # If a cmdline positional group consumes more than this many lines without + # nesting, then invalidate the layout (and nest) + max_rows_cmdline = 2 + + # If true, separate flow control names from their parentheses with a space + separate_ctrl_name_with_space = False + + # If true, separate function names from parentheses with a space + separate_fn_name_with_space = False + + # If a statement is wrapped to more than one line, than dangle the closing + # parenthesis on its own line. + dangle_parens = False + + # If the trailing parenthesis must be 'dangled' on its on line, then align it + # to this reference: `prefix`: the start of the statement, `prefix-indent`: + # the start of the statement, plus one indentation level, `child`: align to + # the column of the arguments + dangle_align = 'prefix' + + # If the statement spelling length (including space and parenthesis) is + # smaller than this amount, then force reject nested layouts. + min_prefix_chars = 4 + + # If the statement spelling length (including space and parenthesis) is larger + # than the tab width by more than this amount, then force reject un-nested + # layouts. + max_prefix_chars = 10 + + # If a candidate layout is wrapped horizontally but it exceeds this many + # lines, then reject the layout. + max_lines_hwrap = 2 + + # What style line endings to use in the output. + line_ending = 'unix' + + # Format command names consistently as 'lower' or 'upper' case + command_case = 'canonical' + + # Format keywords consistently as 'lower' or 'upper' case + keyword_case = 'unchanged' + + # A list of command names which should always be wrapped + always_wrap = [] + + # If true, the argument lists which are known to be sortable will be sorted + # lexicographicall + enable_sort = True + + # If true, the parsers may infer whether or not an argument list is sortable + # (without annotation). + autosort = False + + # By default, if cmake-format cannot successfully fit everything into the + # desired linewidth it will apply the last, most agressive attempt that it + # made. If this flag is True, however, cmake-format will print error, exit + # with non-zero status code, and write-out nothing + require_valid_layout = False + + # A dictionary mapping layout nodes to a list of wrap decisions. See the + # documentation for more information. + layout_passes = {} + +# ------------------------------------------------ +# Options affecting comment reflow and formatting. +# ------------------------------------------------ +with section("markup"): + + # What character to use for bulleted lists + bullet_char = '*' + + # What character to use as punctuation after numerals in an enumerated list + enum_char = '.' + + # If comment markup is enabled, don't reflow the first comment block in each + # listfile. Use this to preserve formatting of your copyright/license + # statements. + first_comment_is_literal = False + + # If comment markup is enabled, don't reflow any comment block which matches + # this (regex) pattern. Default is `None` (disabled). + literal_comment_pattern = None + + # Regular expression to match preformat fences in comments default= + # ``r'^\s*([`~]{3}[`~]*)(.*)$'`` + fence_pattern = '^\\s*([`~]{3}[`~]*)(.*)$' + + # Regular expression to match rulers in comments default= + # ``r'^\s*[^\w\s]{3}.*[^\w\s]{3}$'`` + ruler_pattern = '^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$' + + # If a comment line matches starts with this pattern then it is explicitly a + # trailing comment for the preceeding argument. Default is '#<' + explicit_trailing_pattern = '#<' + + # If a comment line starts with at least this many consecutive hash + # characters, then don't lstrip() them off. This allows for lazy hash rulers + # where the first hash char is not separated by space + hashruler_min_length = 10 + + # If true, then insert a space between the first hash char and remaining hash + # chars in a hash ruler, and normalize its length to fill the column + canonicalize_hashrulers = True + + # enable comment markup parsing and reflow + enable_markup = True + +# ---------------------------- +# Options affecting the linter +# ---------------------------- +with section("lint"): + + # a list of lint codes to disable + disabled_codes = [] + + # regular expression pattern describing valid function names + function_pattern = '[0-9a-z_]+' + + # regular expression pattern describing valid macro names + macro_pattern = '[0-9A-Z_]+' + + # regular expression pattern describing valid names for variables with global + # (cache) scope + global_var_pattern = '[A-Z][0-9A-Z_]+' + + # regular expression pattern describing valid names for variables with global + # scope (but internal semantic) + internal_var_pattern = '_[A-Z][0-9A-Z_]+' + + # regular expression pattern describing valid names for variables with local + # scope + local_var_pattern = '[a-z][a-z0-9_]+' + + # regular expression pattern describing valid names for privatedirectory + # variables + private_var_pattern = '_[0-9a-z_]+' + + # regular expression pattern describing valid names for public directory + # variables + public_var_pattern = '[A-Z][0-9A-Z_]+' + + # regular expression pattern describing valid names for function/macro + # arguments and loop variables. + argument_var_pattern = '[a-z][a-z0-9_]+' + + # regular expression pattern describing valid names for keywords used in + # functions or macros + keyword_pattern = '[A-Z][0-9A-Z_]+' + + # In the heuristic for C0201, how many conditionals to match within a loop in + # before considering the loop a parser. + max_conditionals_custom_parser = 2 + + # Require at least this many newlines between statements + min_statement_spacing = 1 + + # Require no more than this many newlines between statements + max_statement_spacing = 2 + max_returns = 6 + max_branches = 12 + max_arguments = 5 + max_localvars = 15 + max_statements = 50 + +# ------------------------------- +# Options affecting file encoding +# ------------------------------- +with section("encode"): + + # If true, emit the unicode byte-order mark (BOM) at the start of the file + emit_byteorder_mark = False + + # Specify the encoding of the input file. Defaults to utf-8 + input_encoding = 'utf-8' + + # Specify the encoding of the output file. Defaults to utf-8. Note that cmake + # only claims to support utf-8 so be careful when using anything else + output_encoding = 'utf-8' + +# ------------------------------------- +# Miscellaneous configurations options. +# ------------------------------------- +with section("misc"): + + # A dictionary containing any per-command configuration overrides. Currently + # only `command_case` is supported. + per_command = {} + diff --git a/CHANGES.md b/CHANGES.md index 7520939..0c74bd9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,10 +16,10 @@ The following Git repository is required: ## Platforms tested -* Mac mini 2023 Apple Silicon (M2 Pro), macOS 14.2.1, Xcode 15.1 Command Line Tools -* MacBook Air 13" Apple Silicon (M1) 2020, macOS 14.2.1, Xcode 15.1 Command Line Tools -* Ubuntu 22.04.3 LTS x86\_64, gcc 12.3.0 -* (still experimental) Raspberry Pi 4, running Raspberry Pi OS aka Raspbian GNU/Linux 11 (bullseye) +* Mac mini 2023 Apple Silicon (M2 Pro), macOS 14.4, Xcode 15.3 Command Line Tools +* MacBook Air 13" Apple Silicon (M1) 2020, macOS 14.4, Xcode 15.3 Command Line Tools +* Ubuntu 22.04.4 LTS x86\_64, gcc 12.3.0 +* Raspberry Pi 4 with Raspberry Pi OS 64bit Lite (Debian Bookworm) ## Features under development @@ -27,8 +27,10 @@ The following Git repository is required: ## Known limitations +* libsndfile 1.1 or later must be installed to support MP3 file output. * For Raspberry Pi 3 and 4, Airspy R2 10Msps and Airspy Mini 6Msps sampling rates are *not supported* due to the hardware limitation. Use in 2.5Msps for R2, 3Msps for Mini. * Since 20231227-0, the buffer length option `-b` is no longer handled and will generate an error. The audio sample data sent to AudioOutput base classes are no longer pre-buffered. +* The author observed anomalies of being unable to run PortAudio with the `snd_aloop` loopback device while testing on Raspberry Pi OS 32bit Debian *Bullseye*. Portaudio anomaly support is out of our development scope. ### Intel Mac support is dropped @@ -36,38 +38,54 @@ Intel Mac hardware is no longer supported by airspy-fmradion, although the autho ## Changes (including requirement changes) +* 20240316-0: Made the following changes: + * Raspberry Pi 4 with Raspberry Pi OS 64bit lite is now officially tested. + * *Note well: Raspberry Pi OS 32bit is not supported*. + * [`-A` AFC option is removed.](https://github.com/jj1bdx/airspy-fmradion/pull/70) + * [Change VOLK version display format.](https://github.com/jj1bdx/airspy-fmradion/pull/71) + * [Documentation update](https://github.com/jj1bdx/airspy-fmradion/pull/72): + * Reduce text length of README.md. + * Old README.md is now located at [`doc/old-README-until-2023.md`](doc/old-README-until-2023.md). + * [For PortAudio, the minimum output latency is explicitly set to 40 milliseconds.](https://github.com/jj1bdx/airspy-fmradion/pull/73) + * [Use libsndfile MP3 output capability to generate the MP3 file directly as the audio output, when supported.](https://github.com/jj1bdx/airspy-fmradion/pull/74) + * libsndfile 1.1 or later is required for the MP3 support. + * A conditional compilation flag `LIBSNDFILE_MP3_ENABLED`, set by cmake, is introduced. + * See [`libsndfile.md`](libsndfile.md) for how to installing the latest libsndfile, suggested for Ubuntu 22.04.4 LTS. + * [See also the related GitHub issue.](https://github.com/jj1bdx/airspy-fmradion/issues/47) + * Apply [cmake-format](https://github.com/cheshirekow/cmake_format) for `CMakeLists.txt`. + * Default style: `.cmake-format.py` * 20240107-0: Made the following changes: - - For broadcasting FM, show stereo 19kHz pilot signal level when detected. - - Remove displaying whether FM stereo pilot signal level is stable or unstable. - - Add Git info into the binary program built, with [cmake-git-version-tracking](https://github.com/andrew-hardin/cmake-git-version-tracking.git) (using jj1bdx's fork). - - Add compile command database support on CMakeLists.txt. - - Cleaned up old documents. - - Fixed the following bugs detected by clang-tidy: + * For broadcasting FM, show stereo 19kHz pilot signal level when detected. + * Remove displaying whether FM stereo pilot signal level is stable or unstable. + * Add Git info into the binary program built, with [cmake-git-version-tracking](https://github.com/andrew-hardin/cmake-git-version-tracking.git) (using jj1bdx's fork). + * Add compile command database support on CMakeLists.txt. + * Cleaned up old documents. + * Fixed the following bugs detected by clang-tidy: * [ERR34-C. Detect errors when converting a string to a number](https://wiki.sei.cmu.edu/confluence/display/c/ERR34-C.+Detect+errors+when+converting+a+string+to+a+number) - - Use `Utility::parse_int()` instead of raw `atoi()` + * Use `Utility::parse_int()` instead of raw `atoi()` * [DCL51-CPP. Do not declare or define a reserved identifier](https://wiki.sei.cmu.edu/confluence/display/cplusplus/DCL51-CPP.+Do+not+declare+or+define+a+reserved+identifier) - - Remove unused `_FILE_OFFSET_BITS` - - Fixed the bug of FileSource playback: the code did not terminate after the end of playback. - - main.cpp: add checking pull_end_reached() in the main loop. - - Set RtlSdrSource's default_block_length from 65536 to 16384, to prevent popping cracking sound (observed on Mac mini 2023). - - stat_rate calculation is redesigned by observation of actual SDR units (:i.e., Airspy HF+, Airspy R2, and RTL-SDR). + * Remove unused `_FILE_OFFSET_BITS` + * Fixed the bug of FileSource playback: the code did not terminate after the end of playback. + * main.cpp: add checking pull_end_reached() in the main loop. + * Set RtlSdrSource's default_block_length from 65536 to 16384, to prevent popping cracking sound (observed on Mac mini 2023). + * stat_rate calculation is redesigned by observation of actual SDR units (:i.e., Airspy HF+, Airspy R2, and RTL-SDR). * 20231227-0: Made the following changes: - - Split class PilotPhaseLock from FmDecode. - - Removed submodule readerwriterqueue. - - Re-introduced DataBuffer from commit . - - PhaseDiscriminator now contains NaN-removal code. - - Introduced accurate m_pilot_level computation for PilotPhaseLock. - - Introduced enum PilotState and the state machine for more precisely showing stereo pilot signal detection and the signal levels. - - Removed buffer option `-b` and `--buffer` finally. + * Split class PilotPhaseLock from FmDecode. + * Removed submodule readerwriterqueue. + * Re-introduced DataBuffer from commit . + * PhaseDiscriminator now contains NaN-removal code. + * Introduced accurate m_pilot_level computation for PilotPhaseLock. + * Introduced enum PilotState and the state machine for more precisely showing stereo pilot signal detection and the signal levels. + * Removed buffer option `-b` and `--buffer` finally. * 20231216-0: Removed recording buffer thread. This will simplify the audio output operation. Also, lowered the output level of AM/CW/USB/LSB/WSPR decoder to prevent audio clipping, and changed the IF AGC constants for longer transition timing. * 20231215-0: Fix the following known bugs and refactor the code to streamline the functioning: - - Bug: a hung process during the startup period before valid audio signals are coming out - - Bug: displaying `-nan` in the output level meter in broadcast FM and NBFM - - The NaN is presumably generated by volk_32fc_s32f_atan2_32f() in PhaseDiscriminator::process() - - This NaN issue was presumably the root cause of the multipath filter anomaly first fixed in 20231213-1 - - Enhancement: streamlining processing flow in the main for loop of `main()` - - Enhancement: removing the initial waiting period for startup; the output is now activated from the block number 1 - - Utility addition: adding `Utility::remove_nans()`, a function to check and substitute NaNs and infinity values in IQSamplesDecodedVector + * Bug: a hung process during the startup period before valid audio signals are coming out + * Bug: displaying `-nan` in the output level meter in broadcast FM and NBFM + * The NaN is presumably generated by volk_32fc_s32f_atan2_32f() in PhaseDiscriminator::process() + * This NaN issue was presumably the root cause of the multipath filter anomaly first fixed in 20231213-1 + * Enhancement: streamlining processing flow in the main for loop of `main()` + * Enhancement: removing the initial waiting period for startup; the output is now activated from the block number 1 + * Utility addition: adding `Utility::remove_nans()`, a function to check and substitute NaNs and infinity values in IQSamplesDecodedVector * 20231213-1: Fixed a NaN issue caused by 0+0j (true zero) output of the multipath filter; the true zero output now causes resetting the filter. This is presumably also one of the reasons that caused the audio disruption issue in 20231212-1 and 20231213-0. * 20231213-0: Fixed an uninitialized variable `m_save_phase` in PhaseDiscriminator as in [the pull request](https://github.com/jj1bdx/airspy-fmradion/pull/43) by Clayton Smith. * 20231212-1: FAILED: tried to make API compatible with [VOLK 3.1.0 change for s32fc functions](https://github.com/gnuradio/volk/pull/695), for `volk_32fc_x2_s32fc_multiply_conjugate_add_32fc()`, but this didn't work on Ubuntu 22.04.3. @@ -75,7 +93,7 @@ Intel Mac hardware is no longer supported by airspy-fmradion, although the autho * 20230923: failed changes: low latency setting for buffering-based PortAudio didn't work well. Discarded changes of 20230910-1 to 20230910-4 from the dev branch. * 20230910-0: Updated r8brain-free-src to Version 6.4. * 20230528-2: DataBuffer class is reimplemented as a wrapper of `moodycamel::BlockReaderWriterQueue`, which allows efficient blocking operation and removes the requirements of busy waiting by using `moodycamel::BlockReaderWriterQueue::wait_dequeue()`. -* 20230528-1: DataBuffer class is now implemented as a wrapper of `moodycamel::ReaderWriterQueue` class in . All lock-based synchronization functions from DataBuffer class are removed because they are no longer necessary. The repository readerwriterqueue is added as a git submodule. Also, sample length count is removed from the DataBuffer class because of their rare usage. +* 20230528-1: DataBuffer class is now implemented as a wrapper of `moodycamel::ReaderWriterQueue` class in . All lock-based synchronization functions from DataBuffer class are removed because they are no longer necessary. The repository readerwriterqueue is added as a git submodule. Also, sample length count is removed from the DataBuffer class because of their rare usage. * 20230528-1: All DataBuffer queue length measurement code in main.cpp are bundled under a compilation flag `DATABUFFER_QUEUE_MONITOR`, which is not necessary for the production code. The actual maximum queue length measured in Mac mini 2018 executions are less than 10, even when the output glitch occurs due to a higher-priority process invocation, such as a web browser. The new DataBuffer class sets the default allocated queue length to 128. * 20230526-0: Explicitly skip IF Resampler in class FmDecoder to reduce CPU usage for typical settings (i.e., IF sample rate is set to 384 ksamples/sec for Airspy HF+). * 20230430-0: Forcefully set the coefficient of the reference point of FM multipath filter to 1 + 0j (unity). This may change how the filter behaves. Field testing since 20230214-test shows no notable anomalies. @@ -130,10 +148,11 @@ Intel Mac hardware is no longer supported by airspy-fmradion, although the autho ## FYI: libusb-1.0.25 glitch * Note: This problem has been fixed by the latest implementation of Airspy HF+ driver after [this commit](https://github.com/airspy/airspyhf/commit/3b823ad8fa729358e0729e6c1ca60ac5dfcd656e). -* The author has noticed [libusb-1.0.25 on macOS 12.2 causes segfault when stopping the code with SIGINT or SIGTERM with Airspy HF+ Discovery](https://github.com/jj1bdx/airspy-fmradion/issues/35). +* The author has noticed [libusb-1.0.25 on macOS 12.2 causes segfault when stopping the code with SIGINT or SIGTERM with Airspy HF+ Discovery](https://github.com/jj1bdx/airspy-fmradion/issues/35). * A proper fix of this is to [fix the Airspy HF+ driver](https://github.com/airspy/airspyhf/pull/31). * [A similar case of SDR++ with ArchLinux](https://github.com/libusb/libusb/issues/1059#issuecomment-1030638617) is also reported. -* Since Version 20220205-0, a workaround is implemented to prevent data loss for this bug: the main() loop closes the audio output before calling the function which might cause this segmentation fault (SIGSEGV), which is the stopping function of the SDR source driver. +* Since Version 20220205-0, a workaround is implemented to prevent data loss for this bug: the main() loop closes the audio output before calling the function which might cause this segmentation fault (SIGSEGV), which is the stopping function of the SDR source driver. * Airspy R2 and Mini are not affected. Use the latest driver with [this fix](https://github.com/airspy/airspyone_host/commit/41c439f16818d931c4d0f8a620413ea5131c0bd6). * You can still use 20220205-1 if you need to; there is no functional difference between 20220205-1 and 20220206-0. +[End of CHANGES.md] diff --git a/CMakeLists.txt b/CMakeLists.txt index bef50a2..18da3d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,18 +13,20 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Use cmake-git-version-tracking include(FetchContent) -FetchContent_Declare(cmake_git_version_tracking - GIT_REPOSITORY https://github.com/jj1bdx/cmake-git-version-tracking.git - GIT_TAG 9b5fc5088b4089ff2adc20d607976b9923e3d737 -) +FetchContent_Declare( + cmake_git_version_tracking + GIT_REPOSITORY + https://github.com/jj1bdx/cmake-git-version-tracking.git + GIT_TAG 6c0cb87edd029ddfb403a8e24577c144a03605a6) FetchContent_MakeAvailable(cmake_git_version_tracking) -# EXPORT_COMPILE_COMMANDS is supported for CMake version 3.20 or later only -if (CMAKE_VERSION VERSION_LESS 3.20.0) - message(STATUS "No EXPORT_COMPILE_COMMANDS available") - message(STATUS "Use compdb for proper compile_commands.json handling") +# EXPORT_COMPILE_COMMANDS is supported for CMake version 3.20 or later +# only +if(CMAKE_VERSION VERSION_LESS 3.20.0) + message(STATUS "No EXPORT_COMPILE_COMMANDS available") + message(STATUS "Use compdb for proper compile_commands.json handling") else() - set_target_properties(cmake_git_version_tracking - PROPERTIES EXPORT_COMPILE_COMMANDS OFF) + set_target_properties(cmake_git_version_tracking + PROPERTIES EXPORT_COMPILE_COMMANDS OFF) endif() # Use pkg-config @@ -35,156 +37,185 @@ find_package(Threads) # Find Volk library. pkg_check_modules(PKG_VOLK volk) -if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - find_library(VOLK_LIBRARY libvolk.dylib - HINTS /usr/local/lib /opt/homebrew/lib - ${PKG_VOLK_LIBRARY_DIRS}) +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + find_library( + VOLK_LIBRARY libvolk.dylib HINTS /usr/local/lib /opt/homebrew/lib + ${PKG_VOLK_LIBRARY_DIRS}) else() - find_library(VOLK_LIBRARY libvolk.so - HINTS ${PKG_VOLK_LIBRARY_DIRS}) + find_library(VOLK_LIBRARY libvolk.so HINTS ${PKG_VOLK_LIBRARY_DIRS}) endif() -message(STATUS "volk library: ${VOLK_LIBRARY}") +message(STATUS "volk ${PKG_VOLK_VERSION}: ${VOLK_LIBRARY}") # Find Airspy library. pkg_check_modules(PKG_AIRSPY libairspy) -if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - find_path(AIRSPY_INCLUDE_DIR airspy.h - HINTS /usr/local/include/libairspy - /opt/homebrew/include/libairspy - ${PKG_AIRSPY_INCLUDE_DIRS}) - find_library(AIRSPY_LIBRARY libairspy.a - HINTS /usr/local/lib - /opt/homebrew/lib - ${PKG_AIRSPY_LIBRARY_DIRS}) - set(AIRSPY_INCLUDE_OPTION "-I/usr/local/include -I/opt/homebrew/include") +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + find_path( + AIRSPY_INCLUDE_DIR airspy.h + HINTS /usr/local/include/libairspy /opt/homebrew/include/libairspy + ${PKG_AIRSPY_INCLUDE_DIRS}) + find_library( + AIRSPY_LIBRARY libairspy.a HINTS /usr/local/lib /opt/homebrew/lib + ${PKG_AIRSPY_LIBRARY_DIRS}) + set(AIRSPY_INCLUDE_OPTION + "-I/usr/local/include -I/opt/homebrew/include") else() - find_path(AIRSPY_INCLUDE_DIR airspy.h - HINTS ${PKG_AIRSPY_INCLUDE_DIRS}) - find_library(AIRSPY_LIBRARY libairspy.a - HINTS ${PKG_AIRSPY_LIBRARY_DIRS}) - set(AIRSPY_INCLUDE_OPTION "") + find_path(AIRSPY_INCLUDE_DIR airspy.h + HINTS ${PKG_AIRSPY_INCLUDE_DIRS}) + find_library(AIRSPY_LIBRARY libairspy.a + HINTS ${PKG_AIRSPY_LIBRARY_DIRS}) + set(AIRSPY_INCLUDE_OPTION "") endif() -message(STATUS "libairspy: ${AIRSPY_INCLUDE_DIR}, ${AIRSPY_LIBRARY}") +message( + STATUS + "libairspy ${PKG_AIRSPY_VERSION}: ${AIRSPY_INCLUDE_DIR}, ${AIRSPY_LIBRARY}" +) # Find Airspy HF library. pkg_check_modules(PKG_AIRSPYHF libairspyhf) -if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - find_path(AIRSPYHF_INCLUDE_DIR airspyhf.h - HINTS /usr/local/include/libairspyhf - /opt/homebrew/include/libairspyhf - ${PKG_AIRSPYHF_INCLUDE_DIRS}) - find_library(AIRSPYHF_LIBRARY libairspyhf.a - HINTS /usr/local/lib - /opt/homebrew/lib - ${PKG_AIRSPYHF_LIBRARY_DIRS}) - set(AIRSPYHF_INCLUDE_OPTION "-I/usr/local/include -I/opt/homebrew/include") +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + find_path( + AIRSPYHF_INCLUDE_DIR airspyhf.h + HINTS /usr/local/include/libairspyhf + /opt/homebrew/include/libairspyhf + ${PKG_AIRSPYHF_INCLUDE_DIRS}) + find_library( + AIRSPYHF_LIBRARY libairspyhf.a + HINTS /usr/local/lib /opt/homebrew/lib ${PKG_AIRSPYHF_LIBRARY_DIRS}) + set(AIRSPYHF_INCLUDE_OPTION + "-I/usr/local/include -I/opt/homebrew/include") else() - find_path(AIRSPYHF_INCLUDE_DIR airspyhf.h - HINT ${PKG_AIRSPYHF_INCLUDE_DIRS}) - find_library(AIRSPYHF_LIBRARY libairspyhf.a - HINT ${PKG_AIRSPYHF_LIBRARY_DIRS}) - set(AIRSPYHF_INCLUDE_OPTION "") + find_path(AIRSPYHF_INCLUDE_DIR airspyhf.h HINT + ${PKG_AIRSPYHF_INCLUDE_DIRS}) + find_library(AIRSPYHF_LIBRARY libairspyhf.a HINT + ${PKG_AIRSPYHF_LIBRARY_DIRS}) + set(AIRSPYHF_INCLUDE_OPTION "") endif() -message(STATUS "libairspyhf: ${AIRSPYHF_INCLUDE_DIR}, ${AIRSPYHF_LIBRARY}") +message( + STATUS + "libairspyhf ${PKG_AIRSPYHF_VERSION}: ${AIRSPYHF_INCLUDE_DIR}, ${AIRSPYHF_LIBRARY}" +) # Find RTL-SDR library. pkg_check_modules(PKG_RTLSDR librtlsdr) -find_path(RTLSDR_INCLUDE_DIR rtl-sdr.h - HINTS ${PKG_RTLSDR_INCLUDE_DIRS}) +find_path(RTLSDR_INCLUDE_DIR rtl-sdr.h HINTS ${PKG_RTLSDR_INCLUDE_DIRS}) find_library(RTLSDR_LIBRARY librtlsdr.a - HINTS ${PKG_RTLSDR_LIBRARY_DIRS}) -message(STATUS "librtlsdr: ${RTLSDR_INCLUDE_DIR}, ${RTLSDR_LIBRARY}") + HINTS ${PKG_RTLSDR_LIBRARY_DIRS}) +message( + STATUS + "librtlsdr ${PKG_RTLSDR_VERSION}: ${RTLSDR_INCLUDE_DIR}, ${RTLSDR_LIBRARY}" +) # Find libusb -# See https://github.com/texane/stlink/blob/master/cmake/modules/FindLibUSB.cmake +# +# See +# https://github.com/texane/stlink/blob/master/cmake/modules/FindLibUSB.cmake pkg_check_modules(PKG_LIBUSB libusb-1.0) -find_path(LIBUSB_INCLUDE_DIR libusb.h - HINTS /usr /usr/local /opt /opt/homebrew ${PKG_LIBUSB_INCLUDE_DIRS} - PATH_SUFFIXES libusb-1.0) +find_path( + LIBUSB_INCLUDE_DIR libusb.h + HINTS /usr /usr/local /opt /opt/homebrew ${PKG_LIBUSB_INCLUDE_DIRS} + PATH_SUFFIXES libusb-1.0) set(LIBUSB_NAME usb-1.0) -find_library(LIBUSB_LIBRARY ${LIBUSB_NAME} - HINTS /usr /usr/local /opt /opt/homebrew ${PKG_LIBUSB_LIBRARY_DIRS}) -message(STATUS "libusb: ${LIBUSB_INCLUDE_DIR}, ${LIBUSB_LIBRARY}") - -# Find sndfile library. -FIND_LIBRARY(SNDFILE_LIBRARY sndfile) +find_library( + LIBUSB_LIBRARY ${LIBUSB_NAME} + HINTS /usr /usr/local /opt /opt/homebrew ${PKG_LIBUSB_LIBRARY_DIRS}) +message( + STATUS + "libusb ${PKG_LIBUSB_VERSION}: ${LIBUSB_INCLUDE_DIR}, ${LIBUSB_LIBRARY}" +) -FIND_PATH(SNDFILE_INCLUDE_DIR sndfile.h - HINTS ${PKG_SNDFILE_INCLUDE_DIRS}) -MESSAGE(STATUS "Sndfile: ${SNDFILE_LIBRARY}, ${SNDFILE_INCLUDE_DIR}") +# Find sndfile library +pkg_check_modules(PKG_SNDFILE sndfile) +find_path(SNDFILE_INCLUDE_DIR sndfile.h + HINTS ${PKG_SNDFILE_INCLUDE_DIRS}) +find_library(SNDFILE_LIBRARY sndfile) +message( + STATUS + "sndfile ${PKG_SNDFILE_VERSION}: ${SNDFILE_LIBRARY}, ${SNDFILE_INCLUDE_DIR}" +) +# Determine MP3 support on sndfile +if(PKG_SNDFILE_VERSION VERSION_GREATER_EQUAL "1.1") + set(SNDFILE_INCLUDE_OPTION "-DLIBSNDFILE_MP3_ENABLED") + message(STATUS "sndfile MP3 support enabled") +endif() # for PortAudio pkg_check_modules(PORTAUDIO2 portaudio-2.0) -if (PORTAUDIO2_FOUND) - set(PORTAUDIO_INCLUDE_DIRS ${PORTAUDIO2_INCLUDE_DIRS}) - set(PORTAUDIO_LIBRARY_DIRS ${PORTAUDIO2_LIBRARY_DIRS}) - set(PORTAUDIO_LINK_LIBRARIES ${PORTAUDIO2_LINK_LIBRARIES}) - set(PORTAUDIO_VERSION 19) - set(PORTAUDIO_FOUND TRUE) - find_path(PORTAUDIO_INCLUDE_DIR portaudio.h - HINTS /usr/local/include /opt/homebrew/include - ${PORTAUDIO_INCLUDE_DIRS}) - if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - find_library(PORTAUDIO_LIBRARY libportaudio.dylib - HINTS /usr/local/lib /opt/homebrew/lib - ${PORTAUDIO_LIBRARY_DIRS}) - else (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - find_library(PORTAUDIO_LIBRARY libportaudio.so - HINTS /usr/local/lib /opt/homebrew/lib - ${PORTAUDIO_LIBRARY_DIRS}) - endif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") -else (PORTAUDIO2_FOUND) - message(FATAL_ERROR "Could not find Portaudio") -endif (PORTAUDIO2_FOUND) -message(STATUS "libportaudio: ${PORTAUDIO_INCLUDE_DIR}, ${PORTAUDIO_LIBRARY}") +if(PORTAUDIO2_FOUND) + set(PORTAUDIO_INCLUDE_DIRS ${PORTAUDIO2_INCLUDE_DIRS}) + set(PORTAUDIO_LIBRARY_DIRS ${PORTAUDIO2_LIBRARY_DIRS}) + set(PORTAUDIO_LINK_LIBRARIES ${PORTAUDIO2_LINK_LIBRARIES}) + set(PORTAUDIO_VERSION 19) + set(PORTAUDIO_FOUND TRUE) + find_path(PORTAUDIO_INCLUDE_DIR portaudio.h + HINTS /usr/local/include /opt/homebrew/include + ${PORTAUDIO_INCLUDE_DIRS}) + if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + find_library( + PORTAUDIO_LIBRARY libportaudio.dylib + HINTS /usr/local/lib /opt/homebrew/lib ${PORTAUDIO_LIBRARY_DIRS}) + else(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + find_library( + PORTAUDIO_LIBRARY libportaudio.so + HINTS /usr/local/lib /opt/homebrew/lib ${PORTAUDIO_LIBRARY_DIRS}) + endif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") +else(PORTAUDIO2_FOUND) + message(FATAL_ERROR "Could not find Portaudio") +endif(PORTAUDIO2_FOUND) +message( + STATUS + "libportaudio ${PORTAUDIO2_VERSION}: ${PORTAUDIO_INCLUDE_DIR}, ${PORTAUDIO_LIBRARY}" +) message(STATUS "PortAudio libs: ${PORTAUDIO_LINK_LIBRARIES}") -if(RTLSDR_INCLUDE_DIR AND RTLSDR_LIBRARY) - message(STATUS "Found librtlsdr: ${RTLSDR_INCLUDE_DIR}, ${RTLSDR_LIBRARY}") -else() - message(WARNING "Can not find Osmocom RTL-SDR library") - message("Try again with environment variable PKG_CONFIG_PATH") - message("or with -DRTLSDR_INCLUDE_DIR=/path/rtlsdr/include") - message(" -DRTLSDR_LIBRARY=/path/rtlsdr/lib/librtlsdr.a") -endif() - set(RTLSDR_INCLUDE_DIRS ${RTLSDR_INCLUDE_DIR} ${LIBUSB_INCLUDE_DIR}) -set(RTLSDR_LIBRARIES ${RTLSDR_LIBRARY} ${LIBUSB_LIBRARY}) - +set(RTLSDR_LIBRARIES ${RTLSDR_LIBRARY} ${LIBUSB_LIBRARY}) set(AIRSPY_INCLUDE_DIRS ${AIRSPY_INCLUDE_DIR} ${LIBUSB_INCLUDE_DIR}) -set(AIRSPY_LIBRARIES ${AIRSPY_LIBRARY} ${LIBUSB_LIBRARY}) +set(AIRSPY_LIBRARIES ${AIRSPY_LIBRARY} ${LIBUSB_LIBRARY}) set(AIRSPYHF_INCLUDE_DIRS ${AIRSPYHF_INCLUDE_DIR} ${LIBUSB_INCLUDE_DIR}) -set(AIRSPYHF_LIBRARIES ${AIRSPYHF_LIBRARY} ${LIBUSB_LIBRARY}) +set(AIRSPYHF_LIBRARIES ${AIRSPYHF_LIBRARY} ${LIBUSB_LIBRARY}) -# Optimization flags and options. +# cmake-format: off ## +# Optimization flags and options. +# # Enable speed-based optimization # Do not apply -ffast-math; it enables -menable-no-nans # which cancels detection functions of multipath filter abnormality! +# set(OPTIMIZATION_FLAGS "-O3 -ftree-vectorize ") ## # Use conservative options when failed to run +# #set(OPTIMIZATION_FLAGS "-O2") ## # For vectorization analysis (in Clang only) +# #set(OPTIMIZATION_FLAGS "-O3 -ftree-vectorize -Rpass=loop-vectorize -Rpass-missed=loop-vectorize -Rpass-analysis=loop-vectorize") ## # For clang profiling with optimization +# #set(OPTIMIZATION_FLAGS "-O3 -ftree-vectorize -g -fprofile-instr-generate -fcoverage-mapping") #SET(CMAKE_EXE_LINKER_FLAGS "-fprofile-instr-generate") ## # For Sanitizers # with Thread Sanitizer +# #set(OPTIMIZATION_FLAGS "-fsanitize=thread -O3 -ftree-vectorize ") # with Address Sanitizer +# #set(OPTIMIZATION_FLAGS "-fsanitize=address -O3 -ftree-vectorize ") # For valgrind check +# #set(OPTIMIZATION_FLAGS "-g") +# ## +# cmake-format: on # Common compiler flags and options. -## -set(CMAKE_CXX_FLAGS "-Wall -std=c++17 ${OPTIMIZATION_FLAGS} ${AIRSPY_INCLUDE_OPTION} ${AIRSPYHF_INCLUDE_OPTION} ${EXTRA_FLAGS}") +# +set(CMAKE_CXX_FLAGS + "-Wall -std=c++17 ${OPTIMIZATION_FLAGS} ${AIRSPY_INCLUDE_OPTION} ${AIRSPYHF_INCLUDE_OPTION} ${SNDFILE_INCLUDE_OPTION} ${EXTRA_FLAGS}" +) # For building airspy-fmradion sources @@ -207,8 +238,7 @@ set(sfmbase_SOURCES sfmbase/NbfmDecode.cpp sfmbase/PhaseDiscriminator.cpp sfmbase/PilotPhaseLock.cpp - sfmbase/RtlSdrSource.cpp -) + sfmbase/RtlSdrSource.cpp) set(sfmbase_HEADERS include/AfSimpleAgc.h @@ -236,72 +266,54 @@ set(sfmbase_HEADERS include/RtlSdrSource.h include/Source.h include/SoftFM.h - include/Utility.h -) + include/Utility.h) +# cmake-format: off # For building r8brain-free-src # See https://github.com/baconpaul/SampleRateComparison for the details # (Licensed GPLv3) # Use R8B_PFFFT_DOUBLE for double-precision conversion +# cmake-format: on -add_library(r8b - r8brain-free-src/r8bbase.cpp - r8brain-free-src/pffft_double/pffft_double.c -) -target_include_directories(r8b PUBLIC - r8brain-free-src -) -target_compile_definitions(r8b PUBLIC - R8B_EXTFFT=1 - R8B_FASTTIMING=1 - R8B_PFFFT_DOUBLE=1 -) +add_library(r8b r8brain-free-src/r8bbase.cpp + r8brain-free-src/pffft_double/pffft_double.c) +target_include_directories(r8b PUBLIC r8brain-free-src) +target_compile_definitions(r8b PUBLIC R8B_EXTFFT=1 R8B_FASTTIMING=1 + R8B_PFFFT_DOUBLE=1) # Base sources -set(sfmbase_SOURCES - ${sfmbase_SOURCES} - ${sfmbase_HEADERS} -) +set(sfmbase_SOURCES ${sfmbase_SOURCES} ${sfmbase_HEADERS}) # Libraries -add_library(sfmbase STATIC - ${sfmbase_SOURCES} -) +add_library(sfmbase STATIC ${sfmbase_SOURCES}) # Executable -add_executable(airspy-fmradion - main.cpp -) +add_executable(airspy-fmradion main.cpp) include_directories( - ${CMAKE_SOURCE_DIR}/include - ${CMAKE_SOURCE_DIR}/r8brain-free-src - ${AIRSPYHF_INCLUDE_DIRS} - ${AIRSPY_INCLUDE_DIRS} - ${RTLSDR_INCLUDE_DIRS} - ${PORTAUDIO_INCLUDE_DIR} - ${EXTRA_INCLUDES} -) - -target_link_libraries(airspy-fmradion - sfmbase - r8b - ${CMAKE_THREAD_LIBS_INIT} - ${PORTAUDIO_LINK_LIBRARIES} - ${VOLK_LIBRARY} - ${EXTRA_LIBS} - cmake_git_version_tracking -) - -target_link_libraries(sfmbase - ${SNDFILE_LIBRARY} - ${AIRSPY_LIBRARIES} - ${AIRSPYHF_LIBRARIES} - ${RTLSDR_LIBRARIES} -) + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/r8brain-free-src + ${AIRSPYHF_INCLUDE_DIRS} + ${AIRSPY_INCLUDE_DIRS} + ${RTLSDR_INCLUDE_DIRS} + ${PORTAUDIO_INCLUDE_DIR} + ${EXTRA_INCLUDES}) + +target_link_libraries( + airspy-fmradion + sfmbase + r8b + ${CMAKE_THREAD_LIBS_INIT} + ${PORTAUDIO_LINK_LIBRARIES} + ${VOLK_LIBRARY} + ${EXTRA_LIBS} + cmake_git_version_tracking) + +target_link_libraries(sfmbase ${SNDFILE_LIBRARY} ${AIRSPY_LIBRARIES} + ${AIRSPYHF_LIBRARIES} ${RTLSDR_LIBRARIES}) # Installation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97cf1cd..eeda175 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ All documents and source code comments in the repository should be written in En * Create a topic branch from where you want to base your work. * Use the tagged branches as the starting point. * Make commits separated by logical units. -* Apply the proper C++ coding style. +* Apply the suggested coding styles listed in this document. * Make sure your commit messages are in the proper format. * Push your changes to a topic branch in your fork of the repository. * Submit a pull request to jj1bdx/airspy-fmradion. @@ -47,6 +47,19 @@ The coding style is defined in the file `.clang-format`. To follow this style, d clang-format -i main.cpp include/*.h sfmbase/*.cpp ``` +## CMakeLists.txt coding style + +Use cmake-format, a part of [cmake language tools](https://cmake-format.readthedocs.io/en/latest/). +The style is defined in the file `.cmake-format.py`. +To follow this style, do the following: + +* Install cmake language tools. +* Run the following command at the top directory of the repository: + +``` +cmake-format -i CMakeLists.txt +``` + ## License airspy-fmradion is GPLv3 licensed. Do not include the code which is not following the license. diff --git a/README.md b/README.md index 93a1a14..d02d64d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # airspy-fmradion -* Version 20240107-0 +* Version 20240316-0 * For macOS (Apple Silicon) and Linux ## Contributing @@ -11,96 +11,84 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the details. ## Known issues and changes -Please read [CHANGES.md](CHANGES.md) before using the software. +* Please read [CHANGES.md](CHANGES.md) before using the software. +* Refer to [doc/old-README-until-2023.md](doc/old-README-until-2023.md) for the other miscellaneous technical details. ## What is airspy-fmradion? -* **airspy-fmradion** is a software-defined radio receiver for FM and AM broadcast radio, and also NBFM/DSB/USB/LSB/CW/WSPR utility communications, specifically designed for Airspy R2, Airspy Mini, Airspy HF+, and RTL-SDR. +* **airspy-fmradion** is software-defined radio receiver (SDR) software with command-line interface. ## What does airspy-fmradion provide? -- Mono or stereo decoding of FM broadcasting stations -- Mono decoding of AM stations -- Decoding NBFM/DSB/USB/LSB/CW/WSPR stations -- Buffered real-time playback to soundcard or dumping to file -- Command-line interface (*only*) +* Supported SDR frontends: Airspy R2/Mini, Airspy HF+, and RTL-SDR +* Playing back from I/Q WAV files +* Monaural/stereo decoding of FM broadcasting stations +* Monaural decoding of AM stations +* Decoding NBFM/DSB/USB/LSB/CW/WSPR station audio +* Playback to soundcard through PortAudio or saving to file through libsndfile +* Command-line interface ## Usage ```sh # Portaudio output airspy-fmradion -t airspy -q \ - -c freq=88100000,srate=10000000,lgain=2,mgain=0,vgain=10 \ + -c freq=82500k,srate=1000k,lgain=2,mgain=0,vgain=10 \ -P - # 16-bit signed integer WAV output (pipe is not supported) airspy-fmradion -t airspyhf -q \ - -c freq=88100000,srate=768000 \ + -c freq=82500000,srate=384000 \ -W output_s16_le.wav # 32-bit float WAV output (pipe is not supported) airspy-fmradion -m am -t airspyhf -q \ - -c freq=666000 \ + -c freq=666k \ -G output_f32_le.wav ``` ### airspy-fmradion requirements - - Linux / macOS - - C++17 (gcc, clang/llvm) - - [Airspy library](https://github.com/airspy/airspyone_host) - - [Airspy HF library](https://github.com/airspy/airspyhf) - - [RTL-SDR library](http://sdr.osmocom.org/trac/wiki/rtl-sdr) - - [sndfile](https://github.com/erikd/libsndfile) - - [r8brain-free-src](https://github.com/avaneev/r8brain-free-src), a sample rate converter designed by Aleksey Vaneev of Voxengo - - [VOLK](https://www.libvolk.org/) - - [PortAudio](http://www.portaudio.com) - - [jj1bdx's fork of cmake-git-version-tracking](https://github.com/jj1bdx/cmake-git-version-tracking) - - Tested: Airspy R2, Airspy Mini, Airspy HF+ Dual Port, RTL-SDR V3 - - Fast computer - - Medium-to-strong radio signals +* Linux / macOS +* C++17 (gcc, clang/llvm) +* [Airspy library](https://github.com/airspy/airspyone_host) +* [Airspy HF library](https://github.com/airspy/airspyhf) +* [RTL-SDR library](http://sdr.osmocom.org/trac/wiki/rtl-sdr) +* [libsndfile](https://github.com/erikd/libsndfile) +* [r8brain-free-src](https://github.com/avaneev/r8brain-free-src), a sample rate converter designed by Aleksey Vaneev of Voxengo +* [VOLK](https://www.libvolk.org/) +* [PortAudio](http://www.portaudio.com) +* [jj1bdx's fork of cmake-git-version-tracking](https://github.com/jj1bdx/cmake-git-version-tracking) -For the latest version, see +### Git branches and tags -### Recommended utilities - - - [sox](http://sox.sourceforge.net/) - -### Branches and tags - - - Official releases are tagged - - _main_ is the "production" branch with the most stable release (often ahead of the latest release though) - - _dev_ is the development branch that contains current developments that will be eventually released in the main branch - - Other branches are experimental (and presumably abandoned) +* Official releases are tagged +* _main_ is the "production" branch with the most stable release (often ahead of the latest release though) +* _dev_ is the development branch that contains current developments that will be eventually released in the main branch +* Other branches are experimental (and presumably abandoned) ## Prerequisites -### Airspy HF+ firmware - -Use the latest version of Airspy HF+ firmware, available at [Airspy HF+ Dual Port](https://airspy.com/airspy-hf-plus/) and [Airspy HF+ Discovery](https://airspy.com/airspy-hf-discovery/) Web pages. - -airspy-fmradion sets the default sampling rates to 384kHz for FM broadcast, and 192kHz for the other modes. Old Airspy HF+ firmwares do not support the lower sampling rate other than 768kHz. - -### Required libraries - -Note: the main (formerly master) branch of libvolk is now required from v0.8.1. - -If you install from source in your own installation path, you have to specify the include path and library path. -For example if you installed it in `/opt/install/libairspy` you have to add `-DAIRSPY_INCLUDE_DIR=/opt/install/libairspy/include -DAIRSPYHF_INCLUDE_DIR=/opt/install/libairspyhf/include` to the cmake options. - ### Debian/Ubuntu Linux - - `sudo apt-get install cmake pkg-config libusb-1.0-0-dev libasound2-dev libairspy-dev libairspyhf-dev librtlsdr-dev libsndfile1-dev portaudio19-dev` +```sh +sudo apt-get install cmake pkg-config \ + libusb-1.0-0-dev \ + libasound2-dev \ + libairspy-dev \ + libairspyhf-dev \ + librtlsdr-dev \ + libsndfile1-dev \ + portaudio19-dev \ + libvolk2-dev +``` ### macOS * Install HomeBrew libraries as in the following shell script -* See * Use HEAD for `airspy` and `airspyhf` -```shell -brew tap pothosware/homebrew-pothos -brew tap dholm/homebrew-sdr #other sdr apps +```sh brew update brew install portaudio brew install libsndfile @@ -112,337 +100,109 @@ brew install volk ### Install the supported libvolk -Install libvolk as described in [libvolk.md](libvolk.md). +* Install libvolk as described in [libvolk.md](libvolk.md). +* Run `volk_profile` and save the configuration data for speed optimization. -### Dependency installation details +### Install the supported libsndfile for MP3 capability -#### libvolk +* You may need to install libsndfile as described in [libsndfile.md](libsndfile.md). -* See [GitHub gnuradio/volk repository](https://github.com/gnuradio/volk) for the details. +## Installation -#### libairspyhf - -Use the latest HEAD version. - -#### libairspy - -*Note: this is applicable for both macOS and Linux.* - -*Install and use the latest libairspy --HEAD version* for: - -* Working `airspy_open_devices()`, required by `airspy_open_sn()`. See [this commit](https://github.com/airspy/airspyone_host/commit/61fec20fbd710fc54d57dfec732d314d693b5a2f) for the details. -* Proper transfer block size. `if_blocksize` for Airspy HF+ is reduced from 16384 to 2048, following [this commit](https://github.com/airspy/airspyhf/commit/a1f6f4a0537f53bede6e80c51826fc9d45061c28). - -#### git submodules - -r8brain-free-src is the submodule of this repository. Download the submodule repositories by the following git procedure: - -- `git submodule update --init --recursive` - -## Installing - -### A quick way - -```shell +```sh /bin/rm -rf build +mkdir build git submodule update --init --recursive cmake -S . -B build cmake --build build --target all ``` -### In details - -To install airspy-fmradion, download and unpack the source code and go to the top level directory. Then do like this: - - - `git submodule update --init --recursive` - - `mkdir build` - - `cd build` - - `cmake ..` - -CMake tries to find librtlsdr. If this fails, you need to specify -the location of the library in one the following ways: - -```shell -cmake .. \ - -DAIRSPY_INCLUDE_DIR=/path/airspy/include \ - -DAIRSPY_LIBRARY_PATH=/path/airspy/lib/libairspy.a - -DAIRSPYHF_INCLUDE_DIR=/path/airspyhf/include \ - -DAIRSPYHF_LIBRARY_PATH=/path/airspyhf/lib/libairspyhf.a \ - -DRTLSDR_INCLUDE_DIR=/path/rtlsdr/include \ - -DRTLSDR_LIBRARY_PATH=/path/rtlsdr/lib/librtlsdr.a - -PKG_CONFIG_PATH=/path/to/airspy/lib/pkgconfig cmake .. -``` - -### Static analysis of the code - -For using static analyzers such as [OCLint](https://oclint.org) and [Clangd](https://clangd.llvm.org), use the `compile_commands.json` file built in `build/` directory, with the following commands: - -``` -cd build -ln -s `pwd`/compile_commands.json .. -``` - -The following limitation is applicable: - -* For *CMake 3.20 or later*, cmake-git-version-tracking code is intentionally removed from the compile command database. This is not applicable for the older CMake. -* Use [compdb](https://github.com/Sarcasm/compdb.git) for a more precise analysis including all the header files, with the following command: `compdb -p build/ list > compile_commands.json` - -### Compile and install - - - `make -j4` (for machines with 4 CPUs) - - `make install` - -### Copying binary to another directory - -On M1 Mac, using cp causes a trouble. Use the following command to properly install the command to a local directory: - -```shell -install -o user -m 0700 -c -s build/airspy-fmradion $(HOME)/bin -``` - ## Basic command options -*Note well: `-b` option is removed and will cause an error.* - - - `-m devtype` is modulation type, one of `fm`, `nbfm`, `am`, `dsb`, `usb`, `lsb`, `cw`, `wspr` (default fm) - - `-t devtype` is mandatory and must be `airspy` for Airspy R2 / Airspy Mini, `airspyhf` for Airspy HF+, `rtlsdr` for RTL-SDR, and `filesource` for the File Source driver. - - `-q` Quiet mode. - - `-c config` Comma separated list of configuration options as key=value pairs or just key for switches. Depends on device type (see next paragraph). - - `-d devidx` Device index, 'list' to show device list (default 0) - - `-M` Disable stereo decoding - - `-R filename` Write audio data as raw `S16_LE` samples. Use filename `-` to write to stdout - - `-F filename` Write audio data as raw `FLOAT_LE` samples. Use filename `-` to write to stdout - - `-W filename` Write audio data as RF64/WAV `S16_LE` samples. Use filename `-` to write to stdout (*pipe is not supported*) - - `-G filename` Write audio data as RF64/WAV `FLOAT_LE` samples. Use filename `-` to write to stdout (*pipe is not supported*) - - `-P device_num` Play audio via PortAudio device index number. Use string `-` to specify the default PortAudio device - - `-T filename` Write pulse-per-second timestamps. Use filename '-' to write to stdout - - `-X` Shift pilot phase (for Quadrature Multipath Monitor) (-X is ignored under mono mode (-M)) - - `-U` Set deemphasis to 75 microseconds (default: 50) - - `-f` Set Filter type - - for FM: wide and default: none, medium: +-156kHz, narrow: +-121kHz - - for AM: wide: +-9kHz, default: +-6kHz, medium: +-4.5kHz, narrow: +-3kHz - - for NBFM: wide: +-20kHz, default: +-10kHz, medium: +-8kHz, narrow: +-6.25kHz - - `-l dB` Enable IF squelch, set the level to minus given value of dB - - `-E stages` Enable multipath filter for FM (For stable reception only: turn off if reception becomes unstable) - - `-r ppm` Set IF offset in ppm (range: +-1000000ppm) (Note: this option affects output pitch and timing: *use for the output timing compensation only!* - - `-A` (For FM only) Experimental 10Hz-step IF AFC - -## Major changes - -### Timestamp file format +* `-m devtype` is modulation type, one of `fm`, `nbfm`, `am`, `dsb`, `usb`, `lsb`, `cw`, `wspr` (default fm) +* `-t devtype` is mandatory and must be `airspy` for Airspy R2 / Airspy Mini, `airspyhf` for Airspy HF+, `rtlsdr` for RTL-SDR, and `filesource` for the File Source driver. +* `-q` Quiet mode. +* `-c config` Comma separated list of configuration options as key=value pairs or just key for switches. Depends on device type (see next paragraph). +* `-d devidx` Device index, 'list' to show device list (default 0) +* `-M` Disable stereo decoding +* `-R filename` Write audio data as raw `S16_LE` samples. Use filename `-` to write to stdout +* `-F filename` Write audio data as raw `FLOAT_LE` samples. Use filename `-` to write to stdout +* `-W filename` Write audio data as RF64/WAV `S16_LE` samples. Use filename `-` to write to stdout (_pipe is not supported_) +* `-G filename` Write audio data as RF64/WAV `FLOAT_LE` samples. Use filename `-` to write to stdout (_pipe is not supported_) +* `-C filename` Write audio data to MP3 file of VBR -V 1. Use filename '-' to write to stdout. (_This function is available when linked with supported libsndfile only._) +* `-P device_num` Play audio via PortAudio device index number. Use string `-` to specify the default PortAudio device +* `-T filename` Write pulse-per-second timestamps. Use filename '-' to write to stdout +* `-X` Shift pilot phase (for Quadrature Multipath Monitor) (-X is ignored under mono mode (-M)) +* `-U` Set deemphasis to 75 microseconds (default: 50) +* `-f` Set Filter type + * for FM: wide and default: none, medium: +-156kHz, narrow: +-121kHz + * for AM: wide: +-9kHz, default: +-6kHz, medium: +-4.5kHz, narrow: +-3kHz + * for NBFM: wide: +-20kHz, default: +-10kHz, medium: +-8kHz, narrow: +-6.25kHz +* `-l dB` Enable IF squelch, set the level to minus given value of dB +* `-E stages` Enable multipath filter for FM (For stable reception only: turn off if reception becomes unstable) +* `-r ppm` Set IF offset in ppm (range: +-1000000ppm) (Note: this option affects output pitch and timing: *use for the output timing compensation only!* + +## Timestamp file format * For FM: `pps_index sample_index unix_time if_level` * For the other modes: `block unix_time if_level` * if\_level is in dB -### Rate compensation for adjusting audio device playback speed offset - -* Background: some audio devices shows non-negligible offset of playback speed, which causes eventual audio output buffer overflow and significant delay in long-term playback. -* How to fix: compensating the playback speed offset gives more accurate playback timing, by sacrificing output audio pitch accuracy. A proper compensation will eliminate the cause of increasing output buffer length, by sending less data (lower sampling rate) to the conversion process. -* You can specify the compensation rate by ppm using `-r` option. -* How to estimate the rate offset: when elapsed playback time is `Tp` [seconds] and output buffer length (`buf=` in the debug output) increases during the time is `Ts` [seconds], the compensation rate is `(Ts/Tp) * 1000000` [ppm]. -* For example, if the output buffer length increases for 1 second after playing back for 7 hours (25200 seconds), the offset rate is 1/25200 * 1000000 ~= 39.68ppm. -* +- 100ppm offset is not uncommon among the consumer-grade audio devices. -* +- 100ppm pitch change may not be recongizable by human. - -#### Caveats for the rate compensation - -* *Do not use this feature if the per-sample accuracy is essential.* -* *Do not use this feature for non-realtime output (for example, to files).* -* Output audio pitch increases as the offset increases. -* Too much compensation will cause output underflow. -* This feature causes fractional (non-integer) resampling by `IfResampler` class, which causes more CPU usage. - -### Smaller latency +## Output audio specification -* v0.9.2 uses smaller latency algorithms for all modulation types and filters. The output frequency characteristics may be different from the previous versions. - -### Audio gain adjustment - -* Since v0.4.2, output maximum level is back at -6dB (0.5) (`adjust_gain()` is reintroduced) again, as in pre-v0.2.7 -* During v0.2.7 to v0.4.1, output level was at unity (`adjust_gain()` is removed) -* Before v0.2.7, output maximum level is at -6dB (0.5) - -### Audio and IF downsampling is performed by r8brain-free-src - -* Output of the stereo decoder is downsampled to 48kHz -* 19kHz cut LPF implemented for post-processing resampler output -* Audio sample rate is fixed to 48000Hz -* `r8b::CDSPResampler24` is used for IF resampling - -### Phase discriminator uses GNU Radio fast\_atan2f() - -* From v0.7.8-pre0, GNU Radio `fast_atan2f()` which has ~20-bit accuracy, is used for PhaseDiscriminator class and the 19kHz pilot PLL. -* The past `fastatan2()` used in v0.6.10 and before was removed due to low accuracy (of ~10 bits) -* Changing from the past `atan2()` to `fast_atan2f()` showed no noticeable difference of the THD+N (0.218%) and THD (0.018%). (Measured from JOBK-FM NHK Osaka FM 88.1MHz hourly time tone 880Hz, using airwaves after the multipath canceler filter of -E36) -* [The past `fastatan2()` allowed +-0.005 radian max error](https://www.dsprelated.com/showarticle/1052.php) -* libm `atan2()` allows only approx. 0.5 ULP as the max error for macOS 10.14.5, measured by using the code from ["Error analysis of system mathematical functions -" by Gaston H. Gonnet](http://www-oldurls.inf.ethz.ch/personal/gonnet/FPAccuracy/Analysis.html) (1 ULP for macOS 64bit `double` = 2^(-53) = approx. 10^(15.95)) +* Output maximum level is nominally -6dB (0.5) but may increase up to 0dB (1.0) +* Output audio sample rate is fixed to 48000Hz ### FM multipath filter * A Normalized LMS-based multipath filter can be enabled after IF AGC -* IF sample stages can be defined by `-E` options -* Reference amplitude level: 1.0 -* For Mac mini 2018 with 3.2 GHz Intel Core i7, 288 stages consume 99% of one CPU core -* This filter is not effective when the IF bandwidth is narrow (192kHz) -* The multipath filter starts after discarding the first 100 blocks. This change is to avoid the initial instability of Airspy R2. -* Note: this filter recalculates the coefficients for every four (4) samples, to reduce the processing load. - -### Multipath filter configuration - -* v0.7.3 and later: -E36 for 108 previous and 36 after stages (ratio 3:1). The multipath filter order: (4 * stages) + 1 -* For reference only: v0.7.3-pre1 and before: -E72 for 72 previous and 72 after stages (ratio 1:1), summary: set the -E parameter to 1/2 of the previous value -* Rule of thumb: -E36 is sufficient for a stable strong singal (albeit with considerable multipath distortion). - -### FM L-R signal boosted for the stereo separation improvement - -* Teruhiko Hayashi suggested boosting L-R signal by 1.017 for a better stereo separation. Implemented since v0.7.6-pre3. -* DiscriminatorEqualizer removed since v1.7.6-pre3 (needs more precise compensation, presumably with an FIR filter. - -### FM deemphasis error prevention - -* Teruhiko Hayashi suggested applying deemphasis *before* the sampling rate conversion, at the demodulator rate, higher than the audio output rate. Implemented since v0.7.6. - -## No-goals - -* CIC filters for the IF 1st stage (unable to explore parallelism, too complex to compensate) -* Using lock-free threads (`boost::lockfree::spsc_queue` didn't make things faster, and consumed x2 CPU power) - -## Filter design documentation - -### General characteristics - -* If resampling converter affects the passband characteristics - -### For FM - -* FM Filter coefficients are listed under `doc/filter-design` - -### For NBFM - -* Deviation: normal +-8kHz, for wide +-17kHz -* Output audio LPF: flat up to 4kHz -* NBFM Filter coefficients are listed under `doc/filter-design` -* `default` filter width: +-10kHz -* Narrower filters by `-f` options: `middle` +-8kHz, `narrow` +-6.25kHz -* Wider filters by `-f` options: `wide` +-20kHz (with wider deviation of +-17kHz) -* Audio gain reduced by -3dB to prevent output clipping - -### For AM and DSB - -* AM Filter coefficients are listed under `doc/filter-design` -* `default` filter width: +-6kHz -* Narrower filters by `-f` options: `middle` +-4.5kHz, `narrow` +-3kHz -* Wider filters by `-f` options: `wide` +-9kHz - -### For SSB (USB/LSB) +* IF sample stages is defined by `-E` options +* Practical upper limit of `-E` value: 200 +* Practical `-E` value: up to 50 for Raspberry Pi 4, approx. 100 for a modern computer -* For USB: shift down 1.5kHz -> LPF -> shift up 1.5kHz -* For USB: shift up 1.5kHz -> LPF -> shift down 1.5kHz -* Rate conversion of 48kHz to 12kHz and vice versa for the input and output of LPF -* 12kHz sampling rate LPF: flat till +-1200Hz, -3dB +-1320Hz, -10dB +-1370Hz, -58.64dB at +-1465Hz +## Airspy R2 / Mini configuration options -### For CW +* `freq=` Desired tune frequency in Hz. Valid range from 1M to 1.8G. (default 100M: `100000000`) +* `srate=` Device sample rate. `list` lists valid values and exits. (default `10000000`). Valid values depend on the Airspy firmware. Airspy firmware and library must support dynamic sample rate query. +* `lgain=` LNA gain in dB. Valid values are: `0, 1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10, 11 12, 13, 14, list`. `list` lists valid values and exits. (default `8`) +* `mgain=` Mixer gain in dB. Valid values are: `0, 1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10, 11 12, 13, 14, 15, list`. `list` lists valid values and exits. (default `8`) +* `vgain=` VGA gain in dB. Valid values are: `0, 1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10, 11 12, 13, 14, 15, list`. `list` lists valid values and exits. (default `0`) +* `antbias` Turn on the antenna bias for remote LNA (default off) +* `lagc` Turn on the LNA AGC (default off) +* `magc` Turn on the mixer AGC (default off) -* Zeroed-in pitch: 500Hz -* Filter width: +- 100Hz flat, +-250Hz for cutoff -* Uses downsampling to 12kHz for applying a steep filter +## Airspy HF+ configuration options -### For WSPR +* `freq=` Desired tune frequency in Hz. Valid range from 0 to 31M, and from 60M to 240M. (default 100M: `100000000`) +* `srate=` Device sample rate. `list` lists valid values and exits. (default `384000`). Valid values depend on the Airspy HF firmware. Airspy HF firmware and library must support dynamic sample rate query. +* `hf_att=` HF attenuation level and AGC control. + * 0: enable AGC, no attenuation + * 1 - 8: disable AGC, apply attenuation of value * 6dB -* Set USB TX carrier frequency for reception (as shown in [WSPRnet](https://wsprnet.org/)) -* Pitch: 1500Hz -* Filter width: +- 100Hz flat, +-250Hz for cutoff -* Uses downsampling to 12kHz for applying a steep filter +## RTL*SDR configuration options -## AGC algorithms - -### Simple AGC with Tisserand-Berviller Algorithm - -* Reference: Etienne Tisserand, Yves Berviller. Design and implementation of a new digital automatic gain control. Electronics Letters, IET, 2016, 52 (22), pp.1847 - 1849. ff10.1049/el.2016.1398ff. ffhal-01397371f -* Implementation reference: -* Implemented for IF AGC since 20220808-0 -* Implemented for AF AGC since 20220808-3 - -## Airspy R2 / Mini modification from ngsoftfm-jj1bdx - -### Feature changes - -* Finetuner is removed (Not really needed for +-1ppm or less offset) - -### Airspy R2 / Mini configuration options - - - `freq=` Desired tune frequency in Hz. Valid range from 1M to 1.8G. (default 100M: `100000000`) - - `srate=` Device sample rate. `list` lists valid values and exits. (default `10000000`). Valid values depend on the Airspy firmware. Airspy firmware and library must support dynamic sample rate query. - - `lgain=` LNA gain in dB. Valid values are: `0, 1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10, 11 12, 13, 14, list`. `list` lists valid values and exits. (default `8`) - - `mgain=` Mixer gain in dB. Valid values are: `0, 1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10, 11 12, 13, 14, 15, list`. `list` lists valid values and exits. (default `8`) - - `vgain=` VGA gain in dB. Valid values are: `0, 1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10, 11 12, 13, 14, 15, list`. `list` lists valid values and exits. (default `0`) - - `antbias` Turn on the antenna bias for remote LNA (default off) - - `lagc` Turn on the LNA AGC (default off) - - `magc` Turn on the mixer AGC (default off) - -## Airspy HF+ modification from airspy-fmradion v0.2.7 - -### Sample rates and IF modes - -* This version supports 768kHz zero-IF mode and 384/256/192kHz low-IF modes - -### Conversion process for 768kHz zero-IF mode - -* LPFIQ is single-stage -* IF center frequency is down Fs/4 than the station frequency, i.e: when the station is 76.5MHz, the tuned frequency is 76.308MHz -* Airspy HF+ allows only 660kHz alias-free BW, so the maximum alias-free BW for IF is (660/2)kHz - 192kHz = 138kHz - -### Conversion process for 384/256/192kHz low-IF mode - -* IF center frequency is not shifted - -### Airspy HF configuration options - - - `freq=` Desired tune frequency in Hz. Valid range from 0 to 31M, and from 60M to 240M. (default 100M: `100000000`) - - `srate=` Device sample rate. `list` lists valid values and exits. (default `384000`). Valid values depend on the Airspy HF firmware. Airspy HF firmware and library must support dynamic sample rate query. - - `hf_att=` HF attenuation level and AGC control. - - 0: enable AGC, no attenuation - - 1 - 8: disable AGC, apply attenuation of value * 6dB - -## RTL-SDR - -### Sample rate - -* Valid sample rates are from 1000000 to 1250000 [Hz] -* The default value is 1200000Hz - -### RTL-SDR configuration options - - - `freq=` Desired tune frequency in Hz. Accepted range from 10M to 2.2G. +* `freq=` Desired tune frequency in Hz. Accepted range from 10M to 2.2G. (default 100M: `100000000`) - - `gain=` (default `auto`) - - `auto` Selects gain automatically - - `list` Lists available gains and exit - - `` gain in dB. Possible gains in dB are: `0.0, 0.9, 1.4, 2.7, 3.7, +* `gain=` (default `auto`) + * `auto` Selects gain automatically + * `list` Lists available gains and exit + * `` gain in dB. Possible gains in dB are: `0.0, 0.9, 1.4, 2.7, 3.7, 7.7, 8.7, 12.5, 14.4, 15.7, 16.6, 19.7, 20.7, 22.9, 25.4, 28.0, 29.7, 32.8, 33.8 , 36.4, 37.2, 38.6, 40.2, 42.1, 43.4, 43.9, 44.5, 48.0, 49.6` - - `srate=` Device sample rate. valid values in the [900001, 3200000] range. (default `1152000`) - - `blklen=` Device block length in bytes (default RTL-SDR default i.e. 64k) - - `agc` Activates device AGC (default off) - - `antbias` Turn on the antenna bias for remote LNA (default off) - -## File Source driver - -* Reads an IQ signal format file and decode the output. -* *This device is still experimental.* +* `srate=` Device sample rate. valid values in the [900001, 3200000] range. (default `1152000`) +* `blklen=` Device block length in bytes (default RTL-SDR default i.e. 64k) +* `agc` Activates device AGC (default off) +* `antbias` Turn on the antenna bias for remote LNA (default off) -### File Source configuration options +## File Source driver configuration options - - `freq=` Frequency of radio station in Hz. - - `srate=` IF sample rate in Hz. - - `filename=` Source file name. Supported encodings: `FLOAT`, `S24_LE`, `S16_LE` - - `zero_offset` Set if the source file is in zero offset, which requires Fs/4 IF shifting. - - `blklen=` Set block length in samples. - - `raw` Set if the file is raw binary. - - `format=` Set the file format for the raw binary file. Supported formats: `U8_LE`, `S8_LE`, `S16_LE`, `S24_LE`, `FLOAT` +* `freq=` Frequency of radio station in Hz. +* `srate=` IF sample rate in Hz. +* `filename=` Source file name. Supported encodings: `FLOAT`, `S24_LE`, `S16_LE` +* `zero_offset` Set if the source file is in zero offset, which requires Fs/4 IF shifting. +* `blklen=` Set block length in samples. +* `raw` Set if the file is raw binary. +* `format=` Set the file format for the raw binary file. Supported formats: `U8_LE`, `S8_LE`, `S16_LE`, `S24_LE`, `FLOAT` ## Authors and contributors @@ -464,6 +224,6 @@ install -o user -m 0700 -c -s build/airspy-fmradion $(HOME)/bin ## License * As a whole package: GPLv3 (and later). See [LICENSE](LICENSE). -* [csdr](https://github.com/simonyiszk/csdr) AGC code: BSD license. -* Some source code files are stating GPL "v2 and later" license, and the MIT License. +* Each source code file might state a GPLv3-compatible license. +[End of README.md] diff --git a/doc/old-README-until-2023.md b/doc/old-README-until-2023.md new file mode 100644 index 0000000..95c4dda --- /dev/null +++ b/doc/old-README-until-2023.md @@ -0,0 +1,472 @@ +[//]: # (-*- coding: utf-8 -*-) + +# airspy-fmradion old README until 2023 + +*Note well: contents in this file is not necessarily up to date.* + +* Version 20240107-0 +* For macOS (Apple Silicon) and Linux + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the details. + +## Known issues and changes + +Please read [CHANGES.md](CHANGES.md) before using the software. + +## What is airspy-fmradion? + +* **airspy-fmradion** is software-defined radio receiver (SDR) software with command-line interface. + +## What does airspy-fmradion provide? + +- Supported SDR frontends: Airspy R2/Mini, Airspy HF+, and RTL-SDR +- I/Q WAV file frontend is also supported +- Mono or stereo decoding of FM broadcasting stations +- Mono decoding of AM stations +- Decoding NBFM/DSB/USB/LSB/CW/WSPR stations +- Playback to soundcard through PortAudio or dumping to file +- Command-line interface (*only*) + +## Usage + +```sh +# Portaudio output +airspy-fmradion -t airspy -q \ + -c freq=88100000,srate=10000000,lgain=2,mgain=0,vgain=10 \ + -P - + +# 16-bit signed integer WAV output (pipe is not supported) +airspy-fmradion -t airspyhf -q \ + -c freq=88100000,srate=768000 \ + -W output_s16_le.wav + +# 32-bit float WAV output (pipe is not supported) +airspy-fmradion -m am -t airspyhf -q \ + -c freq=666000 \ + -G output_f32_le.wav +``` + +### airspy-fmradion requirements + + - Linux / macOS + - C++17 (gcc, clang/llvm) + - [Airspy library](https://github.com/airspy/airspyone_host) + - [Airspy HF library](https://github.com/airspy/airspyhf) + - [RTL-SDR library](http://sdr.osmocom.org/trac/wiki/rtl-sdr) + - [sndfile](https://github.com/erikd/libsndfile) + - [r8brain-free-src](https://github.com/avaneev/r8brain-free-src), a sample rate converter designed by Aleksey Vaneev of Voxengo + - [VOLK](https://www.libvolk.org/) + - [PortAudio](http://www.portaudio.com) + - [jj1bdx's fork of cmake-git-version-tracking](https://github.com/jj1bdx/cmake-git-version-tracking) + - Supported SDR frontends: Airspy R2, Airspy Mini, Airspy HF+ Dual Port, Airspy HF+ Discovery, and RTL-SDR V3 + - Fast computer + - Medium-to-strong radio signals + +For the latest version, see + +### Recommended utilities + + - [sox](http://sox.sourceforge.net/) + +### Git branches and tags + + - Official releases are tagged + - _main_ is the "production" branch with the most stable release (often ahead of the latest release though) + - _dev_ is the development branch that contains current developments that will be eventually released in the main branch + - Other branches are experimental (and presumably abandoned) + +## Prerequisites + +### Airspy HF+ firmware + +Use the latest version of Airspy HF+ firmware, available at [Airspy HF+ Dual Port](https://airspy.com/airspy-hf-plus/) and [Airspy HF+ Discovery](https://airspy.com/airspy-hf-discovery/) Web pages. + +airspy-fmradion sets the default sampling rates to 384kHz for FM broadcast, and 192kHz for the other modes. Old Airspy HF+ firmwares do not support the lower sampling rate other than 768kHz. + +### Required libraries + +Note: the main (formerly master) branch of libvolk is now required from v0.8.1. + +If you install from source in your own installation path, you have to specify the include path and library path. +For example if you installed it in `/opt/install/libairspy` you have to add `-DAIRSPY_INCLUDE_DIR=/opt/install/libairspy/include -DAIRSPYHF_INCLUDE_DIR=/opt/install/libairspyhf/include` to the cmake options. + +### Debian/Ubuntu Linux + + - `sudo apt-get install cmake pkg-config libusb-1.0-0-dev libasound2-dev libairspy-dev libairspyhf-dev librtlsdr-dev libsndfile1-dev portaudio19-dev` + +### macOS + +* Install HomeBrew libraries as in the following shell script +* See +* Use HEAD for `airspy` and `airspyhf` + +```shell +brew tap pothosware/homebrew-pothos +brew tap dholm/homebrew-sdr #other sdr apps +brew update +brew install portaudio +brew install libsndfile +brew install rtl-sdr +brew install airspy --HEAD +brew install airspyhf --HEAD +brew install volk +``` + +### Install the supported libvolk + +Install libvolk as described in [libvolk.md](libvolk.md). + +### Dependency installation details + +#### libvolk + +* See [GitHub gnuradio/volk repository](https://github.com/gnuradio/volk) for the details. + +#### libairspyhf + +Use the latest HEAD version. + +#### libairspy + +*Note: this is applicable for both macOS and Linux.* + +*Install and use the latest libairspy --HEAD version* for: + +* Working `airspy_open_devices()`, required by `airspy_open_sn()`. See [this commit](https://github.com/airspy/airspyone_host/commit/61fec20fbd710fc54d57dfec732d314d693b5a2f) for the details. +* Proper transfer block size. `if_blocksize` for Airspy HF+ is reduced from 16384 to 2048, following [this commit](https://github.com/airspy/airspyhf/commit/a1f6f4a0537f53bede6e80c51826fc9d45061c28). + +#### git submodules + +r8brain-free-src is the submodule of this repository. Download the submodule repositories by the following git procedure: + +- `git submodule update --init --recursive` + +## Installing + +### A quick way + +```shell +/bin/rm -rf build +git submodule update --init --recursive +cmake -S . -B build +cmake --build build --target all +``` + +### In details + +To install airspy-fmradion, download and unpack the source code and go to the top level directory. Then do like this: + + - `git submodule update --init --recursive` + - `mkdir build` + - `cd build` + - `cmake ..` + +CMake tries to find librtlsdr. If this fails, you need to specify +the location of the library in one the following ways: + +```shell +cmake .. \ + -DAIRSPY_INCLUDE_DIR=/path/airspy/include \ + -DAIRSPY_LIBRARY_PATH=/path/airspy/lib/libairspy.a + -DAIRSPYHF_INCLUDE_DIR=/path/airspyhf/include \ + -DAIRSPYHF_LIBRARY_PATH=/path/airspyhf/lib/libairspyhf.a \ + -DRTLSDR_INCLUDE_DIR=/path/rtlsdr/include \ + -DRTLSDR_LIBRARY_PATH=/path/rtlsdr/lib/librtlsdr.a + +PKG_CONFIG_PATH=/path/to/airspy/lib/pkgconfig cmake .. +``` + +### Static analysis of the code + +For using static analyzers such as [OCLint](https://oclint.org) and [Clangd](https://clangd.llvm.org), use the `compile_commands.json` file built in `build/` directory, with the following commands: + +``` +cd build +ln -s `pwd`/compile_commands.json .. +``` + +The following limitation is applicable: + +* For *CMake 3.20 or later*, cmake-git-version-tracking code is intentionally removed from the compile command database. This is not applicable for the older CMake. +* Use [compdb](https://github.com/Sarcasm/compdb.git) for a more precise analysis including all the header files, with the following command: `compdb -p build/ list > compile_commands.json` + +### Compile and install + + - `make -j4` (for machines with 4 CPUs) + - `make install` + +### Copying binary to another directory + +On M1 Mac, using cp causes a trouble. Use the following command to properly install the command to a local directory: + +```shell +install -o user -m 0700 -c -s build/airspy-fmradion $(HOME)/bin +``` + +## Basic command options + +*Note well: `-b` option is removed and will cause an error.* + + - `-m devtype` is modulation type, one of `fm`, `nbfm`, `am`, `dsb`, `usb`, `lsb`, `cw`, `wspr` (default fm) + - `-t devtype` is mandatory and must be `airspy` for Airspy R2 / Airspy Mini, `airspyhf` for Airspy HF+, `rtlsdr` for RTL-SDR, and `filesource` for the File Source driver. + - `-q` Quiet mode. + - `-c config` Comma separated list of configuration options as key=value pairs or just key for switches. Depends on device type (see next paragraph). + - `-d devidx` Device index, 'list' to show device list (default 0) + - `-M` Disable stereo decoding + - `-R filename` Write audio data as raw `S16_LE` samples. Use filename `-` to write to stdout + - `-F filename` Write audio data as raw `FLOAT_LE` samples. Use filename `-` to write to stdout + - `-W filename` Write audio data as RF64/WAV `S16_LE` samples. Use filename `-` to write to stdout (*pipe is not supported*) + - `-G filename` Write audio data as RF64/WAV `FLOAT_LE` samples. Use filename `-` to write to stdout (*pipe is not supported*) + - `-P device_num` Play audio via PortAudio device index number. Use string `-` to specify the default PortAudio device + - `-T filename` Write pulse-per-second timestamps. Use filename '-' to write to stdout + - `-X` Shift pilot phase (for Quadrature Multipath Monitor) (-X is ignored under mono mode (-M)) + - `-U` Set deemphasis to 75 microseconds (default: 50) + - `-f` Set Filter type + - for FM: wide and default: none, medium: +-156kHz, narrow: +-121kHz + - for AM: wide: +-9kHz, default: +-6kHz, medium: +-4.5kHz, narrow: +-3kHz + - for NBFM: wide: +-20kHz, default: +-10kHz, medium: +-8kHz, narrow: +-6.25kHz + - `-l dB` Enable IF squelch, set the level to minus given value of dB + - `-E stages` Enable multipath filter for FM (For stable reception only: turn off if reception becomes unstable) + - `-r ppm` Set IF offset in ppm (range: +-1000000ppm) (Note: this option affects output pitch and timing: *use for the output timing compensation only!* + +## Major changes + +### Timestamp file format + +* For FM: `pps_index sample_index unix_time if_level` +* For the other modes: `block unix_time if_level` +* if\_level is in dB + +### Rate compensation for adjusting audio device playback speed offset + +* Background: some audio devices shows non-negligible offset of playback speed, which causes eventual audio output buffer overflow and significant delay in long-term playback. +* How to fix: compensating the playback speed offset gives more accurate playback timing, by sacrificing output audio pitch accuracy. A proper compensation will eliminate the cause of increasing output buffer length, by sending less data (lower sampling rate) to the conversion process. +* You can specify the compensation rate by ppm using `-r` option. +* How to estimate the rate offset: when elapsed playback time is `Tp` [seconds] and output buffer length (`buf=` in the debug output) increases during the time is `Ts` [seconds], the compensation rate is `(Ts/Tp) * 1000000` [ppm]. +* For example, if the output buffer length increases for 1 second after playing back for 7 hours (25200 seconds), the offset rate is 1/25200 * 1000000 ~= 39.68ppm. +* +- 100ppm offset is not uncommon among the consumer-grade audio devices. +* +- 100ppm pitch change may not be recongizable by human. + +#### Caveats for the rate compensation + +* *Do not use this feature if the per-sample accuracy is essential.* +* *Do not use this feature for non-realtime output (for example, to files).* +* Output audio pitch increases as the offset increases. +* Too much compensation will cause output underflow. +* This feature causes fractional (non-integer) resampling by `IfResampler` class, which causes more CPU usage. + +### Smaller latency + +* v0.9.2 uses smaller latency algorithms for all modulation types and filters. The output frequency characteristics may be different from the previous versions. + +### Audio gain adjustment + +* Since v0.4.2, output maximum level is back at -6dB (0.5) (`adjust_gain()` is reintroduced) again, as in pre-v0.2.7 +* During v0.2.7 to v0.4.1, output level was at unity (`adjust_gain()` is removed) +* Before v0.2.7, output maximum level is at -6dB (0.5) + +### Audio and IF downsampling is performed by r8brain-free-src + +* Output of the stereo decoder is downsampled to 48kHz +* 19kHz cut LPF implemented for post-processing resampler output +* Audio sample rate is fixed to 48000Hz +* `r8b::CDSPResampler24` is used for IF resampling + +### Phase discriminator uses GNU Radio fast\_atan2f() + +* From v0.7.8-pre0, GNU Radio `fast_atan2f()` which has ~20-bit accuracy, is used for PhaseDiscriminator class and the 19kHz pilot PLL. +* The past `fastatan2()` used in v0.6.10 and before was removed due to low accuracy (of ~10 bits) +* Changing from the past `atan2()` to `fast_atan2f()` showed no noticeable difference of the THD+N (0.218%) and THD (0.018%). (Measured from JOBK-FM NHK Osaka FM 88.1MHz hourly time tone 880Hz, using airwaves after the multipath canceler filter of -E36) +* [The past `fastatan2()` allowed +-0.005 radian max error](https://www.dsprelated.com/showarticle/1052.php) +* libm `atan2()` allows only approx. 0.5 ULP as the max error for macOS 10.14.5, measured by using the code from ["Error analysis of system mathematical functions +" by Gaston H. Gonnet](http://www-oldurls.inf.ethz.ch/personal/gonnet/FPAccuracy/Analysis.html) (1 ULP for macOS 64bit `double` = 2^(-53) = approx. 10^(15.95)) + +### FM multipath filter + +* A Normalized LMS-based multipath filter can be enabled after IF AGC +* IF sample stages can be defined by `-E` options +* Reference amplitude level: 1.0 +* For Mac mini 2018 with 3.2 GHz Intel Core i7, 288 stages consume 99% of one CPU core +* This filter is not effective when the IF bandwidth is narrow (192kHz) +* The multipath filter starts after discarding the first 100 blocks. This change is to avoid the initial instability of Airspy R2. +* Note: this filter recalculates the coefficients for every four (4) samples, to reduce the processing load. + +### Multipath filter configuration + +* v0.7.3 and later: -E36 for 108 previous and 36 after stages (ratio 3:1). The multipath filter order: (4 * stages) + 1 +* For reference only: v0.7.3-pre1 and before: -E72 for 72 previous and 72 after stages (ratio 1:1), summary: set the -E parameter to 1/2 of the previous value +* Rule of thumb: -E36 is sufficient for a stable strong singal (albeit with considerable multipath distortion). + +### FM L-R signal boosted for the stereo separation improvement + +* Teruhiko Hayashi suggested boosting L-R signal by 1.017 for a better stereo separation. Implemented since v0.7.6-pre3. +* DiscriminatorEqualizer removed since v1.7.6-pre3 (needs more precise compensation, presumably with an FIR filter. + +### FM deemphasis error prevention + +* Teruhiko Hayashi suggested applying deemphasis *before* the sampling rate conversion, at the demodulator rate, higher than the audio output rate. Implemented since v0.7.6. + +## No-goals + +* CIC filters for the IF 1st stage (unable to explore parallelism, too complex to compensate) +* Using lock-free threads (`boost::lockfree::spsc_queue` didn't make things faster, and consumed x2 CPU power) + +## Filter design documentation + +### General characteristics + +* If resampling converter affects the passband characteristics + +### For FM + +* FM Filter coefficients are listed under `doc/filter-design` + +### For NBFM + +* Deviation: normal +-8kHz, for wide +-17kHz +* Output audio LPF: flat up to 4kHz +* NBFM Filter coefficients are listed under `doc/filter-design` +* `default` filter width: +-10kHz +* Narrower filters by `-f` options: `middle` +-8kHz, `narrow` +-6.25kHz +* Wider filters by `-f` options: `wide` +-20kHz (with wider deviation of +-17kHz) +* Audio gain reduced by -3dB to prevent output clipping + +### For AM and DSB + +* AM Filter coefficients are listed under `doc/filter-design` +* `default` filter width: +-6kHz +* Narrower filters by `-f` options: `middle` +-4.5kHz, `narrow` +-3kHz +* Wider filters by `-f` options: `wide` +-9kHz + +### For SSB (USB/LSB) + +* For USB: shift down 1.5kHz -> LPF -> shift up 1.5kHz +* For USB: shift up 1.5kHz -> LPF -> shift down 1.5kHz +* Rate conversion of 48kHz to 12kHz and vice versa for the input and output of LPF +* 12kHz sampling rate LPF: flat till +-1200Hz, -3dB +-1320Hz, -10dB +-1370Hz, -58.64dB at +-1465Hz + +### For CW + +* Zeroed-in pitch: 500Hz +* Filter width: +- 100Hz flat, +-250Hz for cutoff +* Uses downsampling to 12kHz for applying a steep filter + +### For WSPR + +* Set USB TX carrier frequency for reception (as shown in [WSPRnet](https://wsprnet.org/)) +* Pitch: 1500Hz +* Filter width: +- 100Hz flat, +-250Hz for cutoff +* Uses downsampling to 12kHz for applying a steep filter + +## AGC algorithms + +### Simple AGC with Tisserand-Berviller Algorithm + +* Reference: Etienne Tisserand, Yves Berviller. Design and implementation of a new digital automatic gain control. Electronics Letters, IET, 2016, 52 (22), pp.1847 - 1849. ff10.1049/el.2016.1398ff. ffhal-01397371f +* Implementation reference: +* Implemented for IF AGC since 20220808-0 +* Implemented for AF AGC since 20220808-3 + +## Airspy R2 / Mini modification from ngsoftfm-jj1bdx + +### Feature changes + +* Finetuner is removed (Not really needed for +-1ppm or less offset) + +### Airspy R2 / Mini configuration options + + - `freq=` Desired tune frequency in Hz. Valid range from 1M to 1.8G. (default 100M: `100000000`) + - `srate=` Device sample rate. `list` lists valid values and exits. (default `10000000`). Valid values depend on the Airspy firmware. Airspy firmware and library must support dynamic sample rate query. + - `lgain=` LNA gain in dB. Valid values are: `0, 1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10, 11 12, 13, 14, list`. `list` lists valid values and exits. (default `8`) + - `mgain=` Mixer gain in dB. Valid values are: `0, 1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10, 11 12, 13, 14, 15, list`. `list` lists valid values and exits. (default `8`) + - `vgain=` VGA gain in dB. Valid values are: `0, 1, 2, 3, 4, 5, 6, 7, 8 ,9 ,10, 11 12, 13, 14, 15, list`. `list` lists valid values and exits. (default `0`) + - `antbias` Turn on the antenna bias for remote LNA (default off) + - `lagc` Turn on the LNA AGC (default off) + - `magc` Turn on the mixer AGC (default off) + +## Airspy HF+ modification from airspy-fmradion v0.2.7 + +### Sample rates and IF modes + +* This version supports 768kHz zero-IF mode and 384/256/192kHz low-IF modes + +### Conversion process for 768kHz zero-IF mode + +* LPFIQ is single-stage +* IF center frequency is down Fs/4 than the station frequency, i.e: when the station is 76.5MHz, the tuned frequency is 76.308MHz +* Airspy HF+ allows only 660kHz alias-free BW, so the maximum alias-free BW for IF is (660/2)kHz - 192kHz = 138kHz + +### Conversion process for 384/256/192kHz low-IF mode + +* IF center frequency is not shifted + +### Airspy HF configuration options + + - `freq=` Desired tune frequency in Hz. Valid range from 0 to 31M, and from 60M to 240M. (default 100M: `100000000`) + - `srate=` Device sample rate. `list` lists valid values and exits. (default `384000`). Valid values depend on the Airspy HF firmware. Airspy HF firmware and library must support dynamic sample rate query. + - `hf_att=` HF attenuation level and AGC control. + - 0: enable AGC, no attenuation + - 1 - 8: disable AGC, apply attenuation of value * 6dB + +## RTL-SDR + +### Sample rate + +* Valid sample rates are from 1000000 to 1250000 [Hz] +* The default value is 1200000Hz + +### RTL-SDR configuration options + + - `freq=` Desired tune frequency in Hz. Accepted range from 10M to 2.2G. +(default 100M: `100000000`) + - `gain=` (default `auto`) + - `auto` Selects gain automatically + - `list` Lists available gains and exit + - `` gain in dB. Possible gains in dB are: `0.0, 0.9, 1.4, 2.7, 3.7, +7.7, 8.7, 12.5, 14.4, 15.7, 16.6, 19.7, 20.7, 22.9, 25.4, 28.0, 29.7, 32.8, 33.8 +, 36.4, 37.2, 38.6, 40.2, 42.1, 43.4, 43.9, 44.5, 48.0, 49.6` + - `srate=` Device sample rate. valid values in the [900001, 3200000] range. (default `1152000`) + - `blklen=` Device block length in bytes (default RTL-SDR default i.e. 64k) + - `agc` Activates device AGC (default off) + - `antbias` Turn on the antenna bias for remote LNA (default off) + +## File Source driver + +* Reads an IQ signal format file and decode the output. +* *This device is still experimental.* + +### File Source configuration options + + - `freq=` Frequency of radio station in Hz. + - `srate=` IF sample rate in Hz. + - `filename=` Source file name. Supported encodings: `FLOAT`, `S24_LE`, `S16_LE` + - `zero_offset` Set if the source file is in zero offset, which requires Fs/4 IF shifting. + - `blklen=` Set block length in samples. + - `raw` Set if the file is raw binary. + - `format=` Set the file format for the raw binary file. Supported formats: `U8_LE`, `S8_LE`, `S16_LE`, `S24_LE`, `FLOAT` + +## Authors and contributors + +* Joris van Rantwijk, primary author of SoftFM +* Edouard Griffiths, F4EXB, primary author of NGSoftFM (no longer involving in maintaining NGSoftFM) +* Kenji Rikitake, JJ1BDX, maintainer/developer/experimenter +* AndrĂ¡s Retzler, HA7ILM, for the former AF/IF AGC code in [csdr](https://github.com/simonyiszk/csdr) +* Youssef Touil, Airspy Founder, aka Twitter [@lambdaprog](https://twitter.com/lambdaprog/), for the intriguing exchange of Airspy product design details and the technical support +* [Iowa Hills Software](http://iowahills.com), for their FIR and IIR filter design tools +* [Brian Beezley, K6STI](http://ham-radio.com/k6sti/), for his comprehensive Web site of FM broadcasting reception expertise and the idea of [Quadrature Multipath Monitor](http://ham-radio.com/k6sti/qmm.htm) +* [Ryuji Suzuki](https://github.com/rsuzuki0), for reviewing the FM multipath filter coefficients and suggesting putting more weight on picking up more previous samples from the reference point than the samples after +* [Teruhiko Hayashi, JA2SVZ](http://fpga.world.coocan.jp/FM/), the creator of FM FPGA Tuner popular in Japan, for reviewing the measurement results of FM broadcast reception of airspy-fmradion, and various constructive suggestions +* [Takehiro Sekine](https://github.com/bstalk), for suggesting using GNU Radio's [VOLK](https://www.libvolk.org/) for faster calculation, and implementing Filesource device driver +* [Takeru Ohta](https://github.com/sile), for his [Rust implementation](https://github.com/sile/dagc) of [Tisserand-Berviller AGC algorithm](https://hal.univ-lorraine.fr/hal-01397371/document) +* [Cameron Desrochers](https://github.com/cameron314), for his [readerwriterqueue](https://github.com/cameron314/readerwriterqueue) implementation of a single-producer-single-consumer lock-free queue for C++ +* [Clayton Smith](https://github.com/argilo), for [a bugfix pull request to airspy-fmradion to find an uninitialized variable](https://github.com/jj1bdx/airspy-fmradion/pull/43) and his help during [bug tracking in VOLK](https://github.com/gnuradio/volk/pull/695). +* [Andrew Hardin](https://github.com/andrew-hardin), for [cmake-git-version-tracking](https://github.com/andrew-hardin/cmake-git-version-tracking.git) + +## License + +* As a whole package: GPLv3 (and later). See [LICENSE](LICENSE). +* [csdr](https://github.com/simonyiszk/csdr) AGC code: BSD license. +* Some source code files are stating GPL "v2 and later" license, and the MIT License. + diff --git a/include/AudioOutput.h b/include/AudioOutput.h index 6c4b955..0581cf7 100644 --- a/include/AudioOutput.h +++ b/include/AudioOutput.h @@ -89,6 +89,9 @@ class SndfileOutput : public AudioOutput { virtual void output_close() override; private: + // Add sndfile log info to m_error and set m_zombie flag + void add_error_log_info(SNDFILE *sf); + const unsigned numberOfChannels; const unsigned sampleRate; int m_fd; @@ -98,6 +101,18 @@ class SndfileOutput : public AudioOutput { class PortAudioOutput : public AudioOutput { public: + // Static variables. + + // Minimum latency for audio output in seconds + + // Values of m_outputparams.suggestedLatency from PortAudio: + // Mac mini 2023 with macOS 14.3.1: 0.014717 + // Ubuntu 22.04.4 on x86_64: 0.034830 + // Kenji's experiments show that + // 40ms (0.04) is sufficient for macOS, Ubuntu, and Raspberry Pi OS + + static constexpr PaTime minimum_latency = 0.04; + // // Construct PortAudio output stream. // diff --git a/include/SoftFM.h b/include/SoftFM.h index b91c4b9..3aa931b 100644 --- a/include/SoftFM.h +++ b/include/SoftFM.h @@ -51,7 +51,10 @@ enum class OutputMode { RAW_FLOAT32, WAV_INT16, WAV_FLOAT32, - PORTAUDIO + PORTAUDIO, +#if defined(LIBSNDFILE_MP3_ENABLED) + MP3_FMAUDIO +#endif // LIBSNDFILE_MP3_ENABLED }; enum class PilotState { NotDetected, Detected }; diff --git a/libsndfile.md b/libsndfile.md new file mode 100644 index 0000000..7693177 --- /dev/null +++ b/libsndfile.md @@ -0,0 +1,34 @@ +# Installing the latest libsndfile + +You need to install the latest libsndfile for using the MP3 output file capability of airspy-fmradion. + +## MP3 capability + +* libsndfile 1.1 and later is MP3-capable when LAME is installed. +* libsndfile 1.0.31, the stock version for Ubuntu 22.04.4 LTS, *does not have* MP3 handling capability. + +## For macOS: use Homebrew + +```sh +brew install libsndfile +``` + +## Installing the latest libsndfile on Linux + +### For Ubuntu 22.04.4 LTS + +You need to install at least the related MP3 library: + +```sh +sudo apt install lame libmp3lame-dev +``` + +Use the following cmake command in the `CMakeBuild/` directory: + +```sh +cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DBUILD_SHARED_LIBS=ON \ + -DBUILD_TESTING=ON +``` + +[End of memorandum] diff --git a/libvolk.md b/libvolk.md index 1a24f69..a227f03 100644 --- a/libvolk.md +++ b/libvolk.md @@ -1,14 +1,17 @@ -# Installing the latest libvolk on macOS and Linux +# Installing the latest libvolk -## Compatible versions +## Incompatible versions + +*Do not use the following versions*: + +* 3.1.0 (due to NaN problem, use 3.1.1 and later) + +## Suggested compatible versions Use the following versions of VOLK aka libvolk: * 2.5.2 -* 3.0.0 -* 3.1.0 - -Note: libvolk 2.1 to 2.5.1 will work without issues, but no guarantee. +* 3.1.2 ## For x86_64 and M1 macOS: use Homebrew @@ -19,6 +22,7 @@ brew install volk ## For Linux * See [libvolk README.md](https://github.com/gnuradio/volk#readme). +* You need to install python3-mako as: `sudo apt install python3-mako` or `pip3 install mako` ## After installation diff --git a/main.cpp b/main.cpp index 4c3f6d8..1083775 100644 --- a/main.cpp +++ b/main.cpp @@ -48,7 +48,7 @@ // define this for enabling coefficient monitor functions // #undef COEFF_MONITOR -#define AIRSPY_FMRADION_VERSION "20240107-0" +#define AIRSPY_FMRADION_VERSION "20240316-0" // Flag to set graceful termination // in process_signals() @@ -88,6 +88,11 @@ static void usage() { " -G filename Write audio data to RF64/WAV FLOAT_LE file\n" " use filename '-' to write to stdout\n" " (Pipe is not supported)\n" +#if defined(LIBSNDFILE_MP3_ENABLED) + " -C filename Write audio data to MP3 file\n" + " of VBR -V 1 (experimental)\n" + " use filename '-' to write to stdout\n" +#endif // LIBSNDFILE_MP3_ENABLED " -P device_num Play audio via PortAudio device index number\n" " use string '-' to specify the default PortAudio " "device\n" @@ -119,7 +124,6 @@ static void usage() { " -r ppm Set IF offset in ppm (range: +-1000000ppm)\n" " (This option affects output pitch and timing:\n" " use for the output timing compensation only!)\n" - " -A (FM only) experimental 10Hz-step IF AFC\n" "\n" "Configuration options for RTL-SDR devices\n" " freq= Frequency of radio station in Hz (default 100000000)\n" @@ -284,7 +288,6 @@ int main(int argc, char **argv) { int multipathfilter_stages = 0; bool ifrate_offset_enable = false; double ifrate_offset_ppm = 0; - bool enable_fm_afc = false; std::string config_str; std::string devtype_str; DevType devtype; @@ -332,33 +335,46 @@ int main(int argc, char **argv) { } else { fprintf(stderr, "Git commit unknown\n"); } - fprintf(stderr, "VOLK_VERSION = %.6o\n", VOLK_VERSION); + fprintf(stderr, "VOLK Version = %u.%u.%u\n", VOLK_VERSION_MAJOR, + VOLK_VERSION_MINOR, VOLK_VERSION_MAINT); +#if defined(LIBSNDFILE_MP3_ENABLED) + fprintf(stderr, "libsndfile MP3 support enabled\n"); +#endif // LIBSNDFILE_MP3_ENABLED const struct option longopts[] = { - {"modtype", optional_argument, nullptr, 'm'}, - {"devtype", optional_argument, nullptr, 't'}, - {"quiet", required_argument, nullptr, 'q'}, - {"config", optional_argument, nullptr, 'c'}, - {"dev", required_argument, nullptr, 'd'}, - {"mono", no_argument, nullptr, 'M'}, - {"raw", required_argument, nullptr, 'R'}, - {"float", required_argument, nullptr, 'F'}, - {"wav", required_argument, nullptr, 'W'}, - {"wavfloat", required_argument, nullptr, 'G'}, - {"play", optional_argument, nullptr, 'P'}, - {"pps", required_argument, nullptr, 'T'}, - {"pilotshift", no_argument, nullptr, 'X'}, - {"usa", no_argument, nullptr, 'U'}, - {"filtertype", optional_argument, nullptr, 'f'}, - {"squelch", required_argument, nullptr, 'l'}, - {"multipathfilter", required_argument, nullptr, 'E'}, - {"ifrateppm", optional_argument, nullptr, 'r'}, - {"afc", optional_argument, nullptr, 'A'}, - {nullptr, no_argument, nullptr, 0}}; + {"modtype", optional_argument, nullptr, 'm'}, + {"devtype", optional_argument, nullptr, 't'}, + {"quiet", required_argument, nullptr, 'q'}, + {"config", optional_argument, nullptr, 'c'}, + {"dev", required_argument, nullptr, 'd'}, + {"mono", no_argument, nullptr, 'M'}, + {"raw", required_argument, nullptr, 'R'}, + {"float", required_argument, nullptr, 'F'}, + {"wav", required_argument, nullptr, 'W'}, + {"wavfloat", required_argument, nullptr, 'G'}, + {"play", optional_argument, nullptr, 'P'}, + {"pps", required_argument, nullptr, 'T'}, + {"pilotshift", no_argument, nullptr, 'X'}, + {"usa", no_argument, nullptr, 'U'}, + {"filtertype", optional_argument, nullptr, 'f'}, + {"squelch", required_argument, nullptr, 'l'}, + {"multipathfilter", required_argument, nullptr, 'E'}, + {"ifrateppm", optional_argument, nullptr, 'r'}, +#if defined(LIBSNDFILE_MP3_ENABLED) + {"mp3fmaudio", required_argument, nullptr, 'C'}, +#endif // LIBSNDFILE_MP3_ENABLED + {nullptr, no_argument, nullptr, 0} + }; int c, longindex; - while ((c = getopt_long(argc, argv, "m:t:c:d:MR:F:W:G:f:l:P:T:qXUE:r:A", - longopts, &longindex)) >= 0) { + +#if defined(LIBSNDFILE_MP3_ENABLED) + const char *optstring = "m:t:c:d:MR:F:W:G:f:l:P:T:qXUE:r:C:"; +#else // !LIBSNDFILE_MP3_ENABLED + const char *optstring = "m:t:c:d:MR:F:W:G:f:l:P:T:qXUE:r:"; +#endif // LIBSNDFILE_MP3_ENABLED + + while ((c = getopt_long(argc, argv, optstring, longopts, &longindex)) >= 0) { switch (c) { case 'm': modtype_str.assign(optarg); @@ -437,9 +453,12 @@ int main(int argc, char **argv) { badarg("-r"); } break; - case 'A': - enable_fm_afc = true; +#if defined(LIBSNDFILE_MP3_ENABLED) + case 'C': + outmode = OutputMode::MP3_FMAUDIO; + filename = optarg; break; +#endif // LIBSNDFILE_MP3_ENABLED default: usage(); fprintf(stderr, "ERROR: Invalid command line options\n"); @@ -595,6 +614,14 @@ int main(int argc, char **argv) { } fprintf(stderr, "name '%s'\n", audio_output->get_device_name().c_str()); break; +#if defined(LIBSNDFILE_MP3_ENABLED) + case OutputMode::MP3_FMAUDIO: + audio_output.reset(new SndfileOutput( + filename, pcmrate, stereo, SF_FORMAT_MPEG | SF_FORMAT_MPEG_LAYER_III)); + fprintf(stderr, "writing MP3 FM-broadcast audio samples to '%s'\n", + filename.c_str()); + break; +#endif // LIBSNDFILE_MP3_ENABLED } if (!(*audio_output)) { @@ -823,17 +850,10 @@ int main(int argc, char **argv) { const unsigned int ppm_average_stages = 100; MovingAverage ppm_average(ppm_average_stages, 0.0f); - // Initialize moving average object for FM AFC. - const unsigned int fm_afc_average_stages = 1000; - MovingAverage fm_afc_average(fm_afc_average_stages, 0.0f); - const unsigned int fm_afc_hz_step = 10; - FineTuner fm_afc_finetuner((unsigned int)fm_target_rate / fm_afc_hz_step); // Initialize moving average object for FM stereo pilot level monitoring. const unsigned int pilot_level_average_stages = 10; MovingAverage pilot_level_average(pilot_level_average_stages, 0.0f); - float fm_afc_offset_sum = 0.0; - float audio_level = 0; double block_time = Utility::get_time(); @@ -856,7 +876,6 @@ int main(int argc, char **argv) { // Pull next block from source buffer. IQSampleVector iqsamples = source_buffer.pull(); - IQSampleVector if_afc_samples; IQSampleVector if_shifted_samples; IQSampleVector if_downsampled_samples; IQSampleVector if_samples; @@ -878,27 +897,13 @@ int main(int argc, char **argv) { // so long as the stability of the receiver device is // within the range of +- 1ppm (~100Hz or less). - // Experimental FM broadcast AFC code - if (modtype == ModType::FM && enable_fm_afc) { - // get the frequency offset - fm_afc_average.feed(fm.get_tuning_offset()); - if ((block % fm_afc_average_stages) == 0) { - fm_afc_offset_sum += 0.7 * fm_afc_average.average(); - fm_afc_finetuner.set_freq_shift( - -((unsigned int)std::round(fm_afc_offset_sum / fm_afc_hz_step))); - } - fm_afc_finetuner.process(iqsamples, if_afc_samples); - } else { - if_afc_samples = std::move(iqsamples); - } - if (enable_fs_fourth_downconverter) { // Fs/4 downconvering is required // to avoid frequency zero offset // because Airspy HF+ and RTL-SDR are Zero IF receivers - fourth_downconverter.process(if_afc_samples, if_shifted_samples); + fourth_downconverter.process(iqsamples, if_shifted_samples); } else { - if_shifted_samples = std::move(if_afc_samples); + if_shifted_samples = std::move(iqsamples); } // Downsample IF for the decoder. diff --git a/sfmbase/AudioOutput.cpp b/sfmbase/AudioOutput.cpp index cbbd766..5d4b312 100644 --- a/sfmbase/AudioOutput.cpp +++ b/sfmbase/AudioOutput.cpp @@ -72,7 +72,7 @@ SndfileOutput::SndfileOutput(const std::string &filename, sf_command(m_sndfile, SFC_RF64_AUTO_DOWNGRADE, NULL, SF_TRUE)) { m_error = "unable to set SFC_RF64_AUTO_DOWNGRADE to SF_TRUE on '" + filename + "' (" + sf_strerror(m_sndfile) + ")"; - m_zombie = true; + add_error_log_info(m_sndfile); return; } } @@ -83,10 +83,37 @@ SndfileOutput::SndfileOutput(const std::string &filename, sf_command(m_sndfile, SFC_SET_UPDATE_HEADER_AUTO, NULL, SF_TRUE)) { m_error = "unable to set SFC_SET_UPDATE_HEADER_AUTO to SF_TRUE on '" + filename + "' (" + sf_strerror(m_sndfile) + ")"; - m_zombie = true; + add_error_log_info(m_sndfile); + return; + } + } + +#if defined(LIBSNDFILE_MP3_ENABLED) + // Set MP3 parameters here + if (filetype == SF_FORMAT_MPEG) { + fprintf(stderr, "Set MP3 parameters\n"); + // Set MP3 default format parameters to: + // Variable bitrate mode, compression level = 0.1 + int constant_mode = SF_BITRATE_MODE_VARIABLE; + double compression_level = 0.1; + // NOTE: you need to set SFC_SET_COMPRESSION_LEVEL *BEFORE* + // executing SFC_SET_BITRATE_MODE. + if (SF_TRUE != sf_command(m_sndfile, SFC_SET_COMPRESSION_LEVEL, + &compression_level, sizeof(double))) { + m_error = "unable to set SFC_SET_COMPRESSION_LEVEL on '" + filename + + "' (" + sf_strerror(m_sndfile) + ")"; + add_error_log_info(m_sndfile); + return; + } + if (SF_TRUE != sf_command(m_sndfile, SFC_SET_BITRATE_MODE, &constant_mode, + sizeof(int))) { + m_error = "unable to set SFC_SET_BITRATE_MODE on '" + filename + "' (" + + sf_strerror(m_sndfile) + ")"; + add_error_log_info(m_sndfile); return; } } +#endif // LIBSNDFILE_MP3_ENABLED m_device_name = "SndfileOutput"; } @@ -129,6 +156,20 @@ bool SndfileOutput::write(const SampleVector &samples) { return true; } +// Add sndfile log info to m_error and set m_zombie flag +void SndfileOutput::add_error_log_info(SNDFILE *sf) { + const int max_length = 8192; + char buffer[max_length]; + int length; + + length = sf_command(sf, SFC_GET_LOG_INFO, buffer, max_length); + std::string logmsg(buffer, length); + + m_error += "\n=== SFC_GET_LOG_INFO output:\n"; + m_error += logmsg; + m_error += "\n=== End of SFC_GET_LOG_INFO output\n"; + m_zombie = true; +} // Class PortAudioOutput // Construct PortAudio output stream. @@ -164,6 +205,13 @@ PortAudioOutput::PortAudioOutput(const PaDeviceIndex device_index, Pa_GetDeviceInfo(m_outputparams.device)->defaultHighOutputLatency; m_outputparams.hostApiSpecificStreamInfo = NULL; + // Guarantee minimum latency. + if (m_outputparams.suggestedLatency < minimum_latency) { + m_outputparams.suggestedLatency = minimum_latency; + } + + fprintf(stderr, "suggestedLatency = %f\n", m_outputparams.suggestedLatency); + m_paerror = Pa_OpenStream(&m_stream, NULL, // no input