Merge pull request #594 from cjcliffe/audio_recording

Audio recording
This commit is contained in:
Charles J. Cliffe 2018-01-14 19:57:48 -05:00 committed by GitHub
commit 7fb66b6998
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2278 additions and 657 deletions

View File

@ -6,7 +6,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/")
SET(CUBICSDR_VERSION_MAJOR "0")
SET(CUBICSDR_VERSION_MINOR "2")
SET(CUBICSDR_VERSION_PATCH "2")
SET(CUBICSDR_VERSION_PATCH "3")
SET(CUBICSDR_VERSION_SUFFIX "")
SET(CUBICSDR_VERSION "${CUBICSDR_VERSION_MAJOR}.${CUBICSDR_VERSION_MINOR}.${CUBICSDR_VERSION_PATCH}${CUBICSDR_VERSION_SUFFIX}")
@ -347,6 +347,10 @@ SET (cubicsdr_sources
src/modules/modem/analog/ModemLSB.cpp
src/modules/modem/analog/ModemUSB.cpp
src/audio/AudioThread.cpp
src/audio/AudioSinkThread.cpp
src/audio/AudioSinkFileThread.cpp
src/audio/AudioFile.cpp
src/audio/AudioFileWAV.cpp
src/util/Gradient.cpp
src/util/Timer.cpp
src/util/MouseTracker.cpp
@ -451,6 +455,10 @@ SET (cubicsdr_headers
src/modules/modem/analog/ModemLSB.h
src/modules/modem/analog/ModemUSB.h
src/audio/AudioThread.h
src/audio/AudioSinkThread.h
src/audio/AudioSinkFileThread.h
src/audio/AudioFile.h
src/audio/AudioFileWAV.h
src/util/Gradient.h
src/util/Timer.h
src/util/ThreadBlockingQueue.h
@ -995,7 +1003,7 @@ IF (WIN32 AND BUILD_INSTALLER)
IF (MSVC)
install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/external/msvc/${EX_PLATFORM_NAME}/vc_redist.${EX_PLATFORM_NAME}.exe DESTINATION vc_redist)
set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "ExecWait '\\\"$INSTDIR\\\\vc_redist\\\\vc_redist.${EX_PLATFORM_NAME}.exe\\\" /q:a'")
set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "ExecWait '\\\"$INSTDIR\\\\vc_redist\\\\vc_redist.${EX_PLATFORM_NAME}.exe\\\" /passive /norestart'")
ENDIF (MSVC)

View File

