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