Add files via upload
If building for iOS, replace the Main branch with these files. Signed-off-by: rohithzmoi <166651631+rohithzmoi@users.noreply.github.com>
This commit is contained in:
parent
c94fee6743
commit
5b75e89df3
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
Copyright (C) 2024 Rohith Namboothiri
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
|
||||
extern "C" void setupAVAudioSession() {
|
||||
@try {
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
NSError *error = nil;
|
||||
|
||||
NSLog(@"Setting up AVAudioSession...");
|
||||
|
||||
|
||||
|
||||
BOOL success = [session setCategory:AVAudioSessionCategoryPlayAndRecord
|
||||
withOptions:AVAudioSessionCategoryOptionAllowBluetooth |
|
||||
AVAudioSessionCategoryOptionMixWithOthers |
|
||||
AVAudioSessionCategoryOptionDefaultToSpeaker
|
||||
error:&error];
|
||||
|
||||
if (!success || error) {
|
||||
NSLog(@"Error setting AVAudioSession category: %@, code: %ld", error.localizedDescription, (long)error.code);
|
||||
return;
|
||||
}
|
||||
NSLog(@"AVAudioSession category set to PlayAndRecord with DefaultToSpeaker option");
|
||||
|
||||
|
||||
success = [session setActive:YES error:&error];
|
||||
if (!success || error) {
|
||||
NSLog(@"Error activating AVAudioSession: %@, code: %ld", error.localizedDescription, (long)error.code);
|
||||
return;
|
||||
}
|
||||
NSLog(@"AVAudioSession activated successfully");
|
||||
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:AVAudioSessionInterruptionNotification
|
||||
object:session
|
||||
queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(NSNotification *note) {
|
||||
AVAudioSessionInterruptionType interruptionType = (AVAudioSessionInterruptionType)[note.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
|
||||
if (interruptionType == AVAudioSessionInterruptionTypeBegan) {
|
||||
NSLog(@"Audio session interruption began");
|
||||
// Pause audio processing
|
||||
} else if (interruptionType == AVAudioSessionInterruptionTypeEnded) {
|
||||
NSLog(@"Audio session interruption ended, attempting to reactivate...");
|
||||
NSError *activationError = nil;
|
||||
BOOL reactivationSuccess = [session setActive:YES error:&activationError];
|
||||
if (!reactivationSuccess) {
|
||||
NSLog(@"Error re-activating AVAudioSession after interruption: %@, code: %ld", activationError.localizedDescription, (long)activationError.code);
|
||||
} else {
|
||||
NSLog(@"Audio session successfully reactivated after interruption");
|
||||
// Resume audio processing
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:AVAudioSessionRouteChangeNotification
|
||||
object:session
|
||||
queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(NSNotification *note) {
|
||||
AVAudioSessionRouteChangeReason reason = (AVAudioSessionRouteChangeReason)[note.userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
|
||||
NSLog(@"Audio route changed, reason: %lu", (unsigned long)reason);
|
||||
if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable ||
|
||||
reason == AVAudioSessionRouteChangeReasonNewDeviceAvailable ||
|
||||
reason == AVAudioSessionRouteChangeReasonOverride) {
|
||||
NSLog(@"Audio route change detected, attempting to reactivate...");
|
||||
NSError *activationError = nil;
|
||||
BOOL reactivationSuccess = [session setActive:YES error:&activationError];
|
||||
if (!reactivationSuccess) {
|
||||
NSLog(@"Error re-activating AVAudioSession after route change: %@, code: %ld", activationError.localizedDescription, (long)activationError.code);
|
||||
} else {
|
||||
NSLog(@"Audio session successfully reactivated after route change");
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"Exception setting up AVAudioSession: %@", exception.reason);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extern "C" void deactivateAVAudioSession() {
|
||||
@try {
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
NSError *error = nil;
|
||||
|
||||
NSLog(@"Deactivating AVAudioSession...");
|
||||
|
||||
|
||||
BOOL success = [session setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
|
||||
if (!success || error) {
|
||||
NSLog(@"Error deactivating AVAudioSession: %@, code: %ld", error.localizedDescription, (long)error.code);
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"AVAudioSession deactivated successfully");
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"Exception deactivating AVAudioSession: %@", exception.reason);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle background audio setup
|
||||
extern "C" void setupBackgroundAudio() {
|
||||
@try {
|
||||
|
||||
__block UIBackgroundTaskIdentifier bgTask = UIBackgroundTaskInvalid;
|
||||
|
||||
|
||||
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
|
||||
|
||||
NSLog(@"Background task expired. Cleaning up...");
|
||||
|
||||
|
||||
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
|
||||
bgTask = UIBackgroundTaskInvalid;
|
||||
}];
|
||||
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
NSError *error = nil;
|
||||
|
||||
NSLog(@"Configuring AVAudioSession for background...");
|
||||
|
||||
|
||||
[session setActive:NO error:nil];
|
||||
|
||||
|
||||
BOOL success = [session setCategory:AVAudioSessionCategoryPlayAndRecord
|
||||
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker |
|
||||
AVAudioSessionCategoryOptionAllowBluetooth |
|
||||
AVAudioSessionCategoryOptionAllowBluetoothA2DP |
|
||||
AVAudioSessionCategoryOptionMixWithOthers
|
||||
error:&error];
|
||||
|
||||
if (!success || error) {
|
||||
NSLog(@"Error setting AVAudioSession category for background: %@, code: %ld", error.localizedDescription, (long)error.code);
|
||||
} else {
|
||||
NSLog(@"AVAudioSession category set successfully for background audio");
|
||||
|
||||
|
||||
success = [session setActive:YES error:&error];
|
||||
if (!success || error) {
|
||||
NSLog(@"Error activating AVAudioSession in background: %@, code: %ld", error.localizedDescription, (long)error.code);
|
||||
} else {
|
||||
NSLog(@"AVAudioSession activated successfully in background");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
|
||||
bgTask = UIBackgroundTaskInvalid;
|
||||
});
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"Exception setting up AVAudioSession: %@", exception.reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
QT += quick quickcontrols2 network multimedia
|
||||
//QT += xlsx
|
||||
|
||||
unix:!ios:QT += serialport
|
||||
win32:QT += serialport
|
||||
!win32:LIBS += -ldl
|
||||
win32:LIBS += -lws2_32
|
||||
win32:QMAKE_LFLAGS += -static
|
||||
QMAKE_LFLAGS_WINDOWS += --enable-stdcall-fixup
|
||||
RC_ICONS = images/droidstar.ico
|
||||
ICON = images/droidstar.icns
|
||||
macx:LIBS += -framework AVFoundation
|
||||
macx:QMAKE_MACOSX_DEPLOYMENT_TARGET = 12.0
|
||||
macx:QMAKE_INFO_PLIST = Info.plist.mac
|
||||
ios:LIBS += -framework AVFoundation -framework AudioToolbox -framework UIKit
|
||||
ios:QMAKE_IOS_DEPLOYMENT_TARGET=14.0
|
||||
ios:QMAKE_TARGET_BUNDLE_PREFIX = org.dudetronics
|
||||
ios:QMAKE_BUNDLE = droidstar
|
||||
ios:VERSION = 0.43.20
|
||||
ios:Q_ENABLE_BITCODE.name = ENABLE_BITCODE
|
||||
ios:Q_ENABLE_BITCODE.value = NO
|
||||
ios:QMAKE_MAC_XCODE_SETTINGS += Q_ENABLE_BITCODE
|
||||
ios:QMAKE_ASSET_CATALOGS += Images.xcassets
|
||||
ios:QMAKE_INFO_PLIST = Info.plist
|
||||
VERSION_BUILD='$(shell cd $$PWD;git rev-parse --short HEAD)'
|
||||
DEFINES += VERSION_NUMBER=\"\\\"$${VERSION_BUILD}\\\"\"
|
||||
DEFINES += QT_DEPRECATED_WARNINGS
|
||||
#DEFINES += QT_DEBUG_PLUGINS=1
|
||||
#DEFINES += VOCODER_PLUGIN
|
||||
#DEFINES += USE_FLITE
|
||||
#DEFINES += USE_EXTERNAL_CODEC2
|
||||
#DEFINES += USE_MD380_VOCODER
|
||||
|
||||
SOURCES += \
|
||||
CRCenc.cpp \
|
||||
vuidupdater.cpp \
|
||||
LogHandler.cpp \
|
||||
Golay24128.cpp \
|
||||
M17Convolution.cpp \
|
||||
SHA256.cpp \
|
||||
YSFConvolution.cpp \
|
||||
YSFFICH.cpp \
|
||||
audioengine.cpp \
|
||||
cbptc19696.cpp \
|
||||
cgolay2087.cpp \
|
||||
chamming.cpp \
|
||||
crs129.cpp \
|
||||
dcs.cpp \
|
||||
dmr.cpp \
|
||||
droidstar.cpp \
|
||||
httpmanager.cpp \
|
||||
iax.cpp \
|
||||
imbe_vocoder/aux_sub.cc \
|
||||
imbe_vocoder/basicop2.cc \
|
||||
imbe_vocoder/ch_decode.cc \
|
||||
imbe_vocoder/ch_encode.cc \
|
||||
imbe_vocoder/dc_rmv.cc \
|
||||
imbe_vocoder/decode.cc \
|
||||
imbe_vocoder/dsp_sub.cc \
|
||||
imbe_vocoder/encode.cc \
|
||||
imbe_vocoder/imbe_vocoder.cc \
|
||||
imbe_vocoder/imbe_vocoder_impl.cc \
|
||||
imbe_vocoder/math_sub.cc \
|
||||
imbe_vocoder/pe_lpf.cc \
|
||||
imbe_vocoder/pitch_est.cc \
|
||||
imbe_vocoder/pitch_ref.cc \
|
||||
imbe_vocoder/qnt_sub.cc \
|
||||
imbe_vocoder/rand_gen.cc \
|
||||
imbe_vocoder/sa_decode.cc \
|
||||
imbe_vocoder/sa_encode.cc \
|
||||
imbe_vocoder/sa_enh.cc \
|
||||
imbe_vocoder/tbls.cc \
|
||||
imbe_vocoder/uv_synt.cc \
|
||||
imbe_vocoder/v_synt.cc \
|
||||
imbe_vocoder/v_uv_det.cc \
|
||||
m17.cpp \
|
||||
main.cpp \
|
||||
mode.cpp \
|
||||
nxdn.cpp \
|
||||
p25.cpp \
|
||||
ref.cpp \
|
||||
xrf.cpp \
|
||||
ysf.cpp
|
||||
# Android-specific source files
|
||||
android:SOURCES += androidserialport.cpp
|
||||
|
||||
# Non-iOS source files
|
||||
!ios:SOURCES += serialambe.cpp serialmodem.cpp
|
||||
|
||||
# Objective-C source files for macOS and iOS
|
||||
macx:OBJECTIVE_SOURCES += micpermission.mm
|
||||
ios:OBJECTIVE_SOURCES += micpermission.mm AudioSessionManager.mm
|
||||
|
||||
# Enable background audio mode for iOS
|
||||
ios:QMAKE_MAC_XCODE_SETTINGS += QMAKE_IOS_BACKGROUND_MODES = YES
|
||||
ios:QMAKE_INFO_PLIST_EXTRA += "<key>UIBackgroundModes</key>"
|
||||
ios:QMAKE_INFO_PLIST_EXTRA += "<array>"
|
||||
ios:QMAKE_INFO_PLIST_EXTRA += " <string>audio</string>"
|
||||
ios:QMAKE_INFO_PLIST_EXTRA += "</array>"
|
||||
ios:QMAKE_CXXFLAGS += -fobjc-arc
|
||||
|
||||
|
||||
|
||||
|
||||
resources.files = main.qml AboutTab.qml HostsTab.qml LogTab.qml MainTab.qml SettingsTab.qml fontawesome-webfont.ttf QsoTab.qml
|
||||
resources.prefix = /$${TARGET}
|
||||
RESOURCES += resources
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
# Additional import path used to resolve QML modules just for Qt Quick Designer
|
||||
QML_DESIGNER_IMPORT_PATH =
|
||||
|
||||
# Default rules for deployment.
|
||||
qnx: target.path = /tmp/$${TARGET}/bin
|
||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||
!isEmpty(target.path): INSTALLS += target
|
||||
|
||||
HEADERS += \
|
||||
CRCenc.h \
|
||||
DMRDefines.h \
|
||||
vuidupdater.h \
|
||||
LogHandler.h \
|
||||
AudioSessionManager.h \
|
||||
Golay24128.h \
|
||||
M17Convolution.h \
|
||||
M17Defines.h \
|
||||
MMDVMDefines.h \
|
||||
SHA256.h \
|
||||
YSFConvolution.h \
|
||||
YSFFICH.h \
|
||||
audioengine.h \
|
||||
cbptc19696.h \
|
||||
cgolay2087.h \
|
||||
chamming.h \
|
||||
crs129.h \
|
||||
dcs.h \
|
||||
dmr.h \
|
||||
droidstar.h \
|
||||
httpmanager.h \
|
||||
iax.h \
|
||||
iaxdefines.h \
|
||||
imbe_vocoder/aux_sub.h \
|
||||
imbe_vocoder/basic_op.h \
|
||||
imbe_vocoder/ch_decode.h \
|
||||
imbe_vocoder/ch_encode.h \
|
||||
imbe_vocoder/dc_rmv.h \
|
||||
imbe_vocoder/decode.h \
|
||||
imbe_vocoder/dsp_sub.h \
|
||||
imbe_vocoder/encode.h \
|
||||
imbe_vocoder/globals.h \
|
||||
imbe_vocoder/imbe.h \
|
||||
imbe_vocoder/imbe_vocoder.h \
|
||||
imbe_vocoder/imbe_vocoder_api.h \
|
||||
imbe_vocoder/imbe_vocoder_impl.h \
|
||||
imbe_vocoder/math_sub.h \
|
||||
imbe_vocoder/pe_lpf.h \
|
||||
imbe_vocoder/pitch_est.h \
|
||||
imbe_vocoder/pitch_ref.h \
|
||||
imbe_vocoder/qnt_sub.h \
|
||||
imbe_vocoder/rand_gen.h \
|
||||
imbe_vocoder/sa_decode.h \
|
||||
imbe_vocoder/sa_encode.h \
|
||||
imbe_vocoder/sa_enh.h \
|
||||
imbe_vocoder/tbls.h \
|
||||
imbe_vocoder/typedef.h \
|
||||
imbe_vocoder/typedefs.h \
|
||||
imbe_vocoder/uv_synt.h \
|
||||
imbe_vocoder/v_synt.h \
|
||||
imbe_vocoder/v_uv_det.h \
|
||||
m17.h \
|
||||
mode.h \
|
||||
nxdn.h \
|
||||
p25.h \
|
||||
ref.h \
|
||||
vocoder_plugin.h \
|
||||
xrf.h \
|
||||
ysf.h
|
||||
|
||||
!contains(DEFINES, USE_EXTERNAL_CODEC2){
|
||||
HEADERS += \
|
||||
codec2/codec2_api.h \
|
||||
codec2/codec2_internal.h \
|
||||
codec2/defines.h \
|
||||
codec2/kiss_fft.h \
|
||||
codec2/lpc.h \
|
||||
codec2/nlp.h \
|
||||
codec2/qbase.h \
|
||||
codec2/quantise.h
|
||||
SOURCES += \
|
||||
codec2/codebooks.cpp \
|
||||
codec2/codec2.cpp \
|
||||
codec2/kiss_fft.cpp \
|
||||
codec2/lpc.cpp \
|
||||
codec2/nlp.cpp \
|
||||
codec2/pack.cpp \
|
||||
codec2/qbase.cpp \
|
||||
codec2/quantise.cpp
|
||||
}
|
||||
contains(DEFINES, USE_EXTERNAL_CODEC2){
|
||||
LIBS += -lcodec2
|
||||
}
|
||||
!contains(DEFINES, VOCODER_PLUGIN){
|
||||
HEADERS += \
|
||||
mbe/ambe3600x2400_const.h \
|
||||
mbe/ambe3600x2450_const.h \
|
||||
mbe/ecc_const.h \
|
||||
mbe/mbelib.h \
|
||||
mbe/mbelib_const.h \
|
||||
mbe/mbelib_parms.h \
|
||||
mbe/vocoder_plugin.h \
|
||||
mbe/vocoder_plugin_api.h \
|
||||
mbe/vocoder_tables.h
|
||||
SOURCES += \
|
||||
mbe/ambe3600x2400.c \
|
||||
mbe/ambe3600x2450.c \
|
||||
mbe/ecc.c \
|
||||
mbe/mbelib.c \
|
||||
mbe/vocoder_plugin.cpp
|
||||
}
|
||||
|
||||
android:HEADERS += androidserialport.h
|
||||
macx:HEADERS += micpermission.h
|
||||
!ios:HEADERS += serialambe.h serialmodem.h
|
||||
android:ANDROID_VERSION_CODE = 79
|
||||
android:QT_ANDROID_MIN_SDK_VERSION = 31
|
||||
|
||||
contains(ANDROID_TARGET_ARCH,armeabi-v7a) {
|
||||
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
|
||||
}
|
||||
|
||||
contains(ANDROID_TARGET_ARCH,arm64-v8a) {
|
||||
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
|
||||
}
|
||||
|
||||
contains(DEFINES, USE_FLITE){
|
||||
LIBS += -lflite_cmu_us_slt -lflite_cmu_us_kal16 -lflite_cmu_us_awb -lflite_cmu_us_rms -lflite_usenglish -lflite_cmulex -lflite -lasound
|
||||
}
|
||||
contains(DEFINES, USE_MD380_VOCODER){
|
||||
LIBS += -lmd380_vocoder -Xlinker --section-start=.firmware=0x0800C000 -Xlinker --section-start=.sram=0x20000000
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
#include "LogHandler.h"
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
#include <QTextStream>
|
||||
#include <QStandardPaths>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
|
||||
LogHandler::LogHandler(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString LogHandler::getFilePath(const QString &fileName) const
|
||||
{
|
||||
QString dirPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
QDir dir(dirPath);
|
||||
|
||||
if (!dir.exists()) {
|
||||
if (!dir.mkpath(dirPath)) {
|
||||
qDebug() << "Failed to create directory:" << dirPath;
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
QString filePath = dirPath + "/" + fileName;
|
||||
qDebug() << "Log file path:" << filePath;
|
||||
return filePath;
|
||||
}
|
||||
|
||||
bool LogHandler::saveLog(const QString &fileName, const QJsonArray &logData)
|
||||
{
|
||||
QString filePath = getFilePath(fileName);
|
||||
if (filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
qDebug() << "Failed to open file for writing:" << file.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc(logData);
|
||||
file.write(doc.toJson());
|
||||
file.close();
|
||||
qDebug() << "Log saved successfully.";
|
||||
return true;
|
||||
}
|
||||
|
||||
QJsonArray LogHandler::loadLog(const QString &fileName)
|
||||
{
|
||||
QString filePath = getFilePath(fileName);
|
||||
QJsonArray logData;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qDebug() << "Failed to open file for reading:" << file.errorString();
|
||||
return logData;
|
||||
}
|
||||
|
||||
QByteArray data = file.readAll();
|
||||
QJsonDocument doc(QJsonDocument::fromJson(data));
|
||||
if (!doc.isNull() && doc.isArray()) {
|
||||
logData = doc.array();
|
||||
qDebug() << "Log loaded successfully.";
|
||||
} else {
|
||||
qDebug() << "Failed to parse JSON log.";
|
||||
}
|
||||
|
||||
file.close();
|
||||
return logData;
|
||||
}
|
||||
|
||||
bool LogHandler::clearLog(const QString &fileName)
|
||||
{
|
||||
QString filePath = getFilePath(fileName);
|
||||
QFile file(filePath);
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
file.close();
|
||||
qDebug() << "Log cleared successfully.";
|
||||
return true;
|
||||
}
|
||||
qDebug() << "Failed to clear log:" << file.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
QString LogHandler::getDSLogPath() const {
|
||||
QString documentsPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
||||
QString dsLogPath = documentsPath + "/DSLog";
|
||||
QDir dsLogDir(dsLogPath);
|
||||
|
||||
qDebug() << "Documents path: " << documentsPath;
|
||||
qDebug() << "DSLog path: " << dsLogPath;
|
||||
|
||||
if (!dsLogDir.exists()) {
|
||||
if (!dsLogDir.mkpath(dsLogPath)) {
|
||||
qDebug() << "Failed to create DSLog directory.";
|
||||
return QString();
|
||||
} else {
|
||||
qDebug() << "DSLog directory created successfully.";
|
||||
}
|
||||
} else {
|
||||
qDebug() << "DSLog directory already exists.";
|
||||
}
|
||||
|
||||
return dsLogPath;
|
||||
}
|
||||
|
||||
bool LogHandler::exportLogToCsv(const QString &fileName, const QJsonArray &logData) {
|
||||
QString dsLogPath = getDSLogPath();
|
||||
if (dsLogPath.isEmpty()) {
|
||||
qDebug() << "DSLog path is not available.";
|
||||
return false;
|
||||
}
|
||||
|
||||
QString filePath = dsLogPath + "/" + QFileInfo(fileName).fileName();
|
||||
|
||||
qDebug() << "Attempting to save file at: " << filePath;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
qDebug() << "Failed to open file for writing:" << file.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
|
||||
// Write the CSV headers
|
||||
out << "Sr.No,Callsign,DMR ID,TGID,Handle,Country,Time\n";
|
||||
|
||||
// Write the log data to the CSV file
|
||||
for (int i = 0; i < logData.size(); ++i) {
|
||||
QJsonObject entry = logData[i].toObject();
|
||||
out << entry["serialNumber"].toInt() << ","
|
||||
<< entry["callsign"].toString() << ","
|
||||
<< entry["dmrID"].toInt() << ","
|
||||
<< entry["tgid"].toInt() << ","
|
||||
<< entry["fname"].toString() << ","
|
||||
<< entry["country"].toString() << ","
|
||||
<< entry["currentTime"].toString() << "\n";
|
||||
}
|
||||
|
||||
file.close();
|
||||
qDebug() << "Log exported successfully to" << filePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LogHandler::exportLogToAdif(const QString &fileName, const QJsonArray &logData) {
|
||||
QString dsLogPath = getDSLogPath();
|
||||
if (dsLogPath.isEmpty()) {
|
||||
qDebug() << "DSLog path is not available.";
|
||||
return false;
|
||||
}
|
||||
|
||||
QString filePath = dsLogPath + "/" + QFileInfo(fileName).fileName();
|
||||
|
||||
qDebug() << "Attempting to save ADIF file at: " << filePath;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
qDebug() << "Failed to open file for writing:" << file.errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
|
||||
// Write the ADIF headers
|
||||
out << "ADIF Export\n";
|
||||
out << "<EOH>\n"; // End of Header
|
||||
|
||||
// Write each QSO record in ADIF format
|
||||
for (int i = 0; i < logData.size(); ++i) {
|
||||
QJsonObject entry = logData[i].toObject();
|
||||
|
||||
// Extract and format date and time
|
||||
QString currentTime = entry["currentTime"].toString();
|
||||
QString qsoDate = currentTime.left(10).remove('-'); // Format: YYYYMMDD
|
||||
QString timeOn = currentTime.mid(11, 8).remove(':'); // Format: HHMMSS
|
||||
|
||||
// Write each QSO record with valid ADIF tags
|
||||
out << "<CALL:" << entry["callsign"].toString().length() << ">" << entry["callsign"].toString();
|
||||
out << "<BAND:4>70CM"; // Band is hardcoded as "70CM"
|
||||
out << "<MODE:12>DIGITALVOICE"; // Mode is set to "DIGITALVOICE"
|
||||
// Include the first name in the ADIF record
|
||||
out << "<NAME:" << entry["fname"].toString().length() << ">" << entry["fname"].toString();
|
||||
|
||||
out << "<QSO_DATE:" << qsoDate.length() << ">" << qsoDate;
|
||||
out << "<TIME_ON:6>" << timeOn;
|
||||
out << "<EOR>\n"; // End of Record
|
||||
}
|
||||
|
||||
file.close();
|
||||
qDebug() << "Log exported successfully to" << filePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Extract and display a user-friendly path
|
||||
QString LogHandler::getFriendlyPath(const QString &fullPath) const {
|
||||
return fullPath.mid(fullPath.indexOf("/Documents/"));
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#ifndef LOGHANDLER_H
|
||||
#define LOGHANDLER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonArray>
|
||||
|
||||
class LogHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LogHandler(QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE bool saveLog(const QString &fileName, const QJsonArray &logData);
|
||||
Q_INVOKABLE QJsonArray loadLog(const QString &fileName);
|
||||
Q_INVOKABLE bool clearLog(const QString &fileName);
|
||||
Q_INVOKABLE bool exportLogToCsv(const QString &fileName, const QJsonArray &logData);
|
||||
Q_INVOKABLE QString getDSLogPath() const; // Updated function name
|
||||
Q_INVOKABLE QString getFriendlyPath(const QString &path) const; // Mark as Q_INVOKABLE
|
||||
|
||||
Q_INVOKABLE bool exportLogToAdif(const QString &fileName, const QJsonArray &logData);
|
||||
|
||||
|
||||
|
||||
private:
|
||||
QString getFilePath(const QString &fileName) const;
|
||||
//QString getDownloadsPath() const;
|
||||
|
||||
};
|
||||
|
||||
#endif // LOGHANDLER_H
|
||||
|
|
@ -0,0 +1,591 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Dialogs
|
||||
|
||||
Item {
|
||||
id: qsoTab
|
||||
width: 400
|
||||
height: 600
|
||||
|
||||
property MainTab mainTab: null // This is correctly set from main.qml
|
||||
property int dmrID: -1
|
||||
property int tgid: -1
|
||||
property string logFileName: "logs.json"
|
||||
property string savedFilePath: ""
|
||||
//property int latestSerialNumber: 0 // Track the latest serial number
|
||||
property int latestSerialNumber: 1
|
||||
|
||||
// Signals to update MainTab
|
||||
signal firstRowDataChanged(string serialNumber, string callsign, string handle, string country)
|
||||
signal secondRowDataChanged(string serialNumber, string callsign, string handle, string country)
|
||||
|
||||
ListModel {
|
||||
id: logModel
|
||||
}
|
||||
|
||||
// Update the first and second row data in a single function
|
||||
function updateRowData() {
|
||||
if (logModel.count > 0) {
|
||||
var firstRow = logModel.get(0);
|
||||
firstRowDataChanged(firstRow.serialNumber, firstRow.callsign, firstRow.fname, firstRow.country);
|
||||
} else {
|
||||
firstRowDataChanged("0", "N/A", "N/A", "N/A");
|
||||
}
|
||||
|
||||
if (logModel.count > 1) {
|
||||
var secondRow = logModel.get(1);
|
||||
secondRowDataChanged(secondRow.serialNumber, secondRow.callsign, secondRow.fname, secondRow.country);
|
||||
} else {
|
||||
secondRowDataChanged("0", "N/A", "N/A", "N/A");
|
||||
}
|
||||
}
|
||||
|
||||
// Connections to update row data whenever the model changes
|
||||
Connections {
|
||||
target: logModel
|
||||
onCountChanged: {
|
||||
updateRowData();
|
||||
saveSettings(); // Save logs when the model changes
|
||||
}
|
||||
}
|
||||
|
||||
// Component onCompleted: Initial setup
|
||||
Component.onCompleted: {
|
||||
mainTab.dataUpdated.connect(onDataUpdated);
|
||||
updateRowData(); // Ensure both rows are updated initially
|
||||
loadSettings();
|
||||
if (mainTab === null) {
|
||||
console.error("mainTab is null. Ensure it is passed correctly from the parent.");
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
var logData = [];
|
||||
for (var i = 0; i < logModel.count; i++) {
|
||||
logData.push(logModel.get(i));
|
||||
}
|
||||
logHandler.saveLog(logFileName, logData);
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
var savedData = logHandler.loadLog(logFileName);
|
||||
for (var i = 0; i < savedData.length; i++) {
|
||||
savedData[i].checked = false;
|
||||
logModel.append(savedData[i]);
|
||||
latestSerialNumber = Math.max(latestSerialNumber, savedData[i].serialNumber + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function clearSettings() {
|
||||
logModel.clear();
|
||||
logHandler.clearLog(logFileName);
|
||||
latestSerialNumber = 0; // Reset the serial number counter to 0
|
||||
}
|
||||
|
||||
function exportLog() {
|
||||
saveFileNameDialog.open(); // Prompt for file name
|
||||
}
|
||||
|
||||
// Header Row with Text and Clear Button
|
||||
Text {
|
||||
id: headerText
|
||||
text: "This page logs lastheard stations in descending order. An upgrade to a native Log book is coming soon."
|
||||
wrapMode: Text.WordWrap
|
||||
font.bold: true
|
||||
font.pointSize: 12
|
||||
color: "white"
|
||||
width: parent.width - 50
|
||||
x: 20
|
||||
y: 10
|
||||
}
|
||||
|
||||
Button {
|
||||
id: clearButton
|
||||
text: "Clear"
|
||||
x: 20
|
||||
y: headerText.y + headerText.height + 12
|
||||
onClicked: {
|
||||
clearSettings();
|
||||
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "red" // Set the background color to red
|
||||
radius: 4 // Optional: Add some rounding to the corners
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: clearButton.text
|
||||
color: "white" // Set text color to white for contrast
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.centerIn: parent
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
// Add an Export button
|
||||
Button {
|
||||
id: exportButton
|
||||
text: "Export Log"
|
||||
x: clearButton.x + clearButton.width + 10
|
||||
y: headerText.y + headerText.height + 12
|
||||
onClicked: exportLog()
|
||||
|
||||
background: Rectangle {
|
||||
color: "green" // Set the background color to green
|
||||
radius: 4 // Optional: Add some rounding to the corners
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: exportButton.text
|
||||
color: "white" // Set text color to white for contrast
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.centerIn: parent
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
// Clone the button using properties from mainTab's buttonTX
|
||||
Button {
|
||||
id: clonedButton
|
||||
visible: mainTab.buttonTX.visible
|
||||
enabled: mainTab.buttonTX.enabled
|
||||
x: exportButton.x + exportButton.width + 10
|
||||
y: exportButton.y
|
||||
width: exportButton.width * 1.5 // Increase width to accommodate more text
|
||||
height: exportButton.height
|
||||
background: Rectangle {
|
||||
color: mainTab.buttonTX.tx ? "#800000" : "steelblue"
|
||||
radius: 4
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2 // Add some spacing between the texts
|
||||
|
||||
Text {
|
||||
id: clonedText
|
||||
font.pointSize: 20 // Adjust font size as needed
|
||||
text: qsTr("TX") // Display "TX"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure cloned button follows the same behavior as the original
|
||||
onClicked: mainTab.buttonTX.clicked()
|
||||
onPressed: mainTab.buttonTX.pressed()
|
||||
onReleased: mainTab.buttonTX.released()
|
||||
onCanceled: mainTab.buttonTX.canceled()
|
||||
}
|
||||
|
||||
|
||||
// Dialog to prompt for file name
|
||||
Dialog {
|
||||
id: saveFileNameDialog
|
||||
title: "Enter File Name and Choose Format"
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
modal: true
|
||||
|
||||
contentItem: Column {
|
||||
spacing: 10
|
||||
|
||||
TextField {
|
||||
id: fileNameInput
|
||||
placeholderText: "Enter file name"
|
||||
width: parent.width - 20
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 10
|
||||
RadioButton {
|
||||
id: csvRadioButton
|
||||
text: "CSV"
|
||||
checked: true // Default to CSV
|
||||
}
|
||||
RadioButton {
|
||||
id: adifRadioButton
|
||||
text: "ADIF"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
var logData = [];
|
||||
var hasSelection = false;
|
||||
for (var i = 0; i < logModel.count; i++) {
|
||||
var entry = logModel.get(i);
|
||||
if (entry.checked) {
|
||||
logData.push(entry);
|
||||
hasSelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSelection) { // If no selections, export all
|
||||
for (var i = 0; i < logModel.count; i++) {
|
||||
logData.push(logModel.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
var fileName = fileNameInput.text;
|
||||
var filePath = logHandler.getDSLogPath() + "/" + fileName;
|
||||
|
||||
if (csvRadioButton.checked) {
|
||||
fileName += ".csv";
|
||||
filePath += ".csv";
|
||||
|
||||
if (logHandler.exportLogToCsv(filePath, logData)) {
|
||||
savedFilePath = logHandler.getFriendlyPath(filePath);
|
||||
fileSavedDialog.open();
|
||||
} else {
|
||||
console.error("Failed to save the CSV file.");
|
||||
}
|
||||
} else if (adifRadioButton.checked) {
|
||||
fileName += ".adi";
|
||||
filePath += ".adi";
|
||||
|
||||
if (logHandler.exportLogToAdif(filePath, logData)) {
|
||||
savedFilePath = logHandler.getFriendlyPath(filePath);
|
||||
fileSavedDialog.open();
|
||||
} else {
|
||||
console.error("Failed to save the ADIF file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dialog to show that the file was saved
|
||||
Dialog {
|
||||
id: fileSavedDialog
|
||||
title: "File Saved"
|
||||
standardButtons: Dialog.Ok
|
||||
width: 300 // Set a fixed width for the dialog to avoid binding loops
|
||||
|
||||
onAccepted: {
|
||||
console.log("File saved successfully!");
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "#80c342"
|
||||
radius: 8 // Optional: Add rounded corners
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: "File saved successfully to " + savedFilePath
|
||||
font.pointSize: 14
|
||||
color: "black"
|
||||
wrapMode: Text.WordWrap // Enable text wrapping
|
||||
width: parent.width * 0.9 // Ensure some padding from the edges
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: tableHeader
|
||||
width: parent.width
|
||||
height: 25 // Make the rectangles slightly smaller in height
|
||||
y: clearButton.y + clearButton.height + 10
|
||||
spacing: 4 // Reduce spacing slightly
|
||||
|
||||
Rectangle {
|
||||
width: parent.width / 8
|
||||
height: parent.height
|
||||
color: "darkgrey"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Sr.No"
|
||||
font.bold: true
|
||||
font.pixelSize: 12 // Smaller font size
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "darkgrey"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Callsign"
|
||||
font.bold: true
|
||||
font.pixelSize: 12 // Smaller font size
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "darkgrey"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "DMR ID"
|
||||
font.bold: true
|
||||
font.pixelSize: 12 // Smaller font size
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "darkgrey"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "TGID"
|
||||
font.bold: true
|
||||
font.pixelSize: 12 // Smaller font size
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "darkgrey"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Handle"
|
||||
font.bold: true
|
||||
font.pixelSize: 12 // Smaller font size
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "darkgrey"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Country"
|
||||
font.bold: true
|
||||
font.pixelSize: 12 // Smaller font size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The TableView for the rows
|
||||
TableView {
|
||||
id: tableView
|
||||
x: 0
|
||||
y: tableHeader.y + tableHeader.height + 10
|
||||
width: parent.width
|
||||
height: parent.height - (tableHeader.y + tableHeader.height + 30)
|
||||
model: logModel
|
||||
|
||||
delegate: Rectangle {
|
||||
width: tableView.width
|
||||
implicitWidth: tableView.width
|
||||
implicitHeight: 100 // Adjust the height to accommodate the checkbox and time text
|
||||
height: implicitHeight
|
||||
color: checkBox.checked ? "#b9fbd7" : (index % 2 === 0 ? "lightgrey" : "white") // Changes color when checked
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40 // Set a fixed height for the row
|
||||
|
||||
Rectangle {
|
||||
width: parent.width / 8
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: serialNumber // Using direct access to model properties
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: callsign // Using direct access to model properties
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: dmrID // Using direct access to model properties
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: tgid // Using direct access to model properties
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: fname // Using direct access to model properties
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width / 6
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: country // Using direct access to model properties
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width - 20
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 10
|
||||
y: 40
|
||||
|
||||
CheckBox {
|
||||
id: checkBox
|
||||
checked: model.checked !== undefined ? model.checked : false
|
||||
onCheckedChanged: {
|
||||
model.checked = checked;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: model.currentTime
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: 12
|
||||
width: parent.width - checkBox.width - 50
|
||||
anchors.verticalCenter: checkBox.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
|
||||
Text {
|
||||
id: menuIcon
|
||||
text: "\uf0c9" // FontAwesome icon for bars (hamburger menu)
|
||||
font.family: "FontAwesome"
|
||||
font.pixelSize: 20
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "black"
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if (contextMenu.visible) {
|
||||
contextMenu.close();
|
||||
} else {
|
||||
contextMenu.x = menuIcon.x + menuIcon.width
|
||||
contextMenu.y = menuIcon.y
|
||||
contextMenu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: contextMenu
|
||||
title: "Lookup Options"
|
||||
visible: false // Ensure it's not visible by default
|
||||
MenuItem {
|
||||
text: "Lookup QRZ"
|
||||
onTriggered: Qt.openUrlExternally("https://qrz.com/lookup/" + model.callsign)
|
||||
}
|
||||
MenuItem {
|
||||
text: "Lookup BM"
|
||||
onTriggered: Qt.openUrlExternally("https://brandmeister.network/index.php?page=profile&call=" + model.callsign)
|
||||
}
|
||||
MenuItem {
|
||||
text: "Lookup APRS"
|
||||
onTriggered: Qt.openUrlExternally("https://aprs.fi/#!call=a%2F" + model.callsign)
|
||||
}
|
||||
// onClosed: visible = false
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDataUpdated(receivedDmrID, receivedTGID) {
|
||||
console.log("Received dmrID:", receivedDmrID, "and TGID:", receivedTGID);
|
||||
qsoTab.dmrID = receivedDmrID; // Correctly scope the dmrID assignment
|
||||
qsoTab.tgid = receivedTGID; // Set the TGID in qsoTab
|
||||
fetchData(receivedDmrID, receivedTGID);
|
||||
}
|
||||
|
||||
function fetchData(dmrID, tgid) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "https://radioid.net/api/dmr/user/?id=" + dmrID, true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (response.count > 0) {
|
||||
var result = response.results[0];
|
||||
var data = {
|
||||
callsign: result.callsign,
|
||||
dmrID: result.id,
|
||||
tgid: tgid, // Include TGID in the data object
|
||||
country: result.country,
|
||||
fname: result.fname,
|
||||
currentTime: Qt.formatDateTime(new Date(), "yyyy-MM-dd HH:mm:ss") // Current local time
|
||||
};
|
||||
addEntry(data);
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to fetch data. Status:", xhr.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function addEntry(data) {
|
||||
console.log("Processing data in QsoTab:", JSON.stringify(data));
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
console.error("Invalid data received:", data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment the latest serial number
|
||||
latestSerialNumber += 1;
|
||||
|
||||
// Check and update the country field
|
||||
if (data.country === "United States") {
|
||||
data.country = "USA";
|
||||
} else if (data.country === "United Kingdom") {
|
||||
data.country = "UK";
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Insert the new entry with the next serial number
|
||||
logModel.insert(0, {
|
||||
serialNumber: latestSerialNumber,
|
||||
callsign: data.callsign,
|
||||
dmrID: data.dmrID,
|
||||
tgid: data.tgid,
|
||||
country: data.country,
|
||||
fname: data.fname,
|
||||
currentTime: data.currentTime,
|
||||
checked: false
|
||||
});
|
||||
//latestSerialNumber += 1; // Increment for the next entry
|
||||
|
||||
// Save the updated log
|
||||
saveSettings();
|
||||
|
||||
// Ensure that the log doesn't exceed the maximum number of entries
|
||||
const maxEntries = 250;
|
||||
while (logModel.count > maxEntries) {
|
||||
logModel.remove(logModel.count - 1); // Remove the oldest entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
/*
|
||||
Copyright (C) 2019-2021 Doug McLain
|
||||
Modified Copyright (C) 2024 Rohith Namboothiri
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audioengine.h"
|
||||
#include <QDebug>
|
||||
#include <cmath>
|
||||
#include "AudioSessionManager.h"
|
||||
|
||||
#if defined (Q_OS_MACOS) || defined(Q_OS_IOS)
|
||||
#define MACHAK 1
|
||||
#else
|
||||
#define MACHAK 0
|
||||
#endif
|
||||
|
||||
AudioEngine::AudioEngine(QString in, QString out) :
|
||||
m_outputdevice(out),
|
||||
m_inputdevice(in),
|
||||
m_out(nullptr),
|
||||
m_in(nullptr),
|
||||
m_srm(1),
|
||||
m_dataWritten(false)
|
||||
{
|
||||
m_audio_out_temp_buf_p = m_audio_out_temp_buf;
|
||||
memset(m_aout_max_buf, 0, sizeof(float) * 200);
|
||||
m_aout_max_buf_p = m_aout_max_buf;
|
||||
m_aout_max_buf_idx = 0;
|
||||
m_aout_gain = 100;
|
||||
m_volume = 1.0f;
|
||||
}
|
||||
|
||||
AudioEngine::~AudioEngine()
|
||||
{
|
||||
}
|
||||
|
||||
QStringList AudioEngine::discover_audio_devices(uint8_t d)
|
||||
{
|
||||
QStringList list;
|
||||
QList<QAudioDevice> devices;
|
||||
|
||||
if(d){
|
||||
devices = QMediaDevices::audioOutputs();
|
||||
}
|
||||
else{
|
||||
devices = QMediaDevices::audioInputs();
|
||||
}
|
||||
|
||||
for (QList<QAudioDevice>::ConstIterator it = devices.constBegin(); it != devices.constEnd(); ++it ) {
|
||||
//fprintf(stderr, "Playback device name = %s\n", (*it).deviceName().toStdString().c_str());fflush(stderr);
|
||||
list.append((*it).description());
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
void AudioEngine::init()
|
||||
{
|
||||
QAudioFormat format;
|
||||
format.setSampleRate(8000);
|
||||
format.setChannelCount(1);
|
||||
format.setSampleFormat(QAudioFormat::Int16);
|
||||
|
||||
m_agc = true;
|
||||
|
||||
QList<QAudioDevice> devices = QMediaDevices::audioOutputs();
|
||||
if(devices.size() == 0){
|
||||
qDebug() << "No audio playback hardware found";
|
||||
}
|
||||
else{
|
||||
QAudioDevice device(QMediaDevices::defaultAudioOutput());
|
||||
for (QList<QAudioDevice>::ConstIterator it = devices.constBegin(); it != devices.constEnd(); ++it ) {
|
||||
|
||||
qDebug() << "Playback device name = " << (*it).description();
|
||||
qDebug() << (*it).supportedSampleFormats();
|
||||
qDebug() << (*it).preferredFormat();
|
||||
|
||||
if((*it).description() == m_outputdevice){
|
||||
device = *it;
|
||||
}
|
||||
}
|
||||
if (!device.isFormatSupported(format)) {
|
||||
qWarning() << "Raw audio format not supported by playback device";
|
||||
}
|
||||
|
||||
qDebug() << "Playback device: " << device.description() << "SR: " << format.sampleRate();
|
||||
|
||||
m_out = new QAudioSink(device, format, this);
|
||||
m_out->setBufferSize(1280);
|
||||
connect(m_out, SIGNAL(stateChanged(QAudio::State)), this, SLOT(handleStateChanged(QAudio::State)));
|
||||
}
|
||||
|
||||
devices = QMediaDevices::audioInputs();
|
||||
|
||||
if(devices.size() == 0){
|
||||
qDebug() << "No audio capture hardware found";
|
||||
}
|
||||
else{
|
||||
QAudioDevice device(QMediaDevices::defaultAudioInput());
|
||||
for (QList<QAudioDevice>::ConstIterator it = devices.constBegin(); it != devices.constEnd(); ++it ) {
|
||||
if(MACHAK){
|
||||
qDebug() << "Playback device name = " << (*it).description();
|
||||
qDebug() << (*it).supportedSampleFormats();
|
||||
qDebug() << (*it).preferredFormat();
|
||||
}
|
||||
if((*it).description() == m_inputdevice){
|
||||
device = *it;
|
||||
}
|
||||
}
|
||||
if (!device.isFormatSupported(format)) {
|
||||
qWarning() << "Raw audio format not supported by capture device";
|
||||
}
|
||||
|
||||
int sr = 8000;
|
||||
if(MACHAK){
|
||||
sr = device.preferredFormat().sampleRate();
|
||||
m_srm = (float)sr / 8000.0;
|
||||
}
|
||||
format.setSampleRate(sr);
|
||||
m_in = new QAudioSource(device, format, this);
|
||||
qDebug() << "Capture device: " << device.description() << " SR: " << sr << " resample factor: " << m_srm;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::start_capture()
|
||||
{
|
||||
m_audioinq.clear();
|
||||
// setupAVAudioSession();
|
||||
if(m_in != nullptr){
|
||||
m_indev = m_in->start();
|
||||
if(MACHAK) m_srm = (float)(m_in->format().sampleRate()) / 8000.0;
|
||||
connect(m_indev, SIGNAL(readyRead()), SLOT(input_data_received()));
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::stop_capture()
|
||||
{
|
||||
if(m_in != nullptr){
|
||||
m_indev->disconnect();
|
||||
m_in->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::start_playback()
|
||||
{
|
||||
m_outdev = m_out->start();
|
||||
|
||||
setupAVAudioSession();
|
||||
setupBackgroundAudio();
|
||||
}
|
||||
|
||||
void AudioEngine::stop_playback()
|
||||
{
|
||||
//m_outdev->reset();
|
||||
m_out->reset();
|
||||
m_out->stop();
|
||||
static bool isSessionActive = false;
|
||||
if (isSessionActive) {
|
||||
deactivateAVAudioSession();
|
||||
isSessionActive = false; // Reset session state before reactivation
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::input_data_received()
|
||||
{
|
||||
QByteArray data = m_indev->readAll();
|
||||
|
||||
if (data.size() > 0){
|
||||
/*
|
||||
fprintf(stderr, "AUDIOIN: ");
|
||||
for(int i = 0; i < len; ++i){
|
||||
fprintf(stderr, "%02x ", (uint8_t)data.data()[i]);
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
fflush(stderr);
|
||||
*/
|
||||
if(MACHAK){
|
||||
std::vector<int16_t> samples;
|
||||
for(int i = 0; i < data.size(); i += 2){
|
||||
samples.push_back(((data.data()[i+1] << 8) & 0xff00) | (data.data()[i] & 0xff));
|
||||
}
|
||||
for(float i = 0; i < (float)data.size()/2; i += m_srm){
|
||||
m_audioinq.enqueue(samples[i]);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(int i = 0; i < data.size(); i += (2 * m_srm)){
|
||||
m_audioinq.enqueue(((data.data()[i+1] << 8) & 0xff00) | (data.data()[i] & 0xff));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::write(int16_t *pcm, size_t s)
|
||||
{
|
||||
m_maxlevel = 0;
|
||||
/*
|
||||
fprintf(stderr, "AUDIOOUT: ");
|
||||
for(int i = 0; i < s; ++i){
|
||||
fprintf(stderr, "%04x ", (uint16_t)pcm[i]);
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
fflush(stderr);
|
||||
*/
|
||||
if(m_agc){
|
||||
process_audio(pcm, s);
|
||||
}
|
||||
|
||||
size_t l = m_outdev->write((const char *) pcm, sizeof(int16_t) * s);
|
||||
|
||||
if (l > 0) { // Data was written
|
||||
m_dataWritten = true;
|
||||
}
|
||||
|
||||
if (l*2 < s){
|
||||
qDebug() << "AudioEngine::write() " << s << ":" << l << ":" << (int)m_out->bytesFree() << ":" << m_out->bufferSize() << ":" << m_out->error();
|
||||
}
|
||||
|
||||
for(uint32_t i = 0; i < s; ++i){
|
||||
if(pcm[i] > m_maxlevel){
|
||||
m_maxlevel = pcm[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t AudioEngine::read(int16_t *pcm, int s)
|
||||
{
|
||||
m_maxlevel = 0;
|
||||
|
||||
if(m_audioinq.size() >= s){
|
||||
for(int i = 0; i < s; ++i){
|
||||
pcm[i] = m_audioinq.dequeue();
|
||||
if(pcm[i] > m_maxlevel){
|
||||
m_maxlevel = pcm[i];
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
else if(m_in == nullptr){
|
||||
memset(pcm, 0, sizeof(int16_t) * s);
|
||||
return 1;
|
||||
}
|
||||
else{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t AudioEngine::read(int16_t *pcm)
|
||||
{
|
||||
int s;
|
||||
m_maxlevel = 0;
|
||||
|
||||
if(m_audioinq.size() >= 160){
|
||||
s = 160;
|
||||
}
|
||||
else{
|
||||
s = m_audioinq.size();
|
||||
}
|
||||
|
||||
for(int i = 0; i < s; ++i){
|
||||
pcm[i] = m_audioinq.dequeue();
|
||||
if(pcm[i] > m_maxlevel){
|
||||
m_maxlevel = pcm[i];
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// process_audio() based on code from DSD https://github.com/szechyjs/dsd
|
||||
void AudioEngine::process_audio(int16_t *pcm, size_t s)
|
||||
{
|
||||
float aout_abs, max, gainfactor, gaindelta, maxbuf;
|
||||
|
||||
for(size_t i = 0; i < s; ++i){
|
||||
m_audio_out_temp_buf[i] = static_cast<float>(pcm[i]);
|
||||
}
|
||||
|
||||
// detect max level
|
||||
max = 0;
|
||||
m_audio_out_temp_buf_p = m_audio_out_temp_buf;
|
||||
|
||||
for (size_t i = 0; i < s; i++){
|
||||
aout_abs = fabsf(*m_audio_out_temp_buf_p);
|
||||
|
||||
if (aout_abs > max){
|
||||
max = aout_abs;
|
||||
}
|
||||
|
||||
m_audio_out_temp_buf_p++;
|
||||
}
|
||||
|
||||
*m_aout_max_buf_p = max;
|
||||
m_aout_max_buf_p++;
|
||||
m_aout_max_buf_idx++;
|
||||
|
||||
if (m_aout_max_buf_idx > 24){
|
||||
m_aout_max_buf_idx = 0;
|
||||
m_aout_max_buf_p = m_aout_max_buf;
|
||||
}
|
||||
|
||||
// lookup max history
|
||||
for (size_t i = 0; i < 25; i++){
|
||||
maxbuf = m_aout_max_buf[i];
|
||||
|
||||
if (maxbuf > max){
|
||||
max = maxbuf;
|
||||
}
|
||||
}
|
||||
|
||||
// determine optimal gain level
|
||||
if (max > static_cast<float>(0)){
|
||||
gainfactor = (static_cast<float>(30000) / max);
|
||||
}
|
||||
else{
|
||||
gainfactor = static_cast<float>(50);
|
||||
}
|
||||
|
||||
if (gainfactor < m_aout_gain){
|
||||
m_aout_gain = gainfactor;
|
||||
gaindelta = static_cast<float>(0);
|
||||
}
|
||||
else{
|
||||
if (gainfactor > static_cast<float>(50)){
|
||||
gainfactor = static_cast<float>(50);
|
||||
}
|
||||
|
||||
gaindelta = gainfactor - m_aout_gain;
|
||||
|
||||
if (gaindelta > (static_cast<float>(0.05) * m_aout_gain)){
|
||||
gaindelta = (static_cast<float>(0.05) * m_aout_gain);
|
||||
}
|
||||
}
|
||||
|
||||
gaindelta /= static_cast<float>(s); //160
|
||||
|
||||
// adjust output gain
|
||||
m_audio_out_temp_buf_p = m_audio_out_temp_buf;
|
||||
|
||||
for (size_t i = 0; i < s; i++){
|
||||
*m_audio_out_temp_buf_p = (m_aout_gain + (static_cast<float>(i) * gaindelta)) * (*m_audio_out_temp_buf_p);
|
||||
m_audio_out_temp_buf_p++;
|
||||
}
|
||||
|
||||
m_aout_gain += (static_cast<float>(s) * gaindelta);
|
||||
m_audio_out_temp_buf_p = m_audio_out_temp_buf;
|
||||
|
||||
for (size_t i = 0; i < s; i++){
|
||||
*m_audio_out_temp_buf_p *= m_volume;
|
||||
if (*m_audio_out_temp_buf_p > static_cast<float>(32760)){
|
||||
*m_audio_out_temp_buf_p = static_cast<float>(32760);
|
||||
}
|
||||
else if (*m_audio_out_temp_buf_p < static_cast<float>(-32760)){
|
||||
*m_audio_out_temp_buf_p = static_cast<float>(-32760);
|
||||
}
|
||||
pcm[i] = static_cast<int16_t>(*m_audio_out_temp_buf_p);
|
||||
m_audio_out_temp_buf_p++;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::handleStateChanged(QAudio::State newState)
|
||||
{
|
||||
switch (newState) {
|
||||
case QAudio::ActiveState:
|
||||
qDebug() << "AudioOut state active";
|
||||
setupAVAudioSession();
|
||||
|
||||
|
||||
break;
|
||||
case QAudio::SuspendedState:
|
||||
qDebug() << "AudioOut state suspended";
|
||||
//setupBackgroundAudio();
|
||||
|
||||
break;
|
||||
case QAudio::IdleState:
|
||||
qDebug() << "AudioOut state idle";
|
||||
setupBackgroundAudio();
|
||||
|
||||
break;
|
||||
case QAudio::StoppedState:
|
||||
qDebug() << "AudioOut state stopped";
|
||||
|
||||
break;
|
||||
default:
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
Copyright (C) 2019-2021 Doug McLain
|
||||
Modified Copyright (C) 2024 Rohith Namboothiri
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef AUDIOENGINE_H
|
||||
#define AUDIOENGINE_H
|
||||
|
||||
#include <QObject>
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
|
||||
#include <QAudio>
|
||||
#include <QAudioFormat>
|
||||
#include <QAudioInput>
|
||||
#else
|
||||
#include <QAudioDevice>
|
||||
#include <QAudioSink>
|
||||
#include <QAudioSource>
|
||||
#include <QMediaDevices>
|
||||
#endif
|
||||
#include <QAudioOutput>
|
||||
#include <QQueue>
|
||||
|
||||
#define AUDIO_OUT 1
|
||||
#define AUDIO_IN 0
|
||||
|
||||
class AudioEngine : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
//explicit AudioEngine(QObject *parent = nullptr);
|
||||
AudioEngine(QString in, QString out);
|
||||
~AudioEngine();
|
||||
static QStringList discover_audio_devices(uint8_t d);
|
||||
void init();
|
||||
void start_capture();
|
||||
void stop_capture();
|
||||
void start_playback();
|
||||
void stop_playback();
|
||||
void write(int16_t *, size_t);
|
||||
void set_output_buffer_size(uint32_t b) { m_out->setBufferSize(b); }
|
||||
void set_input_buffer_size(uint32_t b) { if(m_in != nullptr) m_in->setBufferSize(b); }
|
||||
void set_output_volume(qreal v){ m_out->setVolume(v); }
|
||||
void set_input_volume(qreal v){ if(m_in != nullptr) m_in->setVolume(v); }
|
||||
void set_agc(bool agc) { m_agc = agc; }
|
||||
bool frame_available() { return (m_audioinq.size() >= 320) ? true : false; }
|
||||
uint16_t read(int16_t *, int);
|
||||
uint16_t read(int16_t *);
|
||||
uint16_t level() { return m_maxlevel; }
|
||||
bool shouldResumePlayback();
|
||||
bool shouldRestartPlayback();
|
||||
bool m_dataWritten;
|
||||
signals:
|
||||
|
||||
private:
|
||||
QString m_outputdevice;
|
||||
QString m_inputdevice;
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
|
||||
QAudioOutput *m_out;
|
||||
QAudioInput *m_in;
|
||||
#else
|
||||
QAudioSink *m_out;
|
||||
QAudioSource *m_in;
|
||||
#endif
|
||||
QIODevice *m_outdev;
|
||||
QIODevice *m_indev;
|
||||
QQueue<int16_t> m_audioinq;
|
||||
uint16_t m_maxlevel;
|
||||
bool m_agc;
|
||||
float m_srm; // sample rate multiplier for macOS HACK
|
||||
|
||||
float m_audio_out_temp_buf[320]; //!< output of decoder
|
||||
float *m_audio_out_temp_buf_p;
|
||||
|
||||
//float m_audio_out_float_buf[1120]; //!< output of upsampler - 1 frame of 160 samples upampled up to 7 times
|
||||
//float *m_audio_out_float_buf_p;
|
||||
|
||||
float m_aout_max_buf[200];
|
||||
float *m_aout_max_buf_p;
|
||||
int m_aout_max_buf_idx;
|
||||
|
||||
//short m_audio_out_buf[2*48000]; //!< final result - 1s of L+R S16LE samples
|
||||
//short *m_audio_out_buf_p;
|
||||
//int m_audio_out_nb_samples;
|
||||
//int m_audio_out_buf_size;
|
||||
//int m_audio_out_idx;
|
||||
//int m_audio_out_idx2;
|
||||
|
||||
float m_aout_gain;
|
||||
float m_volume;
|
||||
|
||||
private slots:
|
||||
void input_data_received();
|
||||
void process_audio(int16_t *pcm, size_t s);
|
||||
void handleStateChanged(QAudio::State newState);
|
||||
};
|
||||
|
||||
#endif // AUDIOENGINE_H
|
||||
Loading…
Reference in New Issue