Add button to record decoded audio. (#1145)

* Add button to record decoded audio.

* Fix TSan bug discovered in a merged PR's build.

* Make Record buttons horizontal.

* Add additional Tools menu option for decoder recording.

* Consolidate all Record logic to single button.

This commit does the following:

* Removes all Tools->Start Record File... options from menu bar.
* Reverts back to single Record button on main window.
* Record button now pops up file dialog with additional controls for raw/decoded recording.
  (default raw recording)

* Remove Quick Record options as they are no longer necessary.

* Revert "Remove Quick Record options as they are no longer necessary."

This reverts commit 0d0c069d48.

* Add additional config option for decoded recordings.

* Replace file chooser with simpler record setup dialog.

* Remove unused config items.

* Add PR #1145 to changelog.

* Build LAME and mpg123 into FreeDV version of libsndfile.

* Add MP3 format as option for recording.

* Update control labels.

* Fix non-macOS build.

* Fix mpg123 build as well.

* Fix Windows build.

* Disable MP3 support if system sndfile doesn't support it.

* Need to invert test.

* Try regenerating autoconf files for mpg123 to work around Windows build failure.

* Need libltdl-dev for autoreconf to work.

* Disable MP3 recording when recording raw audio.

* Swap ordering of filters for the voice keyer dialogs.

* Allow voice keyer to be turned on during TX.

* Allow voice keyer file to be selected during TX.

* Default suffix to drop-down list entry or FreeDV Reporter selection.

* Revert "Swap ordering of filters for the voice keyer dialogs."

This reverts commit a504063ff2.

* Try installing git from Homebrew to work around macOS CI problem.

* Revert "Try installing git from Homebrew to work around macOS CI problem."

This reverts commit e4cdeb47f1.

* Force not retrieving submodules.

* Explicitly disable submodule recursion.

* Try macos-26 for lint.

* Uninstall Homebrew git due to libiconv problems.
pull/1170/head
Mooneer Salem 2026-01-08 10:07:26 -08:00 committed by GitHub
parent 53bfdbd25d
commit 8a240f8a99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 690 additions and 263 deletions

View File

@ -23,6 +23,7 @@ jobs:
- name: Install needed packages
run: |
brew install automake libtool numpy sox wxwidgets speexdsp portaudio libsndfile libsamplerate hamlib libebur128
brew uninstall git # workaround, forces use of Apple git
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
@ -37,7 +38,7 @@ jobs:
cd build_osx
CC=clang CXX=clang++ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 ..
# Partially build so we have all required .h and .c/.cpp files
make -j$(nproc) build_rade build_codec2 build_rade_integ
make -j$(sysctl -n hw.logicalcpu) build_rade build_codec2 build_rade_integ
- name: Run clang-tidy on codebase
working-directory: ${{github.workspace}}/build_osx
@ -65,6 +66,7 @@ jobs:
- name: Install needed packages
run: |
brew install automake libtool numpy sox wxwidgets speexdsp portaudio libsndfile libsamplerate hamlib libebur128
brew uninstall git # workaround, forces use of Apple git
- name: Install VB-Cable
shell: bash
@ -137,6 +139,7 @@ jobs:
- name: Install needed packages
run: |
brew install automake libtool numpy sox
brew uninstall git # workaround, forces use of Apple git
- name: Initialize code signing infrastructure
if: ${{ vars.FREEDV_MACOS_CODE_SIGNING_TEAM_ID != '' }}
@ -214,6 +217,7 @@ jobs:
- name: Install needed packages
run: |
brew install automake libtool sox
brew uninstall git # workaround, forces use of Apple git
- name: Install VB-Cable
shell: bash

View File

@ -39,7 +39,7 @@ jobs:
- name: Install required packages
shell: bash
working-directory: ${{github.workspace}}
run: sudo apt-get install nsis
run: sudo apt-get install nsis autoconf automake libtool libltdl-dev
- name: Configure WINE
shell: bash

View File

@ -555,7 +555,30 @@ if(NOT USE_STATIC_SNDFILE)
find_path(LIBSNDFILE_INCLUDE_DIR sndfile.h)
message(STATUS " sndfile library: ${LIBSNDFILE}")
message(STATUS " sndfile headers: ${LIBSNDFILE_INCLUDE_DIR}")
if(LIBSNDFILE AND LIBSNDFILE_INCLUDE_DIR)
# Some Linux distros don't include MP3 support for libsamplefile.
# Check for this, and if we don't have the needed config, disable
# MP3 support in the app.
if (NOT CMAKE_CROSSCOMPILING)
if (CMAKE_VERSION VERSION_LESS 3.25)
set(_SNDFILE_TRY_RUN_BINDIR "${CMAKE_CURRENT_BINARY_DIR}/SndfileTryRun")
message(STATUS "Using old try_run syntax for CMake ${CMAKE_VERSION}")
else()
set(_SNDFILE_TRY_RUN_BINDIR "SOURCES")
message(STATUS "Using new try_run syntax for CMake ${CMAKE_VERSION}")
endif(CMAKE_VERSION VERSION_LESS 3.25)
try_run(SAMPLEFILE_RUN_RESULT SAMPLEFILE_COMPILE_RESULT
${_SNDFILE_TRY_RUN_BINDIR} ${CMAKE_SOURCE_DIR}/cmake/sndfile-test.c
CMAKE_FLAGS -DINCLUDE_DIRECTORIES=${LIBSNDFILE_INCLUDE_DIR}
LINK_LIBRARIES ${LIBSNDFILE}
RUN_OUTPUT_VARIABLE SNDFILE_MP3_ENABLED)
if (NOT SAMPLEFILE_COMPILE_RESULT OR NOT SNDFILE_MP3_ENABLED)
message(STATUS "No MP3 support in sndfile, disabling.")
add_definitions(-DSNDFILE_NO_MP3_SUPPORT)
endif (NOT SAMPLEFILE_COMPILE_RESULT OR NOT SNDFILE_MP3_ENABLED)
endif (NOT CMAKE_CROSSCOMPILING)
list(APPEND FREEDV_LINK_LIBS ${LIBSNDFILE})
else(LIBSNDFILE AND LIBSNDFILE_INCLUDE_DIR)
message(STATUS "Will attempt static build of sndfile.")

View File

@ -901,6 +901,7 @@ LDPC | Low Density Parity Check Codes - a family of powerful FEC codes
* FreeDV Reporter: Add idle filter. (PR #1142)
* Flex: allow Ctrl-C to clean up the waveform. (PR #1144, #1146, #1160) - thanks @arodland!
* Flex: allow FreeDV Reporter parameters to be overridden via command line. (PR #1154)
* Add support for recording decoded audio. (PR #1145)
* Flex: add command line option to adjust RX volume. (PR #1159)
3. Build system:
* Use Clang to build AppImages for better performance. (PR #1149)

View File

@ -14,6 +14,8 @@ ExternalProject_Add(build_ebur128
BINARY_DIR ebur128_build
GIT_REPOSITORY https://github.com/jiixyj/libebur128.git
GIT_TAG "v${EBUR128_VERSION}"
GIT_SUBMODULES ""
GIT_SUBMODULES_RECURSE NO
CMAKE_ARGS ${EBUR128_CMAKE_ARGS}
CMAKE_CACHE_ARGS -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=${CMAKE_OSX_DEPLOYMENT_TARGET} -DCMAKE_OSX_ARCHITECTURES:STRING=${CMAKE_OSX_ARCHITECTURES}
PATCH_COMMAND git apply ${CMAKE_SOURCE_DIR}/cmake/Ebur128_CMake.patch

View File

@ -0,0 +1,70 @@
set(CONFIGURE_COMMAND ./configure --disable-shared --prefix=${CMAKE_BINARY_DIR}/external/dist)
if (CMAKE_CROSSCOMPILING)
set(CONFIGURE_COMMAND ${CONFIGURE_COMMAND} --host=${CMAKE_C_COMPILER_TARGET} --target=${CMAKE_C_COMPILER_TARGET})
endif (CMAKE_CROSSCOMPILING)
set(LAME_URL https://download.sourceforge.net/project/lame/lame/3.100/lame-3.100.tar.gz)
set(LAME_HASH SHA512=0844b9eadb4aacf8000444621451277de365041cc1d97b7f7a589da0b7a23899310afd4e4d81114b9912aa97832621d20588034715573d417b2923948c08634b)
include(ExternalProject)
if(APPLE AND BUILD_OSX_UNIVERSAL)
# mp3lame doesn't behave properly when trying to build a universal binary;
# build it twice and use lipo to create a universal library instead.
ExternalProject_Add(build_mp3lame_x86
DOWNLOAD_EXTRACT_TIMESTAMP NO
BUILD_IN_SOURCE 1
CONFIGURE_COMMAND ${CONFIGURE_COMMAND} --host=x86_64-apple-darwin --target=x86_64-apple-darwin CFLAGS=-arch\ x86_64\ -O2\ -mmacosx-version-min=10.11
BUILD_COMMAND $(MAKE) && $(MAKE) install
INSTALL_COMMAND ""
URL ${LAME_URL}
URL_HASH ${LAME_HASH}
)
ExternalProject_Add(build_mp3lame_arm
DOWNLOAD_EXTRACT_TIMESTAMP NO
BUILD_IN_SOURCE 1
CONFIGURE_COMMAND ${CONFIGURE_COMMAND} --host=aarch64-apple-darwin --target=aarch64-apple-darwin CFLAGS=-arch\ arm64\ -O2\ -mmacosx-version-min=10.11
BUILD_COMMAND $(MAKE) && $(MAKE) install
INSTALL_COMMAND ""
URL ${LAME_URL}
URL_HASH ${LAME_HASH}
)
ExternalProject_Get_Property(build_mp3lame_arm SOURCE_DIR)
set(LAME_ARM_BINARY_DIR ${SOURCE_DIR})
ExternalProject_Get_Property(build_mp3lame_x86 SOURCE_DIR)
set(LAME_X86_BINARY_DIR ${SOURCE_DIR})
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/libmp3lame${CMAKE_STATIC_LIBRARY_SUFFIX}
COMMAND lipo ${LAME_ARM_BINARY_DIR}/libmp3lame/.libs/libmp3lame${CMAKE_STATIC_LIBRARY_SUFFIX} ${LAME_X86_BINARY_DIR}/libmp3lame/.libs/libmp3lame${CMAKE_STATIC_LIBRARY_SUFFIX} -output ${CMAKE_BINARY_DIR}/libmp3lame${CMAKE_STATIC_LIBRARY_SUFFIX} -create && cp ${CMAKE_BINARY_DIR}/libmp3lame${CMAKE_STATIC_LIBRARY_SUFFIX} ${CMAKE_BINARY_DIR}/external/dist/lib
DEPENDS build_mp3lame_arm build_mp3lame_x86)
add_custom_target(
libmp3lame.a
DEPENDS ${CMAKE_BINARY_DIR}/libmp3lame${CMAKE_STATIC_LIBRARY_SUFFIX})
add_library(mp3lame STATIC IMPORTED)
add_dependencies(mp3lame libmp3lame.a)
set_target_properties(mp3lame PROPERTIES
IMPORTED_LOCATION "${CMAKE_BINARY_DIR}/external/dist/lib/libmp3lame${CMAKE_STATIC_LIBRARY_SUFFIX}"
)
else(APPLE AND BUILD_OSX_UNIVERSAL)
ExternalProject_Add(build_mp3lame
BUILD_IN_SOURCE 1
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
BUILD_COMMAND $(MAKE) && $(MAKE) install
URL ${LAME_URL}
URL_HASH ${LAME_HASH}
)
add_library(mp3lame STATIC IMPORTED)
add_dependencies(mp3lame build_mp3lame)
set_target_properties(mp3lame PROPERTIES
IMPORTED_LOCATION "${CMAKE_BINARY_DIR}/external/dist/lib/libmp3lame${CMAKE_STATIC_LIBRARY_SUFFIX}"
IMPORTED_IMPLIB "${CMAKE_BINARY_DIR}/external/dist/lib/libmp3lame${CMAKE_STATIC_LIBRARY_SUFFIX}"
)
endif(APPLE AND BUILD_OSX_UNIVERSAL)

View File

@ -0,0 +1,72 @@
set(CONFIGURE_COMMAND autoreconf -i && ./configure --disable-shared --disable-components --enable-libmpg123 --prefix=${CMAKE_BINARY_DIR}/external/dist)
if (CMAKE_CROSSCOMPILING)
set(CONFIGURE_COMMAND ${CONFIGURE_COMMAND} --host=${CMAKE_C_COMPILER_TARGET} --target=${CMAKE_C_COMPILER_TARGET})
endif (CMAKE_CROSSCOMPILING)
set(MPG123_URL https://download.sourceforge.net/project/mpg123/mpg123/1.33.4/mpg123-1.33.4.tar.bz2)
set(MPG123_HASH SHA512=9b7aa93b692132da7eb8dcfef12ce91bf66bf8475af50e9c57d7b80225f96c0e74264e518e558371af1f4cf6d2afda5b3dfc844949fd747db7ac7c44d0e9f6ad)
include(ExternalProject)
if(APPLE AND BUILD_OSX_UNIVERSAL)
# mpg123 build doesn't behave properly when trying to build a universal binary;
# build it twice and use lipo to create a universal library instead.
ExternalProject_Add(build_mpg123_x86
DOWNLOAD_EXTRACT_TIMESTAMP NO
BUILD_IN_SOURCE 1
CONFIGURE_COMMAND ${CONFIGURE_COMMAND} --host=x86_64-apple-darwin --target=x86_64-apple-darwin CFLAGS=-arch\ x86_64\ -O2\ -mmacosx-version-min=10.11
BUILD_COMMAND $(MAKE) && $(MAKE) install
INSTALL_COMMAND ""
URL ${MPG123_URL}
URL_HASH ${MPG123_HASH}
)
ExternalProject_Add(build_mpg123_arm
DOWNLOAD_EXTRACT_TIMESTAMP NO
BUILD_IN_SOURCE 1
CONFIGURE_COMMAND ${CONFIGURE_COMMAND} --host=aarch64-apple-darwin --target=aarch64-apple-darwin CFLAGS=-arch\ arm64\ -O2\ -mmacosx-version-min=10.11
BUILD_COMMAND $(MAKE) && $(MAKE) install
INSTALL_COMMAND ""
URL ${MPG123_URL}
URL_HASH ${MPG123_HASH}
)
ExternalProject_Get_Property(build_mpg123_arm SOURCE_DIR)
set(MPG123_ARM_BINARY_DIR ${SOURCE_DIR})
ExternalProject_Get_Property(build_mpg123_x86 SOURCE_DIR)
set(MPG123_X86_BINARY_DIR ${SOURCE_DIR})
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/libmpg123${CMAKE_STATIC_LIBRARY_SUFFIX}
COMMAND lipo ${MPG123_ARM_BINARY_DIR}/src/libmpg123/.libs/libmpg123${CMAKE_STATIC_LIBRARY_SUFFIX} ${MPG123_X86_BINARY_DIR}/src/libmpg123/.libs/libmpg123${CMAKE_STATIC_LIBRARY_SUFFIX} -output ${CMAKE_BINARY_DIR}/libmpg123${CMAKE_STATIC_LIBRARY_SUFFIX} -create && cp ${CMAKE_BINARY_DIR}/libmpg123${CMAKE_STATIC_LIBRARY_SUFFIX} ${CMAKE_BINARY_DIR}/external/dist/lib
DEPENDS build_mpg123_arm build_mpg123_x86)
add_custom_target(
libmpg123.a
DEPENDS ${CMAKE_BINARY_DIR}/libmpg123${CMAKE_STATIC_LIBRARY_SUFFIX})
add_library(mpg123 STATIC IMPORTED)
add_dependencies(mpg123 libmpg123.a)
set_target_properties(mpg123 PROPERTIES
IMPORTED_LOCATION "${CMAKE_BINARY_DIR}/external/dist/lib/libmpg123${CMAKE_STATIC_LIBRARY_SUFFIX}"
)
else(APPLE AND BUILD_OSX_UNIVERSAL)
ExternalProject_Add(build_mpg123
BUILD_IN_SOURCE 1
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
BUILD_COMMAND $(MAKE) && $(MAKE) install
URL ${MPG123_URL}
URL_HASH ${MPG123_HASH}
)
add_library(mpg123 STATIC IMPORTED)
add_dependencies(mpg123 build_mpg123)
set_target_properties(mpg123 PROPERTIES
IMPORTED_LOCATION "${CMAKE_BINARY_DIR}/external/dist/lib/libmpg123${CMAKE_STATIC_LIBRARY_SUFFIX}"
IMPORTED_IMPLIB "${CMAKE_BINARY_DIR}/external/dist/lib/libmpg123${CMAKE_STATIC_LIBRARY_SUFFIX}"
)
endif(APPLE AND BUILD_OSX_UNIVERSAL)
set(MPG123_ROOT ${CMAKE_BINARY_DIR}/external/dist)

View File

@ -16,6 +16,8 @@ ExternalProject_Add(build_rade
BINARY_DIR rade_build
GIT_REPOSITORY https://github.com/drowe67/radae.git
GIT_TAG main
GIT_SUBMODULES ""
GIT_SUBMODULES_RECURSE NO
CMAKE_ARGS ${RADE_CMAKE_ARGS}
#CMAKE_CACHE_ARGS -DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=${CMAKE_OSX_DEPLOYMENT_TARGET}
INSTALL_COMMAND ""

View File

@ -16,6 +16,8 @@ ExternalProject_Add(build_rade_integ
BINARY_DIR rade_integ_build
GIT_REPOSITORY https://github.com/drowe67/radae.git
GIT_TAG ms-disable-python-gc
GIT_SUBMODULES ""
GIT_SUBMODULES_RECURSE NO
CMAKE_ARGS ${RADE_CMAKE_ARGS}
#CMAKE_CACHE_ARGS -DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=${CMAKE_OSX_DEPLOYMENT_TARGET}
INSTALL_COMMAND ""

View File

@ -1,24 +1,21 @@
set(SNDFILE_TARBALL "libsndfile-1.0.28")
# Ensure that MP3 support is built into libsndfile
include(cmake/BuildMpg123.cmake)
include(cmake/BuildLame.cmake)
if(MINGW AND CMAKE_CROSSCOMPILING)
set(CONFIGURE_COMMAND autoreconf -i && ./configure --host=${HOST} --prefix=${CMAKE_BINARY_DIR}/external/dist --disable-external-libs --disable-shared --disable-sqlite)
elseif(APPLE)
if(BUILD_OSX_UNIVERSAL)
set(CONFIGURE_COMMAND autoreconf -i && ./configure --prefix=${CMAKE_BINARY_DIR}/external/dist --disable-shared --disable-external-libs CFLAGS=-g\ -O2\ -mmacosx-version-min=10.9\ -arch\ x86_64\ -arch\ arm64 LDFLAGS=-arch\ x86_64\ -arch\ arm64)
else()
set(CONFIGURE_COMMAND autoreconf -i && ./configure --prefix=${CMAKE_BINARY_DIR}/external/dist --disable-shared --disable-external-libs CFLAGS=-g\ -O2\ -mmacosx-version-min=10.9)
endif(BUILD_OSX_UNIVERSAL)
else()
set(CONFIGURE_COMMAND autoreconf -i && ./configure --prefix=${CMAKE_BINARY_DIR}/external/dist --disable-external-libs --disable-shared --disable-external-libs)
set(SNDFILE_CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/external/dist -DBUILD_SHARED_LIBS=OFF -DBUILD_PROGRAMS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_TESTING=OFF -DENABLE_CPACK=OFF -DENABLE_EXTERNAL_LIBS=OFF -Dmp3lame_ROOT=${CMAKE_BINARY_DIR}/external/dist -DLAME_ROOT=${CMAKE_BINARY_DIR}/external/dist -Dmpg123_ROOT=${CMAKE_BINARY_DIR}/external/dist)
if(CMAKE_CROSSCOMPILING)
set(SNDFILE_CMAKE_ARGS ${SNDFILE_CMAKE_ARGS} -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE})
endif()
include(ExternalProject)
ExternalProject_Add(sndfile
URL http://www.mega-nerd.com/libsndfile/files/${SNDFILE_TARBALL}.tar.gz
URL https://github.com/libsndfile/libsndfile/archive/refs/tags/1.2.2.tar.gz
BUILD_IN_SOURCE 1
INSTALL_DIR external/dist
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
BUILD_COMMAND $(MAKE) V=1
PATCH_COMMAND patch -p1 < ${CMAKE_SOURCE_DIR}/cmake/sndfile-cmake.patch
CMAKE_ARGS ${SNDFILE_CMAKE_ARGS}
CMAKE_CACHE_ARGS -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=${CMAKE_OSX_DEPLOYMENT_TARGET} -DCMAKE_OSX_ARCHITECTURES:STRING=${CMAKE_OSX_ARCHITECTURES}
BUILD_COMMAND $(MAKE)
INSTALL_COMMAND $(MAKE) install
)
if(MINGW)
@ -28,5 +25,7 @@ else()
endif()
include_directories(${CMAKE_BINARY_DIR}/external/dist/include)
list(APPEND FREEDV_LINK_LIBS ${SNDFILE_LIBRARIES})
list(APPEND FREEDV_STATIC_DEPS sndfile)
list(APPEND FREEDV_LINK_LIBS mp3lame mpg123 ${SNDFILE_LIBRARIES})
list(APPEND FREEDV_STATIC_DEPS mpg123 mp3lame sndfile)
add_dependencies(sndfile mpg123 mp3lame)

View File

@ -0,0 +1,9 @@
diff -ur sndfile.orig/CMakeLists.txt sndfile/CMakeLists.txt
--- sndfile.orig/CMakeLists.txt 2023-08-13 01:53:51
+++ sndfile/CMakeLists.txt 2026-01-04 18:02:53
@@ -1,4 +1,4 @@
-cmake_minimum_required (VERSION 3.1..3.18)
+cmake_minimum_required (VERSION 3.18)
# MSVC runtime library flags are selected by an abstraction, CMake >= 3.15
# This policy still need to be set even with cmake_minimum_required() command above.

View File

@ -0,0 +1,8 @@
#include <stdio.h>
#include <sndfile.h>
int main()
{
printf("SF_FORMAT_MPEG_LAYER_III = %d\n", SF_FORMAT_MPEG_LAYER_III);
return 0;
}

View File

@ -64,10 +64,6 @@ FreeDVConfiguration::FreeDVConfiguration()
/* Recording settings */
, playFileToMicInPath("/File/playFileToMicInPath", _(""))
, recFileFromRadioPath("/File/recFileFromRadioPath", _(""))
, recFileFromRadioSecs("/File/recFileFromRadioSecs", 60)
, recFileFromModulatorPath("/File/recFileFromModulatorPath", _(""))
, recFileFromModulatorSecs("/File/recFileFromModulatorSecs", 60)
, playFileFromRadioPath("/File/playFileFromRadioPath", _(""))
, enableSpaceBarForPTT("/Rig/EnableSpacebarForPTT", true)
@ -81,7 +77,8 @@ FreeDVConfiguration::FreeDVConfiguration()
, multipleReceiveEnabled("/Rig/MultipleRx", true)
, multipleReceiveOnSingleThread("/Rig/SingleRxThread", true)
, quickRecordPath("/QuickRecord/SavePath", _(""))
, quickRecordRawPath("/QuickRecord/SavePath", _(""))
, quickRecordDecodedPath("/QuickRecord/SaveDecodedPath", _(""))
, freedv700Clip("/FreeDV700/txClip", true)
, freedv700TxBPF("/FreeDV700/txBPF", true)
@ -156,10 +153,6 @@ void FreeDVConfiguration::load(wxConfigBase* config)
load_(config, transmitLevel);
load_(config, playFileToMicInPath);
load_(config, recFileFromRadioPath);
load_(config, recFileFromRadioSecs);
load_(config, recFileFromModulatorPath);
load_(config, recFileFromModulatorSecs);
load_(config, playFileFromRadioPath);
load_(config, enableSpaceBarForPTT);
@ -233,8 +226,10 @@ void FreeDVConfiguration::load(wxConfigBase* config)
load_(config, monitorVoiceKeyerAudioVol);
load_(config, monitorTxAudioVol);
quickRecordPath.setDefaultVal(documentsDir);
load_(config, quickRecordPath);
quickRecordRawPath.setDefaultVal(documentsDir);
quickRecordDecodedPath.setDefaultVal(documentsDir);
load_(config, quickRecordRawPath);
load_(config, quickRecordDecodedPath);
load_(config, experimentalFeatures);
load_(config, tabLayout);
@ -283,10 +278,6 @@ void FreeDVConfiguration::save(wxConfigBase* config)
save_(config, transmitLevel);
save_(config, playFileToMicInPath);
save_(config, recFileFromRadioPath);
save_(config, recFileFromRadioSecs);
save_(config, recFileFromModulatorPath);
save_(config, recFileFromModulatorSecs);
save_(config, playFileFromRadioPath);
save_(config, enableSpaceBarForPTT);
@ -300,7 +291,8 @@ void FreeDVConfiguration::save(wxConfigBase* config)
save_(config, multipleReceiveEnabled);
save_(config, multipleReceiveOnSingleThread);
save_(config, quickRecordPath);
save_(config, quickRecordRawPath);
save_(config, quickRecordDecodedPath);
save_(config, freedv700Clip);
save_(config, freedv700TxBPF);

View File

@ -70,10 +70,6 @@ public:
ConfigurationDataElement<int> transmitLevel;
ConfigurationDataElement<wxString> playFileToMicInPath;
ConfigurationDataElement<wxString> recFileFromRadioPath;
ConfigurationDataElement<unsigned int> recFileFromRadioSecs;
ConfigurationDataElement<wxString> recFileFromModulatorPath;
ConfigurationDataElement<unsigned int> recFileFromModulatorSecs;
ConfigurationDataElement<wxString> playFileFromRadioPath;
ConfigurationDataElement<bool> enableSpaceBarForPTT;
@ -87,7 +83,8 @@ public:
ConfigurationDataElement<bool> multipleReceiveEnabled;
ConfigurationDataElement<bool> multipleReceiveOnSingleThread;
ConfigurationDataElement<wxString> quickRecordPath;
ConfigurationDataElement<wxString> quickRecordRawPath;
ConfigurationDataElement<wxString> quickRecordDecodedPath;
ConfigurationDataElement<bool> freedv700Clip;
ConfigurationDataElement<bool> freedv700TxBPF;

View File

@ -1,4 +1,5 @@
add_library(fdv_gui_dialogs STATIC
begin_recording.cpp
dlg_audiooptions.cpp
dlg_easy_setup.cpp
dlg_filter.cpp

View File

@ -0,0 +1,154 @@
//==========================================================================
// Name: begin_recording.cpp
// Purpose: Dialog for setting up recordings.
// Created: January 4, 2026
// Authors: Mooneer Salem
//
// License:
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2.1,
// as published by the Free Software Foundation. This program is
// distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
// License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <http://www.gnu.org/licenses/>.
//
//==========================================================================
#include <wx/wx.h>
#include "begin_recording.h"
BeginRecordingDialog::BeginRecordingDialog(wxWindow* parent, wxString const& defaultRecordingSuffix)
: wxDialog(parent, wxID_ANY, _("Start Recording"), wxDefaultPosition, wxSize(250,-1), wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER|wxTAB_TRAVERSAL)
{
// XXX - FreeDV only supports English but makes a best effort to at least use regional formatting
// for e.g. numbers. Thus, we only need to override layout direction.
SetLayoutDirection(wxLayout_LeftToRight);
// Create top-level of control hierarchy.
wxPanel* panel = new wxPanel(this);
wxBoxSizer* sectionSizer = new wxBoxSizer(wxVERTICAL);
wxStaticBox* recordingSettingsBox = new wxStaticBox(panel, wxID_ANY, _("Recording Settings"));
wxStaticBoxSizer* recordingSettingsBoxSizer = new wxStaticBoxSizer(recordingSettingsBox, wxVERTICAL);
wxFlexGridSizer* gridSizerRecordingSettings = new wxFlexGridSizer(3, 2, 5, 5);
gridSizerRecordingSettings->AddGrowableCol(1);
// Recording suffix
wxStaticText* labelRecordingSuffix = new wxStaticText(recordingSettingsBox, wxID_ANY, wxT("Recording suffix:"), wxDefaultPosition, wxSize(125,-1), 0);
gridSizerRecordingSettings->Add(labelRecordingSuffix, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT, 2);
recordingSuffix_ = new wxTextCtrl(recordingSettingsBox, wxID_ANY, defaultRecordingSuffix, wxDefaultPosition, wxSize(125, -1), 0);
gridSizerRecordingSettings->Add(recordingSuffix_, 0, wxALIGN_CENTER_VERTICAL | wxEXPAND, 2);
wxStaticText* labelRecordingType = new wxStaticText(recordingSettingsBox, wxID_ANY, wxT("Recording type:"), wxDefaultPosition, wxSize(125,-1), 0);
gridSizerRecordingSettings->Add(labelRecordingType, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT, 2);
wxBoxSizer* typeSizer = new wxBoxSizer(wxHORIZONTAL);
rawRecording_ = new wxRadioButton(recordingSettingsBox, wxID_ANY, _("Off Air"));
rawRecording_->SetValue(true);
typeSizer->Add(rawRecording_, 0, wxALL | wxALIGN_CENTER_VERTICAL, 2);
decodedRecording_ = new wxRadioButton(recordingSettingsBox, wxID_ANY, _("Decoded"));
typeSizer->Add(decodedRecording_, 0, wxALL | wxALIGN_CENTER_VERTICAL, 2);
gridSizerRecordingSettings->Add(typeSizer, 0, wxALIGN_CENTER_VERTICAL, 2);
wxStaticText* labelRecordingFormat = new wxStaticText(recordingSettingsBox, wxID_ANY, wxT("Recording format:"), wxDefaultPosition, wxSize(125,-1), 0);
gridSizerRecordingSettings->Add(labelRecordingFormat, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT, 2);
wxBoxSizer* formatSizer = new wxBoxSizer(wxHORIZONTAL);
formatWav_ = new wxRadioButton(recordingSettingsBox, wxID_ANY, _("WAV"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
formatWav_->SetValue(true);
formatSizer->Add(formatWav_, 0, wxALL | wxALIGN_CENTER_VERTICAL, 2);
formatMp3_ = new wxRadioButton(recordingSettingsBox, wxID_ANY, _("MP3"));
formatMp3_->Enable(false);
formatSizer->Add(formatMp3_, 0, wxALL | wxALIGN_CENTER_VERTICAL, 2);
gridSizerRecordingSettings->Add(formatSizer, 0, wxALIGN_CENTER_VERTICAL, 2);
recordingSettingsBoxSizer->Add(gridSizerRecordingSettings, 0, wxEXPAND | wxALIGN_LEFT, 2);
sectionSizer->Add(recordingSettingsBoxSizer, 0, wxALL | wxEXPAND, 2);
// OK/Cancel buttons
wxBoxSizer* buttonSizer = new wxBoxSizer(wxHORIZONTAL);
m_buttonOK = new wxButton(panel, wxID_ANY, _("Start"));
buttonSizer->Add(m_buttonOK, 0, wxALL, 2);
m_buttonCancel = new wxButton(panel, wxID_CANCEL);
buttonSizer->Add(m_buttonCancel, 0, wxALL, 2);
sectionSizer->Add(buttonSizer, 0, wxALL | wxALIGN_CENTER, 2);
// Trigger auto-layout of window.
// ==============================
panel->SetSizer(sectionSizer);
wxBoxSizer* panelSizer = new wxBoxSizer(wxVERTICAL);
panelSizer->Add(panel, 0, wxEXPAND, 0);
this->SetSizerAndFit(panelSizer);
this->Layout();
this->Centre(wxBOTH);
this->SetMinSize(GetBestSize());
// Hook in events
this->Connect(wxEVT_INIT_DIALOG, wxInitDialogEventHandler(BeginRecordingDialog::OnInitDialog));
this->Connect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(BeginRecordingDialog::OnClose));
rawRecording_->Connect(wxEVT_RADIOBUTTON, wxCommandEventHandler(BeginRecordingDialog::OnRecordingTypeChange), NULL, this);
decodedRecording_->Connect(wxEVT_RADIOBUTTON, wxCommandEventHandler(BeginRecordingDialog::OnRecordingTypeChange), NULL, this);
m_buttonOK->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(BeginRecordingDialog::OnOK), NULL, this);
m_buttonCancel->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(BeginRecordingDialog::OnCancel), NULL, this);
}
BeginRecordingDialog::~BeginRecordingDialog()
{
this->Disconnect(wxEVT_INIT_DIALOG, wxInitDialogEventHandler(BeginRecordingDialog::OnInitDialog));
this->Disconnect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler(BeginRecordingDialog::OnClose));
rawRecording_->Disconnect(wxEVT_RADIOBUTTON, wxCommandEventHandler(BeginRecordingDialog::OnRecordingTypeChange), NULL, this);
decodedRecording_->Disconnect(wxEVT_RADIOBUTTON, wxCommandEventHandler(BeginRecordingDialog::OnRecordingTypeChange), NULL, this);
m_buttonOK->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(BeginRecordingDialog::OnOK), NULL, this);
m_buttonCancel->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(BeginRecordingDialog::OnCancel), NULL, this);
}
void BeginRecordingDialog::OnInitDialog(wxInitDialogEvent&)
{
// empty
}
void BeginRecordingDialog::OnCancel(wxCommandEvent&)
{
this->EndModal(wxCANCEL);
}
void BeginRecordingDialog::OnClose(wxCloseEvent&)
{
this->EndModal(wxCANCEL);
}
void BeginRecordingDialog::OnOK(wxCommandEvent&)
{
this->EndModal(wxOK);
}
void BeginRecordingDialog::OnRecordingTypeChange(wxCommandEvent&)
{
#if !defined(SNDFILE_NO_MP3_SUPPORT)
if (rawRecording_->GetValue())
{
formatMp3_->Enable(false);
formatWav_->SetValue(true);
}
else
{
formatMp3_->Enable(true);
}
#endif // !defined(SNDFILE_NO_MP3_SUPPORT)
}

View File

@ -0,0 +1,62 @@
//==========================================================================
// Name: begin_recording.h
// Purpose: Dialog for setting up recordings.
// Created: January 4, 2026
// Authors: Mooneer Salem
//
// License:
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2.1,
// as published by the Free Software Foundation. This program is
// distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
// License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <http://www.gnu.org/licenses/>.
//
//==========================================================================
#ifndef BEGIN_RECORDING_DIALOG_H
#define BEGIN_RECORDING_DIALOG_H
#include "main.h"
#include "defines.h"
#include "../../logging/ILogger.h"
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=
// Class BeginRecordingDialog
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=
class BeginRecordingDialog : public wxDialog
{
public:
BeginRecordingDialog( wxWindow* parent, wxString const& defaultRecordingSuffix );
virtual ~BeginRecordingDialog();
bool isRawRecording() const { return rawRecording_->GetValue(); }
wxString getRecordingSuffix() const { return recordingSuffix_->GetValue(); }
bool isMp3Format() const { return formatMp3_->GetValue(); }
protected:
// Handlers for events.
void OnOK(wxCommandEvent& event);
void OnCancel(wxCommandEvent& event);
void OnClose(wxCloseEvent& event);
void OnInitDialog(wxInitDialogEvent& event);
void OnRecordingTypeChange(wxCommandEvent& event);
wxTextCtrl *recordingSuffix_;
wxRadioButton *rawRecording_;
wxRadioButton *decodedRecording_;
wxRadioButton *formatWav_;
wxRadioButton *formatMp3_;
wxButton* m_buttonOK;
wxButton* m_buttonCancel;
};
#endif // BEGIN_RECORDING_DIALOG_H

View File

@ -435,18 +435,30 @@ OptionsDlg::OptionsDlg(wxWindow* parent, wxWindowID id, const wxString& title, c
wxStaticBox* quickRecordBox = new wxStaticBox(m_keyerTab, wxID_ANY, _("Quick Record"));
wxStaticBoxSizer* sbsQuickRecord = new wxStaticBoxSizer(quickRecordBox, wxVERTICAL);
wxBoxSizer* quickRecordSizer = new wxBoxSizer(wxHORIZONTAL);
wxFlexGridSizer* quickRecordSizer = new wxFlexGridSizer(2, 3, 5, 5);
quickRecordSizer->AddGrowableCol(1);
wxStaticText *staticTextQRPath = new wxStaticText(quickRecordBox, wxID_ANY, _("Location to save recordings: "), wxDefaultPosition, wxDefaultSize, 0);
wxStaticText *staticTextQRPath = new wxStaticText(quickRecordBox, wxID_ANY, _("Location to save raw recordings: "), wxDefaultPosition, wxDefaultSize, 0);
quickRecordSizer->Add(staticTextQRPath, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
m_txtCtrlQuickRecordPath = new wxTextCtrl(quickRecordBox, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(450,-1), 0);
m_txtCtrlQuickRecordPath->SetToolTip(_("Location which to save recordings started via the Record button in the main window."));
quickRecordSizer->Add(m_txtCtrlQuickRecordPath, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
m_txtCtrlQuickRecordRawPath = new wxTextCtrl(quickRecordBox, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(450,-1), 0);
m_txtCtrlQuickRecordRawPath->SetToolTip(_("Location which to save raw recordings started via the Record button in the main window."));
quickRecordSizer->Add(m_txtCtrlQuickRecordRawPath, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
m_buttonChooseQuickRecordPath = new wxButton(quickRecordBox, wxID_APPLY, _("Choose"), wxDefaultPosition, wxSize(-1,-1), 0);
m_buttonChooseQuickRecordPath->SetMinSize(wxSize(120, -1));
quickRecordSizer->Add(m_buttonChooseQuickRecordPath, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
m_buttonChooseQuickRecordRawPath = new wxButton(quickRecordBox, wxID_APPLY, _("Choose"), wxDefaultPosition, wxSize(-1,-1), 0);
m_buttonChooseQuickRecordRawPath->SetMinSize(wxSize(120, -1));
quickRecordSizer->Add(m_buttonChooseQuickRecordRawPath, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
staticTextQRPath = new wxStaticText(quickRecordBox, wxID_ANY, _("Location to save decoded recordings: "), wxDefaultPosition, wxDefaultSize, 0);
quickRecordSizer->Add(staticTextQRPath, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
m_txtCtrlQuickRecordDecodedPath = new wxTextCtrl(quickRecordBox, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(450,-1), 0);
m_txtCtrlQuickRecordDecodedPath->SetToolTip(_("Location which to save decoded recordings started via the Record button in the main window."));
quickRecordSizer->Add(m_txtCtrlQuickRecordDecodedPath, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
m_buttonChooseQuickRecordDecodedPath = new wxButton(quickRecordBox, wxID_APPLY, _("Choose"), wxDefaultPosition, wxSize(-1,-1), 0);
m_buttonChooseQuickRecordDecodedPath->SetMinSize(wxSize(120, -1));
quickRecordSizer->Add(m_buttonChooseQuickRecordDecodedPath, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
sbsQuickRecord->Add(quickRecordSizer);
@ -784,7 +796,8 @@ OptionsDlg::OptionsDlg(wxWindow* parent, wxWindowID id, const wxString& title, c
m_buttonChooseVoiceKeyerWaveFilePath->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnChooseVoiceKeyerWaveFilePath), NULL, this);
m_buttonChooseQuickRecordPath->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnChooseQuickRecordPath), NULL, this);
m_buttonChooseQuickRecordRawPath->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnChooseQuickRecordPath), NULL, this);
m_buttonChooseQuickRecordDecodedPath->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnChooseQuickRecordPath), NULL, this);
m_BtnFifoReset->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnFifoReset), NULL, this);
@ -829,7 +842,8 @@ OptionsDlg::~OptionsDlg()
m_ckboxFreeDV700txClip->Disconnect(wxEVT_COMMAND_CHECKBOX_CLICKED, wxScrollEventHandler(OptionsDlg::OnFreeDV700txClip), NULL, this);
m_buttonChooseVoiceKeyerWaveFilePath->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnChooseVoiceKeyerWaveFilePath), NULL, this);
m_buttonChooseQuickRecordPath->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnChooseQuickRecordPath), NULL, this);
m_buttonChooseQuickRecordRawPath->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnChooseQuickRecordPath), NULL, this);
m_buttonChooseQuickRecordDecodedPath->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnChooseQuickRecordPath), NULL, this);
m_BtnFifoReset->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(OptionsDlg::OnFifoReset), NULL, this);
@ -904,7 +918,8 @@ void OptionsDlg::ExchangeData(int inout, bool storePersistent)
m_txtCtrlVoiceKeyerRxPause->SetValue(wxString::Format(wxT("%i"), wxGetApp().appConfiguration.voiceKeyerRxPause.get()));
m_txtCtrlVoiceKeyerRepeats->SetValue(wxString::Format(wxT("%i"), wxGetApp().appConfiguration.voiceKeyerRepeats.get()));
m_txtCtrlQuickRecordPath->SetValue(wxGetApp().appConfiguration.quickRecordPath);
m_txtCtrlQuickRecordRawPath->SetValue(wxGetApp().appConfiguration.quickRecordRawPath);
m_txtCtrlQuickRecordDecodedPath->SetValue(wxGetApp().appConfiguration.quickRecordDecodedPath);
m_ckHalfDuplex->SetValue(wxGetApp().appConfiguration.halfDuplexMode);
@ -1082,7 +1097,8 @@ void OptionsDlg::ExchangeData(int inout, bool storePersistent)
if (tmp < 0) {tmp = 0;} if (tmp > 100) {tmp = 100;}
wxGetApp().appConfiguration.voiceKeyerRepeats = (int)tmp;
wxGetApp().appConfiguration.quickRecordPath = m_txtCtrlQuickRecordPath->GetValue();
wxGetApp().appConfiguration.quickRecordRawPath = m_txtCtrlQuickRecordRawPath->GetValue();
wxGetApp().appConfiguration.quickRecordDecodedPath = m_txtCtrlQuickRecordDecodedPath->GetValue();
wxGetApp().m_testFrames = m_ckboxTestFrame->GetValue();
@ -1289,17 +1305,28 @@ void OptionsDlg::OnChooseVoiceKeyerWaveFilePath(wxCommandEvent&) {
m_txtCtrlVoiceKeyerWaveFilePath->SetValue(pathDialog.GetPath());
}
void OptionsDlg::OnChooseQuickRecordPath(wxCommandEvent&) {
void OptionsDlg::OnChooseQuickRecordPath(wxCommandEvent& event) {
wxString defaultLocation =
(event.GetEventObject() == m_buttonChooseQuickRecordRawPath) ?
wxGetApp().appConfiguration.quickRecordRawPath :
wxGetApp().appConfiguration.quickRecordDecodedPath;
wxDirDialog pathDialog(
this,
wxT("Choose Quick Record save location"),
wxGetApp().appConfiguration.quickRecordPath
defaultLocation
);
if(pathDialog.ShowModal() == wxID_CANCEL) {
return; // the user changed their mind...
}
m_txtCtrlQuickRecordPath->SetValue(pathDialog.GetPath());
if (event.GetEventObject() == m_buttonChooseQuickRecordRawPath)
{
m_txtCtrlQuickRecordRawPath->SetValue(pathDialog.GetPath());
}
else
{
m_txtCtrlQuickRecordDecodedPath->SetValue(pathDialog.GetPath());
}
}
void OptionsDlg::OnFreeDV700txClip(wxScrollEvent&) {

View File

@ -119,8 +119,10 @@ class OptionsDlg : public wxDialog
wxTextCtrl *m_txtCtrlVoiceKeyerRepeats;
/* Quick Record */
wxButton *m_buttonChooseQuickRecordPath;
wxTextCtrl *m_txtCtrlQuickRecordPath;
wxButton *m_buttonChooseQuickRecordRawPath;
wxTextCtrl *m_txtCtrlQuickRecordRawPath;
wxButton *m_buttonChooseQuickRecordDecodedPath;
wxTextCtrl *m_txtCtrlQuickRecordDecodedPath;
/* test frames, other simulated channel impairments */

View File

@ -85,6 +85,17 @@ class FreeDVReporterDialog : public wxFrame
}
return false;
}
bool getSelectedCallsignInfo(wxString& callsign)
{
auto selectedItem = m_listSpots->GetSelection();
if (selectedItem.IsOk())
{
FreeDVReporterDataModel* model = (FreeDVReporterDataModel*)spotsDataModel_.get();
return model->getSelectedCallsignInfo(selectedItem, callsign);
}
return false;
}
wxString getGridSquareForCallsign(wxString const& callsign)
{
@ -273,6 +284,17 @@ class FreeDVReporterDialog : public wxFrame
return false;
}
bool getSelectedCallsignInfo(wxDataViewItem& item, wxString& callsign)
{
if (item.IsOk())
{
auto data = (ReporterData*)item.GetID();
callsign = data->callsign;
return true;
}
return false;
}
// Required overrides to implement functionality
virtual bool HasDefaultCompare() const override;
virtual int Compare (const wxDataViewItem &item1, const wxDataViewItem &item2, unsigned int column, bool ascending) const override;