@ -4,6 +4,8 @@
#include "AppConfig.h"
#include "CubicSDR.h"
#include <wx/msgdlg.h>
DeviceConfig::DeviceConfig() : deviceId("") {
ppm.store(0);
offset.store(0);
@ -505,6 +507,51 @@ bool AppConfig::getBookmarksVisible() {
return bookmarksVisible.load();
}
void AppConfig::setRecordingPath(std::string recPath) {
recordingPath = recPath;
}
std::string AppConfig::getRecordingPath() {
return recordingPath;
}
bool AppConfig::verifyRecordingPath() {
string recPathStr = wxGetApp().getConfig()->getRecordingPath();
if (recPathStr.empty()) {
wxMessageBox( wxT("Recording path is not set. Please use 'Set Recording Path' from the 'File' Menu."), wxT("Recording Path Error"), wxICON_INFORMATION);
return false;
}
wxFileName recPath(recPathStr);
if (!recPath.Exists() || !recPath.IsDirWritable()) {
wxMessageBox( wxT("Recording path does not exist or is not writable. Please use 'Set Recording Path' from the 'File' Menu."), wxT("Recording Path Error"), wxICON_INFORMATION);
return false;
}
return true;
}
void AppConfig::setRecordingSquelchOption(int enumChoice) {
recordingSquelchOption = enumChoice;
}
int AppConfig::getRecordingSquelchOption() {
return recordingSquelchOption;
}
void AppConfig::setRecordingFileTimeLimit(int nbSeconds) {
recordingFileTimeLimitSeconds = nbSeconds;
}
int AppConfig::getRecordingFileTimeLimit() {
return recordingFileTimeLimitSeconds;
}
void AppConfig::setConfigName(std::string configName) {
this->configName = configName;
@ -559,6 +606,12 @@ bool AppConfig::save() {
*window_node->newChild("bookmark_visible") = bookmarksVisible.load();
}
//Recording settings:
DataNode *rec_node = cfg.rootNode()->newChild("recording");
*rec_node->newChild("path") = recordingPath;
*rec_node->newChild("squelch") = recordingSquelchOption;
*rec_node->newChild("file_time_limit") = recordingFileTimeLimitSeconds;
DataNode *devices_node = cfg.rootNode()->newChild("devices");
std::map<std::string, DeviceConfig *>::iterator device_config_i;
@ -741,6 +794,26 @@ bool AppConfig::load() {
}
}
//Recording settings:
if (cfg.rootNode()->hasAnother("recording")) {
DataNode *rec_node = cfg.rootNode()->getNext("recording");
if (rec_node->hasAnother("path")) {
DataNode *rec_path = rec_node->getNext("path");
recordingPath = rec_path->element()->toString();
}
if (rec_node->hasAnother("squelch")) {
DataNode *rec_squelch = rec_node->getNext("squelch");
rec_squelch->element()->get(recordingSquelchOption);
}
if (rec_node->hasAnother("file_time_limit")) {
DataNode *rec_file_time_limit = rec_node->getNext("file_time_limit");
rec_file_time_limit->element()->get(recordingFileTimeLimitSeconds);
}
}
if (cfg.rootNode()->hasAnother("devices")) {
DataNode *devices_node = cfg.rootNode()->getNext("devices");

View File

@ -138,6 +138,16 @@ public:
void setBookmarksVisible(bool state);
bool getBookmarksVisible();
//Recording settings:
void setRecordingPath(std::string recPath);
std::string getRecordingPath();
bool verifyRecordingPath();
void setRecordingSquelchOption(int enumChoice);
int getRecordingSquelchOption();
void setRecordingFileTimeLimit(int nbSeconds);
int getRecordingFileTimeLimit();
#if USE_HAMLIB
int getRigModel();
@ -185,6 +195,10 @@ private:
std::atomic_int dbOffset;
std::vector<SDRManualDef> manualDevices;
std::atomic_bool bookmarksVisible;
std::string recordingPath = "";
int recordingSquelchOption = 0;
int recordingFileTimeLimitSeconds = 0;
#if USE_HAMLIB
std::atomic_int rigModel, rigRate;
std::string rigPort;

View File

@ -18,12 +18,13 @@
#include <vector>
#include <algorithm>
#include "AudioThread.h"
#include "AudioSinkFileThread.h"
#include "CubicSDR.h"
#include "DataTree.h"
#include "ColorTheme.h"
#include "DemodulatorMgr.h"
#include "ImagePanel.h"
#include "ActionDialog.h"
#include <thread>
#include <iostream>
@ -53,6 +54,22 @@ wxEND_EVENT_TABLE()
#endif
class ActionDialogBookmarkReset : public ActionDialog {
public:
ActionDialogBookmarkReset() : ActionDialog(wxGetApp().getAppFrame(), wxID_ANY, wxT("Reset Bookmarks?")) {
m_questionText->SetLabelText(wxT("Resetting bookmarks will erase all current bookmarks; are you sure?"));
}
void doClickOK() {
wxGetApp().getBookmarkMgr().resetBookmarks();
wxGetApp().getBookmarkMgr().updateBookmarks();
wxGetApp().getBookmarkMgr().updateActiveList();
}
};
/* split a string by 'seperator' into a vector of string */
std::vector<std::string> str_explode(const std::string &seperator, const std::string &in_str);
@ -402,49 +419,13 @@ AppFrame::AppFrame() :
// Make a menubar
menuBar = new wxMenuBar;
wxMenu *menu = new wxMenu;
#ifndef __APPLE__
#ifdef CUBICSDR_ENABLE_ABOUT_DIALOG
menu->Append(wxID_ABOUT_CUBICSDR, "About " CUBICSDR_INSTALL_NAME);
#endif
#endif
menu->Append(wxID_SDR_DEVICES, "SDR Devices");
menu->AppendSeparator();
menu->Append(wxID_SDR_START_STOP, "Stop / Start Device");
menu->AppendSeparator();
menu->Append(wxID_OPEN, "&Open Session");
menu->Append(wxID_SAVE, "&Save Session");
menu->Append(wxID_SAVEAS, "Save Session &As..");
menu->AppendSeparator();
menu->Append(wxID_RESET, "&Reset Session");
menu->AppendSeparator();
menu->Append(wxID_OPEN_BOOKMARKS, "Open Bookmarks");
menu->Append(wxID_SAVE_BOOKMARKS, "Save Bookmarks");
menu->Append(wxID_SAVEAS_BOOKMARKS, "Save Bookmarks As..");
menu->AppendSeparator();
menu->Append(wxID_RESET_BOOKMARKS, "Reset Bookmarks");
#ifndef __APPLE__
menu->AppendSeparator();
menu->Append(wxID_CLOSE);
#else
#ifdef CUBICSDR_ENABLE_ABOUT_DIALOG
if ( wxApp::s_macAboutMenuItemId != wxID_NONE ) {
wxString aboutLabel;
aboutLabel.Printf(_("About %s"), CUBICSDR_INSTALL_NAME);
menu->Append( wxApp::s_macAboutMenuItemId, aboutLabel);
}
#endif
#endif
menuBar->Append(menu, wxT("&File"));
menuBar->Append(makeFileMenu(), wxT("&File"));
settingsMenu = new wxMenu;
menuBar->Append(settingsMenu, wxT("&Settings"));
menu = new wxMenu;
std::vector<RtAudio::DeviceInfo>::iterator devices_i;
std::map<int, RtAudio::DeviceInfo>::iterator mdevices_i;
AudioThread::enumerateDevices(devices);
@ -478,7 +459,7 @@ AppFrame::AppFrame() :
menuBar->Append(sampleRateMenu, wxT("Sample &Rate"));
// Audio Sample Rates
menu = new wxMenu;
wxMenu *audioSampleRateMenu = new wxMenu;
#define NUM_RATES_DEFAULT 4
unsigned int desired_rates[NUM_RATES_DEFAULT] = { 48000, 44100, 96000, 192000 };
@ -508,7 +489,7 @@ AppFrame::AppFrame() :
for (mdevices_i = outputDevices.begin(); mdevices_i != outputDevices.end(); mdevices_i++) {
int menu_id = wxID_AUDIO_BANDWIDTH_BASE + wxID_AUDIO_DEVICE_MULTIPLIER * mdevices_i->first;
wxMenu *subMenu = new wxMenu;
menu->AppendSubMenu(subMenu, mdevices_i->second.name, wxT("Description?"));
audioSampleRateMenu->AppendSubMenu(subMenu, mdevices_i->second.name, wxT("Description?"));
int j = 0;
for (std::vector<unsigned int>::iterator srate = mdevices_i->second.sampleRates.begin(); srate != mdevices_i->second.sampleRates.end();
@ -526,7 +507,12 @@ AppFrame::AppFrame() :
}
}
menuBar->Append(menu, wxT("Audio &Sample Rate"));
menuBar->Append(audioSampleRateMenu, wxT("Audio &Sample Rate"));
//Add a Recording menu
menuBar->Append(makeRecordingMenu(), wxT("Recordin&g"));
//
updateRecordingMenu();
//Add Display menu
displayMenu = new wxMenu;
@ -535,7 +521,7 @@ AppFrame::AppFrame() :
int fontScale = wxGetApp().getConfig()->getFontScale();
fontMenu->AppendRadioItem(wxID_DISPLAY_BASE, "Normal")->Check(GLFont::GLFONT_SCALE_NORMAL == fontScale);
fontMenu->AppendRadioItem(wxID_DISPLAY_BASE, "Default")->Check(GLFont::GLFONT_SCALE_NORMAL == fontScale);
fontMenu->AppendRadioItem(wxID_DISPLAY_BASE + 1, "1.5x")->Check(GLFont::GLFONT_SCALE_MEDIUM == fontScale);
fontMenu->AppendRadioItem(wxID_DISPLAY_BASE + 2, "2.0x")->Check(GLFont::GLFONT_SCALE_LARGE == fontScale);
@ -729,6 +715,137 @@ AppFrame::~AppFrame() {
t_FFTData->join();
}
wxMenu *AppFrame::makeFileMenu() {
wxMenu *menu = new wxMenu;
#ifndef __APPLE__
#ifdef CUBICSDR_ENABLE_ABOUT_DIALOG
menu->Append(wxID_ABOUT_CUBICSDR, "About " CUBICSDR_INSTALL_NAME);
#endif
#endif
menu->Append(wxID_SDR_DEVICES, "SDR Devices");
menu->AppendSeparator();
menu->Append(wxID_SDR_START_STOP, "Stop / Start Device");
menu->AppendSeparator();
wxMenu *sessionMenu = new wxMenu;
sessionMenu->Append(wxID_OPEN, "&Open Session");
sessionMenu->Append(wxID_SAVE, "&Save Session");
sessionMenu->Append(wxID_SAVEAS, "Save Session &As..");
sessionMenu->AppendSeparator();
sessionMenu->Append(wxID_RESET, "&Reset Session");
menu->AppendSubMenu(sessionMenu, "Session");
menu->AppendSeparator();
wxMenu *bookmarkMenu = new wxMenu;
bookmarkMenu->Append(wxID_OPEN_BOOKMARKS, "Open Bookmarks");
bookmarkMenu->Append(wxID_SAVE_BOOKMARKS, "Save Bookmarks");
bookmarkMenu->Append(wxID_SAVEAS_BOOKMARKS, "Save Bookmarks As..");
bookmarkMenu->AppendSeparator();
bookmarkMenu->Append(wxID_RESET_BOOKMARKS, "Reset Bookmarks");
menu->AppendSubMenu(bookmarkMenu, "Bookmarks");
#ifndef __APPLE__
menu->AppendSeparator();
menu->Append(wxID_CLOSE);
#else
#ifdef CUBICSDR_ENABLE_ABOUT_DIALOG
if (wxApp::s_macAboutMenuItemId != wxID_NONE) {
wxString aboutLabel;
aboutLabel.Printf(_("About %s"), CUBICSDR_INSTALL_NAME);
menu->Append(wxApp::s_macAboutMenuItemId, aboutLabel);
}
#endif
#endif
fileMenu = menu;
return menu;
}
wxMenu *AppFrame::makeRecordingMenu() {
recordingMenuItems.clear();
wxMenu *menu = new wxMenu;
recordingMenuItems[wxID_RECORDING_PATH] = menu->Append(wxID_RECORDING_PATH, getSettingsLabel("Set Recording Path", "<Not Set>"));
menu->AppendSeparator();
//Squelch options as sub-menu:
wxMenu *subMenu = new wxMenu;
recordingMenuItems[wxID_RECORDING_SQUELCH_BASE] = menu->AppendSubMenu(subMenu, "Squelch");
recordingMenuItems[wxID_RECORDING_SQUELCH_SILENCE] = subMenu->AppendRadioItem(wxID_RECORDING_SQUELCH_SILENCE, "Record Silence",
"Record below squelch-break audio as silence, i.e records as the user may hear.");
recordingMenuItems[wxID_RECORDING_SQUELCH_SKIP] = subMenu->AppendRadioItem(wxID_RECORDING_SQUELCH_SKIP, "Skip Silence",
"Do not record below squelch-break audio, i.e squelch-break audio parts are packed together.");
recordingMenuItems[wxID_RECORDING_SQUELCH_ALWAYS] = subMenu->AppendRadioItem(wxID_RECORDING_SQUELCH_ALWAYS, "Record Always",
"Record everything irrespective of the squelch level.");
recordingMenuItems[wxID_RECORDING_FILE_TIME_LIMIT] = menu->Append(wxID_RECORDING_FILE_TIME_LIMIT, getSettingsLabel("File time limit", "<Not Set>"),
"Creates a new file automatically, each time the recording lasts longer than the limit, named according to the current time.");
recordingMenuItems[wxID_RECORDING_SQUELCH_SILENCE]->Check(true);
recordingMenu = menu;
return menu;
}
void AppFrame::updateRecordingMenu() {
// Recording path:
std::string recPath = wxGetApp().getConfig()->getRecordingPath();
if (recPath.length() > 32) {
recPath = "..." + recPath.substr(recPath.length() - 32, 32);
}
recordingMenuItems[wxID_RECORDING_PATH]->SetItemLabel(getSettingsLabel("Set Recording Path", recPath.empty() ? "<Not Set>" : recPath));
//Squelch options:
int squelchEnumValue = wxGetApp().getConfig()->getRecordingSquelchOption();
if (squelchEnumValue == AudioSinkFileThread::SQUELCH_RECORD_SILENCE) {
recordingMenuItems[wxID_RECORDING_SQUELCH_SILENCE]->Check(true);
recordingMenuItems[wxID_RECORDING_SQUELCH_BASE]->SetItemLabel(getSettingsLabel("Squelch", "Record Silence"));
} else if (squelchEnumValue == AudioSinkFileThread::SQUELCH_SKIP_SILENCE) {
recordingMenuItems[wxID_RECORDING_SQUELCH_SKIP]->Check(true);
recordingMenuItems[wxID_RECORDING_SQUELCH_BASE]->SetItemLabel(getSettingsLabel("Squelch", "Skip Silence"));
} else if (squelchEnumValue == AudioSinkFileThread::SQUELCH_RECORD_ALWAYS) {
recordingMenuItems[wxID_RECORDING_SQUELCH_ALWAYS]->Check(true);
recordingMenuItems[wxID_RECORDING_SQUELCH_BASE]->SetItemLabel(getSettingsLabel("Squelch", "Record Always"));
}
else {
recordingMenuItems[wxID_RECORDING_SQUELCH_SILENCE]->Check(true);
recordingMenuItems[wxID_RECORDING_SQUELCH_BASE]->SetItemLabel(getSettingsLabel("Squelch", "Record Silence"));
}
//File time limit:
int fileTimeLimitSeconds = wxGetApp().getConfig()->getRecordingFileTimeLimit();
if (fileTimeLimitSeconds <= 0) {
recordingMenuItems[wxID_RECORDING_FILE_TIME_LIMIT]->SetItemLabel(getSettingsLabel("File time limit","<Not Set>"));
}
else {
recordingMenuItems[wxID_RECORDING_FILE_TIME_LIMIT]->SetItemLabel(getSettingsLabel("File time limit",
std::to_string(fileTimeLimitSeconds), "s"));
}
}
void AppFrame::initDeviceParams(SDRDeviceInfo *devInfo) {
this->devInfo = devInfo;
deviceChanged.store(true);
@ -1480,9 +1597,74 @@ bool AppFrame::actionOnMenuLoadSave(wxCommandEvent& event) {
}
else if (event.GetId() == wxID_RESET_BOOKMARKS) {
wxGetApp().getBookmarkMgr().resetBookmarks();
wxGetApp().getBookmarkMgr().updateBookmarks();
wxGetApp().getBookmarkMgr().updateActiveList();
ActionDialog::showDialog(new ActionDialogBookmarkReset());
return true;
}
return false;
}
bool AppFrame::actionOnMenuRecording(wxCommandEvent& event) {
if (event.GetId() == wxID_RECORDING_PATH) {
std::string recPath = wxGetApp().getConfig()->getRecordingPath();
wxDirDialog recPathDialog(this, _("File Path for Recordings"), recPath, wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
if (recPathDialog.ShowModal() == wxID_CANCEL) {
return true;
}
wxGetApp().getConfig()->setRecordingPath(recPathDialog.GetPath().ToStdString());
updateRecordingMenu();
return true;
}
else if (event.GetId() == wxID_RECORDING_SQUELCH_SILENCE) {
wxGetApp().getConfig()->setRecordingSquelchOption(AudioSinkFileThread::SQUELCH_RECORD_SILENCE);
updateRecordingMenu();
return true;
}
else if (event.GetId() == wxID_RECORDING_SQUELCH_SKIP) {
wxGetApp().getConfig()->setRecordingSquelchOption(AudioSinkFileThread::SQUELCH_SKIP_SILENCE);
updateRecordingMenu();
return true;
}
else if (event.GetId() == wxID_RECORDING_SQUELCH_ALWAYS) {
wxGetApp().getConfig()->setRecordingSquelchOption(AudioSinkFileThread::SQUELCH_RECORD_ALWAYS);
updateRecordingMenu();
return true;
}
else if (event.GetId() == wxID_RECORDING_FILE_TIME_LIMIT) {
int currentFileLimitSeconds = wxGetApp().getConfig()->getRecordingFileTimeLimit();
long newFileLimit = wxGetNumberFromUser(wxString("\nFile time limit:\n") +
"\nCreates a new file automatically, each time the recording lasts longer than the limit, named according to the current time.\n\n " +
+ "min: 0 s (no limit)"
+ ", max: 36000 s (10 hours)\n",
"Time in seconds",
"File Time Limit",
//If a manual sample rate has already been input, recall this one.
currentFileLimitSeconds > 0 ? currentFileLimitSeconds : 0,
0,
36000,
this);
if (newFileLimit != -1) {
wxGetApp().getConfig()->setRecordingFileTimeLimit((int)newFileLimit);
updateRecordingMenu();
}
return true;
}
@ -1708,6 +1890,9 @@ void AppFrame::OnMenu(wxCommandEvent& event) {
}
else if (actionOnMenuAudioSampleRate(event)) {
return;
}
else if (actionOnMenuRecording(event)) {
return;
}
else if (actionOnMenuDisplay(event)) {
return;
@ -2576,6 +2761,7 @@ int AppFrame::OnGlobalKeyDown(wxKeyEvent &event) {
case 'S':
case 'P':
case 'M':
case 'R':
return 1;
case '0':
case '1':
@ -2716,6 +2902,13 @@ int AppFrame::OnGlobalKeyUp(wxKeyEvent &event) {
wxGetApp().setSoloMode(!wxGetApp().getSoloMode());
return 1;
break;
case 'R':
if (event.ShiftDown()) {
toggleAllActiveDemodRecording();
} else {
toggleActiveDemodRecording();
}
break;
case 'P':
wxGetApp().getSpectrumProcessor()->setPeakHold(!wxGetApp().getSpectrumProcessor()->getPeakHold());
if (wxGetApp().getDemodSpectrumProcessor()) {
@ -2760,6 +2953,43 @@ int AppFrame::OnGlobalKeyUp(wxKeyEvent &event) {
return 1;
}
void AppFrame::toggleActiveDemodRecording() {
if (!wxGetApp().getConfig()->verifyRecordingPath()) {
return;
}
DemodulatorInstancePtr activeDemod = wxGetApp().getDemodMgr().getActiveDemodulator();
if (activeDemod) {
activeDemod->setRecording(!activeDemod->isRecording());
wxGetApp().getBookmarkMgr().updateActiveList();
}
}
void AppFrame::toggleAllActiveDemodRecording() {
if (!wxGetApp().getConfig()->verifyRecordingPath()) {
return;
}
auto activeDemods = wxGetApp().getDemodMgr().getDemodulators();
bool stateToSet = true;
for (auto i : activeDemods) {
if (i->isActive() && i->isRecording()) {
stateToSet = false;
break;
}
}
for (auto i : activeDemods) {
if (i->isActive() && i->isRecording() != stateToSet) {
i->setRecording(stateToSet);
}
}
}
void AppFrame::setWaterfallLinesPerSecond(int lps) {
waterfallSpeedMeter->setUserInputValue(sqrt(lps));

View File

@ -76,6 +76,13 @@
#define wxID_DEVICE_ID 3500
#define wxID_RECORDING_PATH 8500
#define wxID_RECORDING_SQUELCH_BASE 8501
#define wxID_RECORDING_SQUELCH_SILENCE 8502
#define wxID_RECORDING_SQUELCH_SKIP 8503
#define wxID_RECORDING_SQUELCH_ALWAYS 8504
#define wxID_RECORDING_FILE_TIME_LIMIT 8505
#define wxID_AUDIO_BANDWIDTH_BASE 9000
#define wxID_AUDIO_DEVICE_MULTIPLIER 50
@ -101,6 +108,11 @@ public:
AppFrame();
~AppFrame();
wxMenu *makeFileMenu();
wxMenu *makeRecordingMenu();
void updateRecordingMenu();
void initDeviceParams(SDRDeviceInfo *devInfo);
void updateDeviceParams();
@ -119,6 +131,9 @@ public:
int OnGlobalKeyDown(wxKeyEvent &event);
int OnGlobalKeyUp(wxKeyEvent &event);
void toggleActiveDemodRecording();
void toggleAllActiveDemodRecording();
void setWaterfallLinesPerSecond(int lps);
void setSpectrumAvgSpeed(double avg);
@ -171,6 +186,7 @@ private:
bool actionOnMenuAudioSampleRate(wxCommandEvent& event);
bool actionOnMenuDisplay(wxCommandEvent& event);
bool actionOnMenuLoadSave(wxCommandEvent& event);
bool actionOnMenuRecording(wxCommandEvent& event);
bool actionOnMenuRig(wxCommandEvent& event);
wxString getSettingsLabel(const std::string& settingsName,
@ -205,6 +221,7 @@ private:
std::vector<RtAudio::DeviceInfo> devices;
std::map<int,RtAudio::DeviceInfo> inputDevices;
std::map<int,RtAudio::DeviceInfo> outputDevices;
std::map<int, wxMenuItem *> outputDeviceMenuItems;
std::map<int, wxMenuItem *> sampleRateMenuItems;
std::map<int, wxMenuItem *> antennaMenuItems;
@ -214,6 +231,10 @@ private:
std::map<int, wxMenuItem *> settingsMenuItems;
std::map<int, wxMenuItem *> audioSampleRateMenuItems;
//
std::map<int, wxMenuItem *> recordingMenuItems;
std::map<int, wxMenuItem *> directSamplingMenuItems;
wxMenuBar *menuBar;
@ -222,7 +243,9 @@ private:
wxMenuItem *agcMenuItem = nullptr;
wxMenuItem *iqSwapMenuItem = nullptr;
wxMenuItem *lowPerfMenuItem = nullptr;
wxMenu *fileMenu = nullptr;
wxMenu *settingsMenu = nullptr;
wxMenu *recordingMenu = nullptr;
SoapySDR::ArgInfoList settingArgs;
int settingsIdMax;

View File

@ -417,6 +417,10 @@ void BookmarkMgr::updateActiveList() {
std::lock_guard < std::recursive_mutex > lockData(busy_lock);
if (wxGetApp().isShuttingDown()) {
return;
}
BookmarkView *bmv = wxGetApp().getAppFrame()->getBookmarkView();
if (bmv) {

View File

@ -203,6 +203,7 @@ CubicSDR::CubicSDR() : frequency(0), offset(0), ppm(0), snap(1), sampleRate(DEFA
sampleRateInitialized.store(false);
agcMode.store(true);
soloMode.store(false);
shuttingDown.store(false);
fdlgTarget = FrequencyDialog::FDIALOG_TARGET_DEFAULT;
stoppedDev = nullptr;
}
@ -384,6 +385,8 @@ bool CubicSDR::OnInit() {
}
int CubicSDR::OnExit() {
shuttingDown.store(true);
#if USE_HAMLIB
if (rigIsActive()) {
std::cout << "Terminating Rig thread.." << std::endl << std::flush;
@ -1029,6 +1032,11 @@ bool CubicSDR::getSoloMode() {
return soloMode.load();
}
bool CubicSDR::isShuttingDown()
{
return shuttingDown.load();
}
int CubicSDR::FilterEvent(wxEvent& event) {
if (!appframe) {
return -1;

View File

@ -171,6 +171,8 @@ public:
void setSoloMode(bool solo);
bool getSoloMode();
bool isShuttingDown();
#ifdef USE_HAMLIB
RigThread *getRigThread();
void initRig(int rigModel, std::string rigPort, int rigSerialRate);
@ -195,6 +197,7 @@ private:
std::atomic_llong sampleRate;
std::string antennaName;
std::atomic_bool agcMode;
std::atomic_bool shuttingDown;
SDRThread *sdrThread = nullptr;
SDREnumerator *sdrEnum = nullptr;

50
src/audio/AudioFile.cpp Normal file
View File

@ -0,0 +1,50 @@
// Copyright (c) Charles J. Cliffe
// SPDX-License-Identifier: GPL-2.0+
#include "AudioFile.h"
#include "CubicSDR.h"
#include <sstream>
AudioFile::AudioFile() {
}
AudioFile::~AudioFile() {
}
void AudioFile::setOutputFileName(std::string filename) {
filenameBase = filename;
}
std::string AudioFile::getOutputFileName() {
std::string recPath = wxGetApp().getConfig()->getRecordingPath();
// Strip any invalid characters from the name
std::string stripChars("<>:\"/\\|?*");
std::string filenameBaseSafe = filenameBase;
for (size_t i = 0, iMax = filenameBaseSafe.length(); i < iMax; i++) {
if (stripChars.find(filenameBaseSafe[i]) != std::string::npos) {
filenameBaseSafe.replace(i,1,"_");
}
}
// Create output file name
std::stringstream outputFileName;
outputFileName << recPath << filePathSeparator << filenameBaseSafe;
int idx = 0;
// If the file exists; then find the next non-existing file in sequence.
std::string fileNameCandidate = outputFileName.str();
while (FILE *file = fopen((fileNameCandidate + "." + getExtension()).c_str(), "r")) {
fclose(file);
fileNameCandidate = outputFileName.str() + "-" + std::to_string(++idx);
}
return fileNameCandidate + "." + getExtension();
}

25
src/audio/AudioFile.h Normal file
View File

@ -0,0 +1,25 @@
// Copyright (c) Charles J. Cliffe
// SPDX-License-Identifier: GPL-2.0+
#pragma once
#include "AudioThread.h"
class AudioFile
{
public:
AudioFile();
virtual ~AudioFile();
virtual void setOutputFileName(std::string filename);
virtual std::string getExtension() = 0;
virtual std::string getOutputFileName();
virtual bool writeToFile(AudioThreadInputPtr input) = 0;
virtual bool closeFile() = 0;
protected:
std::string filenameBase;
};

219
src/audio/AudioFileWAV.cpp Normal file
View File

@ -0,0 +1,219 @@
// Copyright (c) Charles J. Cliffe
// SPDX-License-Identifier: GPL-2.0+
#include "AudioFileWAV.h"
#include "CubicSDR.h"
#include <iomanip>
//limit file size to 2GB (- margin) for maximum compatibility.
#define MAX_WAV_FILE_SIZE (0x7FFFFFFF - 1024)
// Simple endian io read/write handling from
// http://www.cplusplus.com/forum/beginner/31584/#msg171056
namespace little_endian_io
{
template <typename Word>
std::ostream& write_word(std::ostream& outs, Word value, unsigned size = sizeof(Word)) {
for (; size; --size, value >>= 8) {
outs.put(static_cast <char> (value & 0xFF));
}
return outs;
}
template <typename Word>
std::istream& read_word(std::istream& ins, Word& value, unsigned size = sizeof(Word)) {
for (unsigned n = 0, value = 0; n < size; ++n) {
value |= ins.get() << (8 * n);
}
return ins;
}
}
namespace big_endian_io
{
template <typename Word>
std::ostream& write_word(std::ostream& outs, Word value, unsigned size = sizeof(Word)) {
while (size) {
outs.put(static_cast <char> ((value >> (8 * --size)) & 0xFF));
}
return outs;
}
template <typename Word>
std::istream& read_word(std::istream& ins, Word& value, unsigned size = sizeof(Word)) {
for (value = 0; size; --size) {
value = (value << 8) | ins.get();
}
return ins;
}
}
using namespace little_endian_io;
AudioFileWAV::AudioFileWAV() : AudioFile() {
}
AudioFileWAV::~AudioFileWAV() {
}
std::string AudioFileWAV::getExtension()
{
return "wav";
}
bool AudioFileWAV::writeToFile(AudioThreadInputPtr input)
{
if (!outputFileStream.is_open()) {
std::string ofName = getOutputFileName();
outputFileStream.open(ofName.c_str(), std::ios::binary);
currentFileSize = 0;
writeHeaderToFileStream(input);
}
size_t maxRoomInCurrentFileInSamples = getMaxWritableNumberOfSamples(input);
if (maxRoomInCurrentFileInSamples >= input->data.size()) {
writePayloadToFileStream(input, 0, input->data.size());
}
else {
//we complete the current file and open another:
writePayloadToFileStream(input, 0, maxRoomInCurrentFileInSamples);
closeFile();
// Open a new file with the next sequence number, and dump the rest of samples in it.
currentSequenceNumber++;
currentFileSize = 0;
std::string ofName = getOutputFileName();
outputFileStream.open(ofName.c_str(), std::ios::binary);
writeHeaderToFileStream(input);
writePayloadToFileStream(input, maxRoomInCurrentFileInSamples, input->data.size());
}
return true;
}
bool AudioFileWAV::closeFile()
{
if (outputFileStream.is_open()) {
size_t file_length = outputFileStream.tellp();
// Fix the data chunk header to contain the data size
outputFileStream.seekp(dataChunkPos + 4);
write_word(outputFileStream, file_length - dataChunkPos + 8);
// Fix the file header to contain the proper RIFF chunk size, which is (file size - 8) bytes
outputFileStream.seekp(0 + 4);
write_word(outputFileStream, file_length - 8, 4);
outputFileStream.close();
currentFileSize = 0;
}
return true;
}
void AudioFileWAV::writeHeaderToFileStream(AudioThreadInputPtr input) {
// Based on simple wav file output code from
// http://www.cplusplus.com/forum/beginner/166954/
// Write the wav file headers
outputFileStream << "RIFF----WAVEfmt "; // (chunk size to be filled in later)
write_word(outputFileStream, 16, 4); // no extension data
write_word(outputFileStream, 1, 2); // PCM - integer samples
write_word(outputFileStream, input->channels, 2); // channels
write_word(outputFileStream, input->sampleRate, 4); // samples per second (Hz)
write_word(outputFileStream, (input->sampleRate * 16 * input->channels) / 8, 4); // (Sample Rate * BitsPerSample * Channels) / 8
write_word(outputFileStream, input->channels * 2, 2); // data block size (size of integer samples, one for each channel, in bytes)
write_word(outputFileStream, 16, 2); // number of bits per sample (use a multiple of 8)
// Write the data chunk header
dataChunkPos = outputFileStream.tellp();
currentFileSize = dataChunkPos;
outputFileStream << "data----"; // (chunk size to be filled in later)
}
void AudioFileWAV::writePayloadToFileStream(AudioThreadInputPtr input, size_t startInputPosition, size_t endInputPosition) {
// Prevent clipping
float intScale = (input->peak < 1.0) ? 32767.0f : (32767.0f / input->peak);
if (input->channels == 1) {
for (size_t i = startInputPosition, iMax = endInputPosition; i < iMax; i++) {
write_word(outputFileStream, int(input->data[i] * intScale), 2);
currentFileSize += 2;
}
}
else if (input->channels == 2) {
for (size_t i = startInputPosition, iMax = endInputPosition / 2; i < iMax; i++) {
write_word(outputFileStream, int(input->data[i * 2] * intScale), 2);
write_word(outputFileStream, int(input->data[i * 2 + 1] * intScale), 2);
currentFileSize += 4;
}
}
}
size_t AudioFileWAV::getMaxWritableNumberOfSamples(AudioThreadInputPtr input) {
long long remainingBytesInFile = (long long)(MAX_WAV_FILE_SIZE) - currentFileSize;
return (size_t)(remainingBytesInFile / (input->channels * 2));
}
void AudioFileWAV::setOutputFileName(std::string filename) {
if (filename != filenameBase) {
currentSequenceNumber = 0;
}
AudioFile::setOutputFileName(filename);
}
std::string AudioFileWAV::getOutputFileName() {
std::string recPath = wxGetApp().getConfig()->getRecordingPath();
// Strip any invalid characters from the name
std::string stripChars("<>:\"/\\|?*");
std::string filenameBaseSafe = filenameBase;
for (size_t i = 0, iMax = filenameBaseSafe.length(); i < iMax; i++) {
if (stripChars.find(filenameBaseSafe[i]) != std::string::npos) {
filenameBaseSafe.replace(i, 1, "_");
}
}
// Create output file name
std::stringstream outputFileName;
outputFileName << recPath << filePathSeparator << filenameBaseSafe;
//customized part: append a sequence number.
if (currentSequenceNumber > 0) {
outputFileName << "_" << std::setfill('0') << std::setw(3) << currentSequenceNumber;
}
int idx = 0;
// If the file exists; then find the next non-existing file in sequence.
std::string fileNameCandidate = outputFileName.str();
while (FILE *file = fopen((fileNameCandidate + "." + getExtension()).c_str(), "r")) {
fclose(file);
fileNameCandidate = outputFileName.str() + "-" + std::to_string(++idx);
}
return fileNameCandidate + "." + getExtension();
}

42
src/audio/AudioFileWAV.h Normal file
View File

@ -0,0 +1,42 @@
// Copyright (c) Charles J. Cliffe
// SPDX-License-Identifier: GPL-2.0+
#pragma once
#include "AudioFile.h"
#include <fstream>
class AudioFileWAV : public AudioFile {
public:
AudioFileWAV();
~AudioFileWAV();
//override to manage name change with multi-part WAV.
virtual void setOutputFileName(std::string filename);
//override of the base method to generate multi-part
//WAV to overcome the WAV format size limit.
virtual std::string getOutputFileName();
virtual std::string getExtension();
virtual bool writeToFile(AudioThreadInputPtr input);
virtual bool closeFile();
protected:
std::ofstream outputFileStream;
size_t dataChunkPos;
long long currentFileSize = 0;
int currentSequenceNumber = 0;
private:
size_t getMaxWritableNumberOfSamples(AudioThreadInputPtr input);
void writeHeaderToFileStream(AudioThreadInputPtr input);
//write [startInputPosition; endInputPosition[ samples from input into the file.
void writePayloadToFileStream(AudioThreadInputPtr input, size_t startInputPosition, size_t endInputPosition);
};

View File

@ -0,0 +1,140 @@
// Copyright (c) Charles J. Cliffe
// SPDX-License-Identifier: GPL-2.0+
#include "AudioSinkFileThread.h"
#include <ctime>
#define HEARTBEAT_CHECK_PERIOD_MICROS (50 * 1000)
AudioSinkFileThread::AudioSinkFileThread() : AudioSinkThread() {
}
AudioSinkFileThread::~AudioSinkFileThread() {
if (audioFileHandler != nullptr) {
audioFileHandler->closeFile();
}
}
void AudioSinkFileThread::sink(AudioThreadInputPtr input) {
if (!audioFileHandler) {
return;
}
//by default, always write something
bool isSomethingToWrite = true;
if (input->is_squelch_active) {
if (squelchOption == SQUELCH_RECORD_SILENCE) {
//patch with "silence"
input->data.assign(input->data.size(), 0.0f);
input->peak = 0.0f;
}
else if (squelchOption == SQUELCH_SKIP_SILENCE) {
isSomethingToWrite = false;
}
}
//else, nothing to do record as if squelch was not enabled.
if (!isSomethingToWrite) {
return;
}
if (fileTimeLimit > 0) {
durationMeasurement.update();
//duration exeeded, close this file and create another
//with "now" as timestamp.
if (durationMeasurement.getSeconds() > fileTimeLimit) {
audioFileHandler->closeFile();
//initialize the filename of the AudioFile with the current time
time_t t = std::time(nullptr);
tm ltm = *std::localtime(&t);
// GCC 5+
// fileName << "_" << std::put_time(&ltm, "%d-%m-%Y_%H-%M-%S");
char timeStr[512];
//International format: Year.Month.Day, also lexicographically sortable
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d_%H-%M-%S", &ltm);
audioFileHandler->setOutputFileName(fileNameBase + std::string("_") + timeStr);
//reset duration counter
durationMeasurement.start();
//the following writeToFile will take care of creating another file.
}
}
// forward to output file handler
audioFileHandler->writeToFile(input);
}
void AudioSinkFileThread::inputChanged(AudioThreadInput oldProps, AudioThreadInputPtr newProps) {
// close, set new parameters, adjust file name sequence and re-open?
if (!audioFileHandler) {
return;
}
audioFileHandler->closeFile();
//reset duration counter
durationMeasurement.start();
}
void AudioSinkFileThread::setAudioFileNameBase(const std::string& baseName) {
fileNameBase = baseName;
}
void AudioSinkFileThread::setAudioFileHandler(AudioFile * output) {
audioFileHandler = output;
//initialize the filename of the AudioFile with the current time
time_t t = std::time(nullptr);
tm ltm = *std::localtime(&t);
// GCC 5+
// fileName << "_" << std::put_time(&ltm, "%d-%m-%Y_%H-%M-%S");
char timeStr[512];
//International format: Year.Month.Day, also lexicographically sortable
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d_%H-%M-%S", &ltm);
audioFileHandler->setOutputFileName(fileNameBase + std::string("_") + timeStr);
// reset Timer
durationMeasurement.start();
}
void AudioSinkFileThread::setSquelchOption(int squelchOptEnumValue) {
if (squelchOptEnumValue == AudioSinkFileThread::SQUELCH_RECORD_SILENCE) {
squelchOption = AudioSinkFileThread::SQUELCH_RECORD_SILENCE;
}
else if (squelchOptEnumValue == AudioSinkFileThread::SQUELCH_SKIP_SILENCE) {
squelchOption = AudioSinkFileThread::SQUELCH_SKIP_SILENCE;
}
else if (squelchOptEnumValue == AudioSinkFileThread::SQUELCH_RECORD_ALWAYS) {
squelchOption = AudioSinkFileThread::SQUELCH_RECORD_ALWAYS;
}
else {
squelchOption = AudioSinkFileThread::SQUELCH_RECORD_SILENCE;
}
}
// Time limit
void AudioSinkFileThread::setFileTimeLimit(int nbSeconds) {
if (nbSeconds > 0) {
fileTimeLimit = nbSeconds;
}
else {
fileTimeLimit = 0;
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) Charles J. Cliffe
// SPDX-License-Identifier: GPL-2.0+
#pragma once
#include "AudioSinkThread.h"
#include "AudioFile.h"
#include "Timer.h"
class AudioSinkFileThread : public AudioSinkThread {
public:
AudioSinkFileThread();
~AudioSinkFileThread();
enum SquelchOption {
SQUELCH_RECORD_SILENCE = 0, // default value, record as a user would hear it.
SQUELCH_SKIP_SILENCE = 1, // skip below-squelch level.
SQUELCH_RECORD_ALWAYS = 2, // record irrespective of the squelch level.
SQUELCH_RECORD_MAX
};
virtual void sink(AudioThreadInputPtr input);
virtual void inputChanged(AudioThreadInput oldProps, AudioThreadInputPtr newProps);
void setAudioFileHandler(AudioFile *output);
void setAudioFileNameBase(const std::string& baseName);
//Squelch
void setSquelchOption(int squelchOptEnumValue);
// Time limit
void setFileTimeLimit(int nbSeconds);
protected:
std::string fileNameBase;
AudioFile *audioFileHandler = nullptr;
SquelchOption squelchOption = SQUELCH_RECORD_SILENCE;
int fileTimeLimit = 0;
int fileTimeDurationSeconds = -1;
Timer durationMeasurement;
};

View File

@ -0,0 +1,54 @@
// Copyright (c) Charles J. Cliffe
// SPDX-License-Identifier: GPL-2.0+
#include "AudioSinkThread.h"
#define HEARTBEAT_CHECK_PERIOD_MICROS (50 * 1000)
AudioSinkThread::AudioSinkThread() {
inputQueuePtr = std::make_shared<AudioThreadInputQueue>();
inputQueuePtr->set_max_num_items(1000);
setInputQueue("input", inputQueuePtr);
}
AudioSinkThread::~AudioSinkThread() {
}
void AudioSinkThread::run() {
#ifdef __APPLE__
pthread_t tID = pthread_self(); // ID of this thread
int priority = sched_get_priority_max(SCHED_RR) - 1;
sched_param prio = { priority }; // scheduling priority of thread
pthread_setschedparam(tID, SCHED_RR, &prio);
#endif
AudioThreadInputPtr inp;
AudioThreadInput inputRef;
while (!stopping) {
if (!inputQueuePtr->pop(inp, HEARTBEAT_CHECK_PERIOD_MICROS)) {
continue;
}
if (inputRef.channels != inp->channels ||
inputRef.frequency != inp->frequency ||
inputRef.inputRate != inp->inputRate ||
inputRef.sampleRate != inp->sampleRate) {
inputChanged(inputRef, inp);
inputRef.channels = inp->channels;
inputRef.frequency = inp->frequency;
inputRef.inputRate = inp->inputRate;
inputRef.sampleRate = inp->sampleRate;
}
sink(inp);
}
}
void AudioSinkThread::terminate() {
IOThread::terminate();
inputQueuePtr->flush();
}

View File

@ -0,0 +1,25 @@
// Copyright (c) Charles J. Cliffe
// SPDX-License-Identifier: GPL-2.0+
#pragma once
#include "AudioThread.h"
class AudioSinkThread : public IOThread {
public:
AudioSinkThread();
virtual ~AudioSinkThread();
virtual void run();
virtual void terminate();
virtual void sink(AudioThreadInputPtr input) = 0;
virtual void inputChanged(AudioThreadInput oldProps, AudioThreadInputPtr newProps) = 0;
protected:
std::recursive_mutex m_mutex;
AudioThreadInputQueuePtr inputQueuePtr;
};

View File

@ -21,13 +21,32 @@ public:
int channels;
float peak;
int type;
bool is_squelch_active;
std::vector<float> data;
AudioThreadInput() :
frequency(0), sampleRate(0), channels(0), peak(0) {
frequency(0), inputRate(0), sampleRate(0), channels(0), peak(0), type(0), is_squelch_active(false) {
}
AudioThreadInput(AudioThreadInput *copyFrom) {
copy(copyFrom);
}
void copy(AudioThreadInput *copyFrom) {
frequency = copyFrom->frequency;
inputRate = copyFrom->inputRate;
sampleRate = copyFrom->sampleRate;
channels = copyFrom->channels;
peak = copyFrom->peak;
type = copyFrom->type;
is_squelch_active = copyFrom->is_squelch_active;
data.assign(copyFrom->data.begin(), copyFrom->data.end());
}
virtual ~AudioThreadInput() {
}

View File

@ -2,12 +2,15 @@
// SPDX-License-Identifier: GPL-2.0+
#include <memory>
#include <iomanip>
#include "DemodulatorInstance.h"
#include "CubicSDR.h"
#include "DemodulatorThread.h"
#include "DemodulatorPreThread.h"
#include "AudioSinkFileThread.h"
#include "AudioFileWAV.h"
#if USE_HAMLIB
#include "RigThread.h"
@ -47,6 +50,7 @@ DemodulatorInstance::DemodulatorInstance() {
active.store(false);
squelch.store(false);
muted.store(false);
recording.store(false);
deltaLock.store(false);
deltaLockOfs.store(0);
currentOutputDevice.store(-1);
@ -106,6 +110,7 @@ DemodulatorInstance::~DemodulatorInstance() {
delete demodulatorPreThread;
delete demodulatorThread;
delete audioThread;
delete audioSinkThread;
break;
}
@ -181,6 +186,10 @@ void DemodulatorInstance::terminate() {
// std::cout << "Terminating demodulator preprocessor thread.." << std::endl;
demodulatorPreThread->terminate();
if (audioSinkThread != nullptr) {
stopRecording();
}
//that will actually unblock the currently blocked push().
pipeIQInputData->flush();
pipeAudioData->flush();
@ -204,6 +213,7 @@ bool DemodulatorInstance::isTerminated() {
bool audioTerminated = audioThread->isTerminated();
bool demodTerminated = demodulatorThread->isTerminated();
bool preDemodTerminated = demodulatorPreThread->isTerminated();
bool audioSinkTerminated = (audioSinkThread == nullptr) || audioSinkThread->isTerminated();
//Cleanup the worker threads, if the threads are indeed terminated.
// threads are linked as t_PreDemod ==> t_Demod ==> t_Audio
@ -240,14 +250,27 @@ bool DemodulatorInstance::isTerminated() {
if (audioTerminated) {
if (t_Audio) {
#ifdef __APPLE__
pthread_join(t_PreDemod, NULL);
#else
t_Audio->join();
delete t_Audio;
#endif
t_Audio = nullptr;
}
}
bool terminated = audioTerminated && demodTerminated && preDemodTerminated;
if (audioSinkTerminated) {
if (t_AudioSink != nullptr) {
t_AudioSink->join();
delete t_AudioSink;
t_AudioSink = nullptr;
}
}
bool terminated = audioTerminated && demodTerminated && preDemodTerminated && audioSinkTerminated;
return terminated;
}
@ -523,6 +546,21 @@ void DemodulatorInstance::setMuted(bool muted) {
wxGetApp().getDemodMgr().setLastMuted(muted);
}
bool DemodulatorInstance::isRecording()
{
return recording.load();
}
void DemodulatorInstance::setRecording(bool recording_in)
{
if (!recording.load() && recording_in) {
startRecording();
}
else if (recording.load() && !recording_in) {
stopRecording();
}
}
DemodVisualCue *DemodulatorInstance::getVisualCue() {
return &visualCue;
}
@ -580,6 +618,65 @@ ModemSettings DemodulatorInstance::getLastModemSettings(std::string demodType) {
}
}
void DemodulatorInstance::startRecording() {
if (recording.load()) {
return;
}
AudioSinkFileThread *newSinkThread = new AudioSinkFileThread();
AudioFileWAV *afHandler = new AudioFileWAV();
std::stringstream fileName;
std::wstring userLabel = getDemodulatorUserLabel();
wxString userLabelForFileName(userLabel);
std::string userLabelStr = userLabelForFileName.ToStdString();
if (!userLabelStr.empty()) {
fileName << userLabelStr;
} else {
fileName << getLabel();
}
newSinkThread->setAudioFileNameBase(fileName.str());
//attach options:
newSinkThread->setSquelchOption(wxGetApp().getConfig()->getRecordingSquelchOption());
newSinkThread->setFileTimeLimit(wxGetApp().getConfig()->getRecordingFileTimeLimit());
newSinkThread->setAudioFileHandler(afHandler);
audioSinkThread = newSinkThread;
t_AudioSink = new std::thread(&AudioSinkThread::threadMain, audioSinkThread);
demodulatorThread->setOutputQueue("AudioSink", audioSinkThread->getInputQueue("input"));
recording.store(true);
}
void DemodulatorInstance::stopRecording() {
if (!recording.load()) {
return;
}
demodulatorThread->setOutputQueue("AudioSink", nullptr);
audioSinkThread->terminate();
t_AudioSink->join();
delete t_AudioSink;
delete audioSinkThread;
t_AudioSink = nullptr;
audioSinkThread = nullptr;
recording.store(false);
}
#if ENABLE_DIGITAL_LAB
ModemDigitalOutput *DemodulatorInstance::getOutput() {
if (activeOutput == nullptr) {

View File

@ -11,6 +11,7 @@
#include "ModemDigital.h"
#include "ModemAnalog.h"
#include "AudioThread.h"
#include "AudioSinkThread.h"
#if ENABLE_DIGITAL_LAB
#include "DigitalConsole.h"
@ -110,6 +111,9 @@ public:
bool isMuted();
void setMuted(bool muted);
bool isRecording();
void setRecording(bool recording);
DemodVisualCue *getVisualCue();
DemodulatorThreadInputQueuePtr getIQInputDataPipe();
@ -131,6 +135,10 @@ public:
void closeOutput();
#endif
protected:
void startRecording();
void stopRecording();
private:
DemodulatorThreadInputQueuePtr pipeIQInputData;
DemodulatorThreadPostInputQueuePtr pipeIQDemodData;
@ -139,6 +147,10 @@ private:
DemodulatorThread *demodulatorThread;
DemodulatorThreadControlCommandQueuePtr threadQueueControl;
AudioSinkThread *audioSinkThread = nullptr;
std::thread *t_AudioSink = nullptr;
AudioThreadInputQueuePtr audioSinkInputQueue;
//protects child thread creation and termination
std::recursive_mutex m_thread_control_mutex;
@ -150,6 +162,8 @@ private:
std::atomic_bool squelch;
std::atomic_bool muted;
std::atomic_bool deltaLock;
std::atomic_bool recording;
std::atomic_int deltaLockOfs;
std::atomic_int currentOutputDevice;

View File

@ -58,7 +58,6 @@ void DemodulatorMgr::terminateAll() {
DemodulatorInstancePtr d = demods.back();
demods.pop_back();
wxGetApp().removeDemodulator(d);
deleteThread(d);
}
}

View File

@ -43,6 +43,12 @@ void DemodulatorThread::onBindOutput(std::string name, ThreadQueueBasePtr thread
audioVisOutputQueue = std::static_pointer_cast<DemodulatorThreadOutputQueue>(threadQueue);
}
if (name == "AudioSink") {
std::lock_guard < std::mutex > lock(m_mutexAudioVisOutputQueue);
audioSinkOutputQueue = std::static_pointer_cast<AudioThreadInputQueue>(threadQueue);
}
}
double DemodulatorThread::abMagnitude(float inphase, float quadrature) {
@ -195,7 +201,7 @@ void DemodulatorThread::run() {
signalLevel = signalLevel + (currentSignalLevel - signalLevel) * 0.05 * sampleTime * 30.0;
}
bool squelched = (muted.load() || (squelchEnabled && (signalLevel < squelchLevel)));
bool squelched = squelchEnabled && (signalLevel < squelchLevel);
if (squelchEnabled) {
if (!squelched && !squelchBreak) {
@ -219,17 +225,22 @@ void DemodulatorThread::run() {
}
}
if (audioOutputQueue != nullptr && ati && ati->data.size() && !squelched) {
std::vector<float>::iterator data_i;
//compute audio peak:
if (audioOutputQueue != nullptr && ati) {
ati->peak = 0;
for (auto data_i : ati->data) {
float p = fabs(data_i);
if (p > ati->peak) {
ati->peak = p;
}
}
} else if (ati) {
ati = nullptr;
}
//attach squelch flag to samples, to be used by audio sink.
if (ati) {
ati->is_squelch_active = squelched;
}
//At that point, capture the current state of audioVisOutputQueue in a local
@ -240,7 +251,7 @@ void DemodulatorThread::run() {
localAudioVisOutputQueue = audioVisOutputQueue;
}
if ((ati || modemDigital) && localAudioVisOutputQueue != nullptr && localAudioVisOutputQueue->empty()) {
if (!squelched && (ati || modemDigital) && localAudioVisOutputQueue != nullptr && localAudioVisOutputQueue->empty()) {
AudioThreadInputPtr ati_vis = std::make_shared<AudioThreadInput>();
@ -310,7 +321,7 @@ void DemodulatorThread::run() {
}
}
if (ati != nullptr) {
if (!squelched && ati != nullptr) {
if (!muted.load() && (!wxGetApp().getSoloMode() || (demodInstance == wxGetApp().getDemodMgr().getLastActiveDemodulator().get()))) {
//non-blocking push needed for audio out
if (!audioOutputQueue->try_push(ati)) {
@ -321,6 +332,23 @@ void DemodulatorThread::run() {
}
}
// Capture audioSinkOutputQueue state in a local variable
DemodulatorThreadOutputQueuePtr localAudioSinkOutputQueue = nullptr;
{
std::lock_guard < std::mutex > lock(m_mutexAudioVisOutputQueue);
localAudioSinkOutputQueue = audioSinkOutputQueue;
}
//Push to audio sink, if any:
if (ati && localAudioSinkOutputQueue != nullptr) {
if (!localAudioSinkOutputQueue->try_push(ati)) {
std::cout << "DemodulatorThread::run() cannot push ati into audioSinkOutputQueue, is full !" << std::endl;
std::this_thread::yield();
}
}
DemodulatorThreadControlCommand command;
//empty command queue, execute commands

View File

@ -67,6 +67,8 @@ protected:
DemodulatorThreadOutputQueuePtr audioVisOutputQueue;
DemodulatorThreadControlCommandQueuePtr threadQueueControl;
DemodulatorThreadOutputQueuePtr audioSinkOutputQueue = nullptr;
//protects the audioVisOutputQueue dynamic binding change at runtime (in DemodulatorMgr)
std::mutex m_mutexAudioVisOutputQueue;
};

View File

@ -821,8 +821,16 @@ void BookmarkView::activeSelection(DemodulatorInstancePtr dsel) {
clearButtons();
addBookmarkChoice(m_buttonPanel);
if (dsel->isActive() && !(dsel->isRecording())) {
addButton(m_buttonPanel, "Start Recording", wxCommandEventHandler( BookmarkView::onStartRecording ));
} else {
addButton(m_buttonPanel, "Stop Recording", wxCommandEventHandler( BookmarkView::onStopRecording ));
}
addButton(m_buttonPanel, "Remove Active", wxCommandEventHandler( BookmarkView::onRemoveActive ));
showProps();
showButtons();
refreshLayout();
@ -1158,6 +1166,30 @@ void BookmarkView::onRemoveActive( wxCommandEvent& /* event */ ) {
}
}
void BookmarkView::onStartRecording( wxCommandEvent& /* event */ ) {
TreeViewItem *curSel = itemToTVI(m_treeView->GetSelection());
if (curSel && curSel->type == TreeViewItem::TREEVIEW_ITEM_TYPE_ACTIVE) {
if (!curSel->demod->isRecording() && wxGetApp().getConfig()->verifyRecordingPath()) {
curSel->demod->setRecording(true);
wxGetApp().getBookmarkMgr().updateActiveList();
}
}
}
void BookmarkView::onStopRecording( wxCommandEvent& /* event */ ) {
TreeViewItem *curSel = itemToTVI(m_treeView->GetSelection());
if (curSel && curSel->type == TreeViewItem::TREEVIEW_ITEM_TYPE_ACTIVE) {
if (curSel->demod->isRecording()) {
curSel->demod->setRecording(false);
wxGetApp().getBookmarkMgr().updateActiveList();
}
}
}
void BookmarkView::onRemoveBookmark( wxCommandEvent& /* event */ ) {
if (editingLabel) {

View File

@ -145,6 +145,8 @@ protected:
void onBookmarkChoice( wxCommandEvent &event );
void onRemoveActive( wxCommandEvent& event );
void onStartRecording( wxCommandEvent& event );
void onStopRecording( wxCommandEvent& event );
void onRemoveBookmark( wxCommandEvent& event );
void onActivateBookmark( wxCommandEvent& event );

View File

@ -6534,6 +6534,421 @@
<event name="OnUpdateUI"></event>
</object>
</object>
<object class="sizeritem" expanded="1">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="1">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Chad Myslinsky</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_dChadMyslinsky</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass"></property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
<event name="OnChar"></event>
<event name="OnEnterWindow"></event>
<event name="OnEraseBackground"></event>
<event name="OnKeyDown"></event>
<event name="OnKeyUp"></event>
<event name="OnKillFocus"></event>
<event name="OnLeaveWindow"></event>
<event name="OnLeftDClick"></event>
<event name="OnLeftDown"></event>
<event name="OnLeftUp"></event>
<event name="OnMiddleDClick"></event>
<event name="OnMiddleDown"></event>
<event name="OnMiddleUp"></event>
<event name="OnMotion"></event>
<event name="OnMouseEvents"></event>
<event name="OnMouseWheel"></event>
<event name="OnPaint"></event>
<event name="OnRightDClick"></event>
<event name="OnRightDown"></event>
<event name="OnRightUp"></event>
<event name="OnSetFocus"></event>
<event name="OnSize"></event>
<event name="OnUpdateUI"></event>
</object>
</object>
<object class="sizeritem" expanded="1">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="1">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Charlie Bruckner</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_dCharlieBruckner</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass"></property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
<event name="OnChar"></event>
<event name="OnEnterWindow"></event>
<event name="OnEraseBackground"></event>
<event name="OnKeyDown"></event>
<event name="OnKeyUp"></event>
<event name="OnKillFocus"></event>
<event name="OnLeaveWindow"></event>
<event name="OnLeftDClick"></event>
<event name="OnLeftDown"></event>
<event name="OnLeftUp"></event>
<event name="OnMiddleDClick"></event>
<event name="OnMiddleDown"></event>
<event name="OnMiddleUp"></event>
<event name="OnMotion"></event>
<event name="OnMouseEvents"></event>
<event name="OnMouseWheel"></event>
<event name="OnPaint"></event>
<event name="OnRightDClick"></event>
<event name="OnRightDown"></event>
<event name="OnRightUp"></event>
<event name="OnSetFocus"></event>
<event name="OnSize"></event>
<event name="OnUpdateUI"></event>
</object>
</object>
<object class="sizeritem" expanded="1">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="1">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Jordan Parker</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_dJordanParker</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass"></property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
<event name="OnChar"></event>
<event name="OnEnterWindow"></event>
<event name="OnEraseBackground"></event>
<event name="OnKeyDown"></event>
<event name="OnKeyUp"></event>
<event name="OnKillFocus"></event>
<event name="OnLeaveWindow"></event>
<event name="OnLeftDClick"></event>
<event name="OnLeftDown"></event>
<event name="OnLeftUp"></event>
<event name="OnMiddleDClick"></event>
<event name="OnMiddleDown"></event>
<event name="OnMiddleUp"></event>
<event name="OnMotion"></event>
<event name="OnMouseEvents"></event>
<event name="OnMouseWheel"></event>
<event name="OnPaint"></event>
<event name="OnRightDClick"></event>
<event name="OnRightDown"></event>
<event name="OnRightUp"></event>
<event name="OnSetFocus"></event>
<event name="OnSize"></event>
<event name="OnUpdateUI"></event>
</object>
</object>
<object class="sizeritem" expanded="1">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="1">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Robert Chave</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_dRobertChave</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass"></property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
<event name="OnChar"></event>
<event name="OnEnterWindow"></event>
<event name="OnEraseBackground"></event>
<event name="OnKeyDown"></event>
<event name="OnKeyUp"></event>
<event name="OnKillFocus"></event>
<event name="OnLeaveWindow"></event>
<event name="OnLeftDClick"></event>
<event name="OnLeftDown"></event>
<event name="OnLeftUp"></event>
<event name="OnMiddleDClick"></event>
<event name="OnMiddleDown"></event>
<event name="OnMiddleUp"></event>
<event name="OnMotion"></event>
<event name="OnMouseEvents"></event>
<event name="OnMouseWheel"></event>
<event name="OnPaint"></event>
<event name="OnRightDClick"></event>
<event name="OnRightDown"></event>
<event name="OnRightUp"></event>
<event name="OnSetFocus"></event>
<event name="OnSize"></event>
<event name="OnUpdateUI"></event>
</object>
</object>
<object class="sizeritem" expanded="1">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="1">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Marvin Calvert</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_dMarvinCalvert</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass">; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
<event name="OnChar"></event>
<event name="OnEnterWindow"></event>
<event name="OnEraseBackground"></event>
<event name="OnKeyDown"></event>
<event name="OnKeyUp"></event>
<event name="OnKillFocus"></event>
<event name="OnLeaveWindow"></event>
<event name="OnLeftDClick"></event>
<event name="OnLeftDown"></event>
<event name="OnLeftUp"></event>
<event name="OnMiddleDClick"></event>
<event name="OnMiddleDown"></event>
<event name="OnMiddleUp"></event>
<event name="OnMotion"></event>
<event name="OnMouseEvents"></event>
<event name="OnMouseWheel"></event>
<event name="OnPaint"></event>
<event name="OnRightDClick"></event>
<event name="OnRightDown"></event>
<event name="OnRightUp"></event>
<event name="OnSetFocus"></event>
<event name="OnSize"></event>
<event name="OnUpdateUI"></event>
</object>
</object>
</object>
</object>
</object>

View File

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////
// C++ code generated with wxFormBuilder (version Oct 27 2017)
// C++ code generated with wxFormBuilder (version Nov 6 2017)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
@ -365,6 +365,26 @@ AboutDialogBase::AboutDialogBase(wxWindow* parent, wxWindowID id, const wxString
m_dPetrikaJaneku->Wrap( -1 );
m_dSizer->Add( m_dPetrikaJaneku, 0, wxALL, 5 );
m_dChadMyslinsky = new wxStaticText( m_dScroll, wxID_ANY, wxT("Chad Myslinsky"), wxDefaultPosition, wxDefaultSize, 0 );
m_dChadMyslinsky->Wrap( -1 );
m_dSizer->Add( m_dChadMyslinsky, 0, wxALL, 5 );
m_dCharlieBruckner = new wxStaticText( m_dScroll, wxID_ANY, wxT("Charlie Bruckner"), wxDefaultPosition, wxDefaultSize, 0 );
m_dCharlieBruckner->Wrap( -1 );
m_dSizer->Add( m_dCharlieBruckner, 0, wxALL, 5 );
m_dJordanParker = new wxStaticText( m_dScroll, wxID_ANY, wxT("Jordan Parker"), wxDefaultPosition, wxDefaultSize, 0 );
m_dJordanParker->Wrap( -1 );
m_dSizer->Add( m_dJordanParker, 0, wxALL, 5 );
m_dRobertChave = new wxStaticText( m_dScroll, wxID_ANY, wxT("Robert Chave"), wxDefaultPosition, wxDefaultSize, 0 );
m_dRobertChave->Wrap( -1 );
m_dSizer->Add( m_dRobertChave, 0, wxALL, 5 );
m_dMarvinCalvert = new wxStaticText( m_dScroll, wxID_ANY, wxT("Marvin Calvert"), wxDefaultPosition, wxDefaultSize, 0 );
m_dMarvinCalvert->Wrap( -1 );
m_dSizer->Add( m_dMarvinCalvert, 0, wxALL, 5 );
m_dBSizer->Add( m_dSizer, 1, wxALL|wxEXPAND, 5 );

View File

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////
// C++ code generated with wxFormBuilder (version Oct 27 2017)
// C++ code generated with wxFormBuilder (version Nov 6 2017)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
@ -114,6 +114,11 @@ protected:
wxStaticText* m_dSergeVanderTorre;
wxStaticText* m_dDieterSchneider;
wxStaticText* m_dPetrikaJaneku;
wxStaticText* m_dChadMyslinsky;
wxStaticText* m_dCharlieBruckner;
wxStaticText* m_dJordanParker;
wxStaticText* m_dRobertChave;
wxStaticText* m_dMarvinCalvert;
wxScrolledWindow* m_stScroll;
wxStaticText* m_stHeader;
wxStaticLine* m_stDivider1;

View File

@ -104,18 +104,27 @@ void PrimaryGLContext::DrawDemodInfo(DemodulatorInstancePtr demod, RGBA4f color,
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
bool soloMode = wxGetApp().getSoloMode();
bool isRecording = demod->isRecording();
bool isSolo = soloMode && demod == wxGetApp().getDemodMgr().getLastActiveDemodulator();
RGBA4f labelBg(0, 0, 0, 0.35f);
if (isSolo) {
glColor4f(0.8f, 0.8f, 0, 0.35f);
labelBg.r = labelBg.g = 0.8f;
} else if (demod->isMuted()) {
glColor4f(0.8f, 0, 0, 0.35f);
labelBg.r = 0.8f;
} else if (soloMode) {
glColor4f(0.2f, 0, 0, 0.35f);
} else {
glColor4f(0, 0, 0, 0.35f);
labelBg.r = 0.2f;
}
// TODO: Better recording indicator... pulsating red circle?
if (isRecording) {
auto t = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count();
labelBg.g = sinf(2.0f * M_PI * (float(t) / 1000.0f)) * 0.25f + 0.75f;
}
glColor4f(labelBg.r, labelBg.g, labelBg.b, labelBg.a);
glBegin(GL_QUADS);
glVertex3f(uxPos - ofsLeft, hPos + labelHeight, 0.0);
glVertex3f(uxPos - ofsLeft, -1.0, 0.0);
@ -156,18 +165,29 @@ void PrimaryGLContext::DrawDemodInfo(DemodulatorInstancePtr demod, RGBA4f color,
glColor4f(1.0, 1.0, 1.0, 0.8f);
std::string demodLabel = demod->getLabel();
if (demod->isMuted()) {
demodLabel = std::string("[M] ") + demodLabel;
} else if (isSolo) {
demodLabel = std::string("[S] ") + demodLabel;
}
std::string demodLabel, demodPrefix;
if (demod->isDeltaLock()) {
demodLabel.append(" [V]");
demodPrefix.append("V");
}
if (isRecording) {
demodPrefix.append("R");
}
if (demod->isMuted()) {
demodPrefix.append("M");
} else if (isSolo) {
demodPrefix.append("S");
}
// Set the prefix
if (!demodPrefix.empty()) {
demodLabel = "[" + demodPrefix + "] ";
}
// Append the default label
demodLabel.append(demod->getLabel());
if (demod->getDemodulatorType() == "USB") {
GLFont::getFont(16, GLFont::getScaleFactor()).drawString(demodLabel, uxPos, hPos, GLFont::GLFONT_ALIGN_LEFT, GLFont::GLFONT_ALIGN_CENTER, 0, 0, true);
} else if (demod->getDemodulatorType() == "LSB") {

View File

@ -565,25 +565,25 @@ void WaterfallCanvas::updateHoverState() {
mouseTracker.setVertDragLock(true);
mouseTracker.setHorizDragLock(false);
setStatusText("Click and drag to change demodulator bandwidth. SPACE or numeric key for direct frequency input. [, ] to nudge, M for mute, D to delete, C to center, E to edit label.");
setStatusText("Drag to change bandwidth. SPACE or 0-9 for direct frequency input. [, ] to nudge, M for mute, D to delete, C to center, E to edit label, R to record.");
} else {
SetCursor(wxCURSOR_SIZING);
nextDragState = WF_DRAG_FREQUENCY;
mouseTracker.setVertDragLock(true);
mouseTracker.setHorizDragLock(false);
setStatusText("Click and drag to change demodulator frequency; SPACE or numeric key for direct input. [, ] to nudge, M for mute, D to delete, C to center, E to edit label.");
setStatusText("Drag to change frequency; SPACE or 0-9 for direct input. [, ] to nudge, M for mute, D to delete, C to center, E to edit label, R to record.");
}
}
else {
SetCursor(wxCURSOR_CROSS);
nextDragState = WF_DRAG_NONE;
if (shiftDown) {
setStatusText("Click to create a new demodulator or hold ALT to drag range, SPACE or numeric key for direct center frequency input.");
setStatusText("Click to create a new demodulator or hold ALT to drag new range.");
}
else {
setStatusText(
"Click to set active demodulator frequency or hold ALT to drag range; hold SHIFT to create new. Right drag or wheel to Zoom. Arrow keys to navigate/zoom, C to center.");
"Click to set demodulator frequency or hold ALT to drag range; hold SHIFT to create new. Right drag or wheel to Zoom. Arrow keys to navigate/zoom, C to center. Shift-R record/stop all.");
}
}
}