View File

@ -163,6 +163,7 @@ extern SNDFILE *g_sfRecFile;
extern bool g_recFileFromRadio;
extern unsigned int g_recFromRadioSamples;
extern int g_recFileFromRadioEventId;
extern int g_recFileFromDecoderEventId;
extern std::atomic<SNDFILE*> g_sfPlayFileFromRadio;
extern std::atomic<bool> g_playFileFromRadio;

View File

@ -254,27 +254,6 @@ private:
wxCheckBox *m_cb;
};
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=
// panel with custom Seconds-to-record control for record file dialog
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=
class MyExtraRecFilePanel : public wxPanel
{
public:
MyExtraRecFilePanel(wxWindow *parent);
~MyExtraRecFilePanel()
{
wxLogDebug("Destructor\n");
}
void setSecondsToRecord(wxString const& value) { m_secondsToRecord->SetValue(value); }
wxString getSecondsToRecord(void)
{
wxLogDebug("getSecondsToRecord: %s\n",m_secondsToRecord->GetValue());
return m_secondsToRecord->GetValue();
}
private:
wxTextCtrl *m_secondsToRecord;
};
class TxRxThread;
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=
@ -342,6 +321,7 @@ class MainFrame : public TopFrame
void StopPlayFileToMicIn(void);
void StopPlaybackFileFromRadio();
void StopRecFileFromRadio();
void StopRecFileFromDecoder();
bool isReceiveOnly();
@ -377,7 +357,6 @@ class MainFrame : public TopFrame
void OnToolsOptions(wxCommandEvent& event) override;
void OnToolsOptionsUI(wxUpdateUIEvent& event) override;
void OnRecFileFromRadio( wxCommandEvent& event ) override;
void OnPlayFileFromRadio( wxCommandEvent& event ) override;
void OnCenterRx(wxCommandEvent& event) override;

View File

@ -130,10 +130,12 @@ extern std::atomic<SNDFILE*> g_sfPlayFile;
extern SNDFILE* g_sfRecFileFromModulator;
extern SNDFILE* g_sfRecFile;
extern SNDFILE* g_sfRecMicFile;
extern SNDFILE* g_sfRecDecoderFile;
extern std::atomic<SNDFILE*> g_sfPlayFileFromRadio;
extern bool g_recFileFromMic;
extern bool g_recVoiceKeyerFile;
extern bool g_recFileFromDecoder;
// TBD -- shouldn't be needed once we've fully converted over
extern int resample(SRC_STATE *src,
@ -506,6 +508,28 @@ void TxRxThread::initializePipeline_()
&g_rxUserdata->sbqSpkOutVol,
g_rxUserdata->spkEqLock);
pipeline_->appendPipelineStep(equalizerStep);
// Record from decoder step (optional)
auto recordDecoderStep = new RecordStep(
outputSampleRate_,
[]() { return g_sfRecDecoderFile; },
[](int) {
// Recording stops when the user explicitly tells us to,
// no action required here.
}
);
auto recordDecoderPipeline = new AudioPipeline(outputSampleRate_, outputSampleRate_);
recordDecoderPipeline->appendPipelineStep(recordDecoderStep);
auto recordDecoderTap = new TapStep(outputSampleRate_, recordDecoderPipeline);
auto bypassRecordDecoder = new AudioPipeline(outputSampleRate_, inputSampleRate_);
auto eitherOrRecordDecoder = new EitherOrStep(
+[]() FREEDV_NONBLOCKING { return (g_recFileFromDecoder) && (g_sfRecDecoderFile != NULL); },
recordDecoderTap,
bypassRecordDecoder
);
pipeline_->appendPipelineStep(eitherOrRecordDecoder);
// Clear anything in the FIFO before resuming decode.
clearFifos_();

View File

@ -73,6 +73,7 @@ public:
stop();
// Free allocated buffer
stop();
inputSamples_ = nullptr;
}

View File

@ -7,6 +7,9 @@
#include "main.h"
#include "gui/dialogs/begin_recording.h"
#include "gui/dialogs/freedv_reporter.h"
extern wxMutex g_mutexProtectingCallbackData;
std::atomic<SNDFILE*> g_sfPlayFile;
std::atomic<bool> g_playFileToMicIn;
@ -21,6 +24,10 @@ int g_recFileFromRadioEventId;
SNDFILE *g_sfRecMicFile;
bool g_recFileFromMic;
SNDFILE* g_sfRecDecoderFile;
bool g_recFileFromDecoder;
int g_recFileFromDecoderEventId;
std::atomic<SNDFILE*> g_sfPlayFileFromRadio;
std::atomic<bool> g_playFileFromRadio;
int g_sfFs;
@ -166,27 +173,6 @@ void MainFrame::OnPlayFileFromRadio(wxCommandEvent& event)
}
}
// extra panel added to file save dialog to set number of seconds to record for
MyExtraRecFilePanel::MyExtraRecFilePanel(wxWindow *parent): wxPanel(parent)
{
wxBoxSizer *sizerTop = new wxBoxSizer(wxHORIZONTAL);
wxStaticText* staticText = new wxStaticText(this, wxID_ANY, _("Seconds:"), wxDefaultPosition, wxDefaultSize, 0);
sizerTop->Add(staticText, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);
m_secondsToRecord = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
m_secondsToRecord->SetToolTip(_("Number of seconds to record for"));
m_secondsToRecord->SetValue(wxString::Format(wxT("%i"), wxGetApp().appConfiguration.recFileFromRadioSecs.get()));
m_secondsToRecord->SetMinSize(wxSize(50, -1));
sizerTop->Add(m_secondsToRecord, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5);
SetSizerAndFit(sizerTop);
}
static wxWindow* createMyExtraRecFilePanel(wxWindow *parent)
{
return new MyExtraRecFilePanel(parent);
}
void MainFrame::StopRecFileFromRadio()
{
if (g_sfRecFile != nullptr)
@ -200,7 +186,24 @@ void MainFrame::StopRecFileFromRadio()
g_sfRecFileFromModulator = nullptr;
SetStatusText(wxT(""));
m_menuItemRecFileFromRadio->SetItemLabel(wxString(_("Start Record File - From Radio...")));
g_mutexProtectingCallbackData.Unlock();
m_audioRecord->SetValue(false);
m_audioRecord->SetBackgroundColour(wxNullColour);
}
}
void MainFrame::StopRecFileFromDecoder()
{
if (g_sfRecDecoderFile != nullptr)
{
log_debug("Stopping Record....");
g_mutexProtectingCallbackData.Lock();
g_recFileFromDecoder = false;
sf_close(g_sfRecDecoderFile);
g_sfRecDecoderFile = nullptr;
SetStatusText(wxT(""));
g_mutexProtectingCallbackData.Unlock();
m_audioRecord->SetValue(false);
@ -209,170 +212,143 @@ void MainFrame::StopRecFileFromRadio()
}
//-------------------------------------------------------------------------
// OnRecFileFromRadio()
// OnTogBtnRecord()
//-------------------------------------------------------------------------
void MainFrame::OnRecFileFromRadio(wxCommandEvent& event)
void MainFrame::OnTogBtnRecord(wxCommandEvent& event)
{
wxUnusedVar(event);
if (g_sfRecFile != nullptr) {
StopRecFileFromRadio();
}
else {
wxString soundFile;
SF_INFO sfInfo;
wxFileDialog openFileDialog(
this,
wxT("Record File From Radio"),
wxGetApp().appConfiguration.recFileFromRadioPath,
wxT("Untitled.wav"),
wxT("WAV and RAW files (*.wav;*.raw)|*.wav;*.raw|")
wxT("All files (*.*)|*.*"),
wxFD_SAVE
);
// add the loop check box
openFileDialog.SetExtraControlCreator(&createMyExtraRecFilePanel);
// Default to WAV.
openFileDialog.SetFilterIndex(0);
if(openFileDialog.ShowModal() == wxID_CANCEL)
{
return; // the user changed their mind...
}
wxString fileName, extension;
soundFile = openFileDialog.GetPath();
wxString tmpString = wxGetApp().appConfiguration.recFileFromRadioPath;
wxFileName::SplitPath(soundFile, &tmpString, &fileName, &extension);
wxLogDebug("m_recFileFromRadioPath: %s", wxGetApp().appConfiguration.recFileFromRadioPath.get());
wxLogDebug("soundFile: %s", soundFile);
sfInfo.format = 0;
int sample_rate = RECORD_FILE_SAMPLE_RATE;
if(!extension.IsEmpty())
{
extension.LowerCase();
if(extension == wxT("raw"))
{
sfInfo.format = SF_FORMAT_RAW | SF_FORMAT_PCM_16;
sfInfo.channels = 1;
sfInfo.samplerate = sample_rate;
}
else if(extension == wxT("wav"))
{
sfInfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
sfInfo.channels = 1;
sfInfo.samplerate = sample_rate;
} else {
wxMessageBox(wxT("Invalid file format"), wxT("Record File From Radio"), wxOK);
return;
}
}
else {
wxMessageBox(wxT("Invalid file format"), wxT("Record File From Radio"), wxOK);
return;
}
// Bug: on Win32 I can't read m_recFileFromRadioSecs, so have hard coded it
#ifdef __WIN32__
long secs = wxGetApp().appConfiguration.recFileFromRadioSecs;
g_recFromRadioSamples = sample_rate*(unsigned int)secs;
#else
// work out number of samples to record
wxWindow * const ctrl = openFileDialog.GetExtraControl();
wxString secsString = static_cast<MyExtraRecFilePanel*>(ctrl)->getSecondsToRecord();
wxLogDebug("test: %s secsString: %s\n", wxT("testing 123"), secsString);
long secs;
if (secsString.ToLong(&secs)) {
wxGetApp().appConfiguration.recFileFromRadioSecs = (unsigned int)secs;
g_recFromRadioSamples = sample_rate*(unsigned int)secs;
}
else {
wxMessageBox(wxT("Invalid number of Seconds"), wxT("Record File From Radio"), wxOK);
return;
}
#endif
g_sfRecFile = sf_open(soundFile.c_str(), SFM_WRITE, &sfInfo);
if(g_sfRecFile == NULL)
{
wxString strErr = sf_strerror(NULL);
wxMessageBox(strErr, wxT("Couldn't open sound file"), wxOK);
return;
}
// Save path for future use
wxGetApp().appConfiguration.recFileFromRadioPath = tmpString;
SetStatusText(wxT("Recording file ") + fileName + wxT(" from radio") , 0);
m_menuItemRecFileFromRadio->SetItemLabel(wxString(_("Stop Record File - From Radio...")));
g_sfRecFileFromModulator = g_sfRecFile;
if (!g_tx.load(std::memory_order_acquire))
{
g_recFileFromModulator = false;
g_recFileFromRadio = true;
}
else
{
g_recFileFromRadio = false;
g_recFileFromModulator = true;
}
m_audioRecord->SetValue(true);
m_audioRecord->SetBackgroundColour(*wxRED);
}
}
void MainFrame::OnTogBtnRecord( wxCommandEvent& )
{
if (g_sfRecFile != nullptr)
{
StopRecFileFromRadio();
}
else
else if (g_sfRecDecoderFile != nullptr)
{
auto currentTime = wxDateTime::Now().Format(_("%Y%m%d-%H%M%S"));
wxFileName filePath(wxGetApp().appConfiguration.quickRecordPath, wxString::Format(_("FreeDV_FromRadio_%s.wav"), currentTime));
wxString soundFile = filePath.GetFullPath();
SF_INFO sfInfo;
sfInfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
sfInfo.channels = 1;
sfInfo.samplerate = RECORD_FILE_SAMPLE_RATE;
g_recFromRadioSamples = UINT32_MAX; // record until stopped
g_sfRecFile = sf_open(soundFile.c_str(), SFM_WRITE, &sfInfo);
if(g_sfRecFile == NULL)
{
wxString strErr = sf_strerror(NULL);
wxMessageBox(strErr, wxT("Couldn't open sound file"), wxOK);
return;
StopRecFileFromDecoder();
}
else
{
wxString dxCall;
auto selected = m_lastReportedCallsignListView->GetFirstSelected();
if (selected != -1)
{
// Get callsign and RX frequency
dxCall = m_lastReportedCallsignListView->GetItemText(selected, 0);
log_info("Using %s from main window drop-down list as default recording suffix", (const char*)dxCall.ToUTF8());
}
SetStatusText(wxT("Recording file ") + soundFile + wxT(" from radio"), 0);
m_menuItemRecFileFromRadio->SetItemLabel(wxString(_("Stop Record File - From Radio...")));
g_sfRecFileFromModulator = g_sfRecFile;
if (!g_tx.load(std::memory_order_acquire))
else if (m_reporterDialog != nullptr && m_reporterDialog->getSelectedCallsignInfo(dxCall))
{
g_recFileFromModulator = false;
g_recFileFromRadio = true;
log_info("Using %s from FreeDV Reporter as default recording suffix", (const char*)dxCall.ToUTF8());
}
BeginRecordingDialog recordDialog(this, dxCall);
if (recordDialog.ShowModal() == wxOK)
{
wxString soundFile;
SF_INFO sfInfo;
auto currentTime = wxDateTime::Now().Format(_("%Y%m%d-%H%M%S"));
wxString folder;
wxString filenameSuffix = currentTime;
wxString filenamePrefix;
wxString recordingSuffix = recordDialog.getRecordingSuffix();
if (recordingSuffix != "")
{
filenameSuffix += wxString::Format(_("_%s"), recordingSuffix);
}
if (recordDialog.isRawRecording())
{
folder = wxGetApp().appConfiguration.quickRecordRawPath;
filenamePrefix = _("FDV_FromRadio");
}
else
{
folder = wxGetApp().appConfiguration.quickRecordDecodedPath;
filenamePrefix = _("FDV_FromDecoder");
}
wxString extension;
#if !defined(SNDFILE_NO_MP3_SUPPORT)
if (recordDialog.isMp3Format())
{
extension = _("mp3");
}
else
#endif // !defined(SNDFILE_NO_MP3_SUPPORT)
{
extension = _("wav");
}
wxFileName filePath(folder, wxString::Format(_("%s_%s.%s"), filenamePrefix, filenameSuffix, extension));
soundFile = filePath.GetFullPath();
log_info("Recording to %s", (const char*)soundFile.ToUTF8());
wxString fileName;
wxString tmpString;
wxFileName::SplitPath(soundFile, &tmpString, &fileName, &extension);
#if !defined(SNDFILE_NO_MP3_SUPPORT)
if (recordDialog.isMp3Format())
{
sfInfo.format = SF_FORMAT_MPEG | SF_FORMAT_MPEG_LAYER_III;
}
else
#endif // !defined(SNDFILE_NO_MP3_SUPPORT)
{
sfInfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
}
sfInfo.channels = 1;
sfInfo.samplerate = RECORD_FILE_SAMPLE_RATE;
g_recFromRadioSamples = UINT32_MAX; // record until stopped
if (!recordDialog.isRawRecording())
{
g_sfRecDecoderFile = sf_open(soundFile.c_str(), SFM_WRITE, &sfInfo);
if(g_sfRecDecoderFile == NULL)
{
wxString strErr = sf_strerror(NULL);
wxMessageBox(strErr, wxT("Couldn't open sound file"), wxOK);
m_audioRecord->SetValue(false);
return;
}
SetStatusText(wxT("Recording file ") + soundFile + wxT(" from decoder"), 0);
g_recFileFromDecoder = true;
}
else
{
g_sfRecFile = sf_open(soundFile.c_str(), SFM_WRITE, &sfInfo);
if(g_sfRecFile == NULL)
{
wxString strErr = sf_strerror(NULL);
wxMessageBox(strErr, wxT("Couldn't open sound file"), wxOK);
m_audioRecord->SetValue(false);
return;
}
SetStatusText(wxT("Recording file ") + fileName + wxT(" from radio") , 0);
g_sfRecFileFromModulator = g_sfRecFile;
if (!g_tx.load(std::memory_order_acquire))
{
g_recFileFromModulator = false;
g_recFileFromRadio = true;
}
else
{
g_recFileFromRadio = false;
g_recFileFromModulator = true;
}
}
m_audioRecord->SetValue(true);
m_audioRecord->SetBackgroundColour(*wxRED);
}
else
{
g_recFileFromRadio = false;
g_recFileFromModulator = true;
m_audioRecord->SetValue(false);
}
m_audioRecord->SetBackgroundColour(*wxRED);
}
}

View File

@ -32,6 +32,7 @@
extern int g_playFileToMicInEventId;
extern int g_recFileFromRadioEventId;
extern int g_recFileFromDecoderEventId;
extern int g_playFileFromRadioEventId;
extern int g_recFileFromModulatorEventId;
extern int g_txLevel;
@ -391,10 +392,6 @@ TopFrame::TopFrame(wxWindow* parent, wxWindowID id, const wxString& title, const
wxMenuItem* toolsSeparator2 = new wxMenuItem(tools, wxID_SEPARATOR);
tools->Append(toolsSeparator2);
m_menuItemRecFileFromRadio = new wxMenuItem(tools, wxID_ANY, wxString(_("Start &Record File - From Radio...")) , _("Records incoming audio from the attached radio"), wxITEM_NORMAL);
g_recFileFromRadioEventId = m_menuItemRecFileFromRadio->GetId();
tools->Append(m_menuItemRecFileFromRadio);
m_menuItemPlayFileFromRadio = new wxMenuItem(tools, wxID_ANY, wxString(_("Start &Play File - From Radio...")) , _("Pipes radio sound input from file"), wxITEM_NORMAL);
g_playFileFromRadioEventId = m_menuItemPlayFileFromRadio->GetId();
tools->Append(m_menuItemPlayFileFromRadio);
@ -504,13 +501,13 @@ TopFrame::TopFrame(wxWindow* parent, wxWindowID id, const wxString& title, const
//------------------------------
// Audio Recording/Playback
//------------------------------
wxStaticBox* audioBox = new wxStaticBox(m_panel, wxID_ANY, _("Audio"), wxDefaultPosition, wxSize(100,-1));
wxStaticBox* audioBox = new wxStaticBox(m_panel, wxID_ANY, _("Audio Recording"), wxDefaultPosition, wxSize(100,-1));
wxStaticBoxSizer* sbSizerAudioRecordPlay = new wxStaticBoxSizer(audioBox, wxVERTICAL);
m_audioRecord = new wxToggleButton(audioBox, wxID_ANY, _("Record"), wxDefaultPosition, wxDefaultSize, 0);
m_audioRecord->SetToolTip(_("Records incoming over the air signals as well as anything transmitted."));
sbSizerAudioRecordPlay->Add(m_audioRecord, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, 5);
leftSizer->Add(sbSizerAudioRecordPlay, 0, wxALL|wxEXPAND, 2);
//------------------------------
@ -838,7 +835,6 @@ TopFrame::TopFrame(wxWindow* parent, wxWindowID id, const wxString& title, const
this->Connect(m_menuItemOptions->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TopFrame::OnToolsOptions));
this->Connect(m_menuItemOptions->GetId(), wxEVT_UPDATE_UI, wxUpdateUIEventHandler(TopFrame::OnToolsOptionsUI));
this->Connect(m_menuItemRecFileFromRadio->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TopFrame::OnRecFileFromRadio));
this->Connect(m_menuItemPlayFileFromRadio->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TopFrame::OnPlayFileFromRadio));
this->Connect(m_menuItemHelpUpdates->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TopFrame::OnHelpCheckUpdates));
@ -860,7 +856,7 @@ TopFrame::TopFrame(wxWindow* parent, wxWindowID id, const wxString& title, const
m_ckboxSNR->Connect(wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler(TopFrame::OnCheckSNRClick), NULL, this);
m_audioRecord->Connect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(TopFrame::OnTogBtnRecord), NULL, this);
m_logQSO->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(TopFrame::OnLogQSO), NULL, this);
m_togBtnOnOff->Connect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(TopFrame::OnTogBtnOnOff), NULL, this);
@ -937,7 +933,6 @@ TopFrame::~TopFrame()
this->Disconnect(wxID_ANY, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TopFrame::OnToolsOptions));
this->Disconnect(wxID_ANY, wxEVT_UPDATE_UI, wxUpdateUIEventHandler(TopFrame::OnToolsOptionsUI));
this->Disconnect(wxID_ANY, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TopFrame::OnRecFileFromRadio));
this->Disconnect(wxID_ANY, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TopFrame::OnPlayFileFromRadio));
this->Disconnect(wxID_ANY, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TopFrame::OnHelpCheckUpdates));

View File

@ -144,7 +144,6 @@ class TopFrame : public wxFrame
wxStaticBox* modeBox;
wxStaticBoxSizer* sbSizer_mode;
wxMenuItem* m_menuItemRecFileFromRadio;
wxMenuItem* m_menuItemPlayFileFromRadio;
// Virtual event handlers, override them in your derived class
@ -170,7 +169,6 @@ class TopFrame : public wxFrame
virtual void OnToolsOptionsUI( wxUpdateUIEvent& event ) { event.Skip(); }
virtual void OnToolsComCfg( wxCommandEvent& event ) { event.Skip(); }
virtual void OnToolsComCfgUI( wxUpdateUIEvent& event ) { event.Skip(); }
virtual void OnRecFileFromRadio( wxCommandEvent& event ) { event.Skip(); }
virtual void OnPlayFileFromRadio( wxCommandEvent& event ) { event.Skip(); }
virtual void OnHelpCheckUpdates( wxCommandEvent& event ) { event.Skip(); }

View File

@ -183,7 +183,7 @@ void MainFrame::OnTogBtnVoiceKeyerRightClick( wxContextMenuEvent& )
{
// Only enable VK file selection on idle
bool enabled = vk_state == VK_IDLE && !m_btnTogPTT->GetValue();
chooseVKFileMenuItem_->Enable(enabled);
chooseVKFileMenuItem_->Enable(vk_state == VK_IDLE);
recordNewVoiceKeyerFileMenuItem_->Enable(enabled);
// Trigger right-click menu popup in a location that will prevent it from
@ -255,7 +255,11 @@ int MainFrame::VoiceKeyerStartTx(void)
g_loopPlayFileToMicIn = false;
g_playFileToMicIn.store(true, std::memory_order_release);
m_btnTogPTT->SetValue(true); togglePTT();
// Allow enabling VK during TX.
if (!m_btnTogPTT->GetValue())
{
m_btnTogPTT->SetValue(true); togglePTT();
}
next_state = VK_TX;
wxColour vkBackgroundColor(55, 155, 175);