From 5b75e89df346365dd98522c41841ac2d7392e194 Mon Sep 17 00:00:00 2001
From: rohithzmoi <166651631+rohithzmoi@users.noreply.github.com>
Date: Tue, 3 Sep 2024 02:15:09 +0530
Subject: [PATCH] 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>
---
iOS_OnlyFIles/AudioSessionManager.mm | 177 ++++++++
iOS_OnlyFIles/DroidStar.pro | 242 +++++++++++
iOS_OnlyFIles/LogHandler.cpp | 203 +++++++++
iOS_OnlyFIles/LogHandler.h | 30 ++
iOS_OnlyFIles/QsoTab.qml | 591 +++++++++++++++++++++++++++
iOS_OnlyFIles/audioengine.cpp | 401 ++++++++++++++++++
iOS_OnlyFIles/audioengine.h | 109 +++++
7 files changed, 1753 insertions(+)
create mode 100644 iOS_OnlyFIles/AudioSessionManager.mm
create mode 100644 iOS_OnlyFIles/DroidStar.pro
create mode 100644 iOS_OnlyFIles/LogHandler.cpp
create mode 100644 iOS_OnlyFIles/LogHandler.h
create mode 100644 iOS_OnlyFIles/QsoTab.qml
create mode 100644 iOS_OnlyFIles/audioengine.cpp
create mode 100644 iOS_OnlyFIles/audioengine.h
diff --git a/iOS_OnlyFIles/AudioSessionManager.mm b/iOS_OnlyFIles/AudioSessionManager.mm
new file mode 100644
index 0000000..9fc0b1d
--- /dev/null
+++ b/iOS_OnlyFIles/AudioSessionManager.mm
@@ -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 .
+*/
+
+#import
+#import
+
+
+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);
+ }
+}
diff --git a/iOS_OnlyFIles/DroidStar.pro b/iOS_OnlyFIles/DroidStar.pro
new file mode 100644
index 0000000..06404ea
--- /dev/null
+++ b/iOS_OnlyFIles/DroidStar.pro
@@ -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 += "UIBackgroundModes"
+ios:QMAKE_INFO_PLIST_EXTRA += ""
+ios:QMAKE_INFO_PLIST_EXTRA += " audio"
+ios:QMAKE_INFO_PLIST_EXTRA += ""
+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
+}
diff --git a/iOS_OnlyFIles/LogHandler.cpp b/iOS_OnlyFIles/LogHandler.cpp
new file mode 100644
index 0000000..2cda642
--- /dev/null
+++ b/iOS_OnlyFIles/LogHandler.cpp
@@ -0,0 +1,203 @@
+#include "LogHandler.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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 << "\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 << "" << entry["callsign"].toString();
+ out << "70CM"; // Band is hardcoded as "70CM"
+ out << "DIGITALVOICE"; // Mode is set to "DIGITALVOICE"
+ // Include the first name in the ADIF record
+ out << "" << entry["fname"].toString();
+
+ out << "" << qsoDate;
+ out << "" << timeOn;
+ out << "\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/"));
+}
diff --git a/iOS_OnlyFIles/LogHandler.h b/iOS_OnlyFIles/LogHandler.h
new file mode 100644
index 0000000..5045769
--- /dev/null
+++ b/iOS_OnlyFIles/LogHandler.h
@@ -0,0 +1,30 @@
+#ifndef LOGHANDLER_H
+#define LOGHANDLER_H
+
+#include
+#include
+
+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
diff --git a/iOS_OnlyFIles/QsoTab.qml b/iOS_OnlyFIles/QsoTab.qml
new file mode 100644
index 0000000..9569294
--- /dev/null
+++ b/iOS_OnlyFIles/QsoTab.qml
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/iOS_OnlyFIles/audioengine.cpp b/iOS_OnlyFIles/audioengine.cpp
new file mode 100644
index 0000000..addbc16
--- /dev/null
+++ b/iOS_OnlyFIles/audioengine.cpp
@@ -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 .
+*/
+
+#include "audioengine.h"
+#include
+#include
+#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 devices;
+
+ if(d){
+ devices = QMediaDevices::audioOutputs();
+ }
+ else{
+ devices = QMediaDevices::audioInputs();
+ }
+
+ for (QList::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 devices = QMediaDevices::audioOutputs();
+ if(devices.size() == 0){
+ qDebug() << "No audio playback hardware found";
+ }
+ else{
+ QAudioDevice device(QMediaDevices::defaultAudioOutput());
+ for (QList::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::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 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(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(0)){
+ gainfactor = (static_cast(30000) / max);
+ }
+ else{
+ gainfactor = static_cast(50);
+ }
+
+ if (gainfactor < m_aout_gain){
+ m_aout_gain = gainfactor;
+ gaindelta = static_cast(0);
+ }
+ else{
+ if (gainfactor > static_cast(50)){
+ gainfactor = static_cast(50);
+ }
+
+ gaindelta = gainfactor - m_aout_gain;
+
+ if (gaindelta > (static_cast(0.05) * m_aout_gain)){
+ gaindelta = (static_cast(0.05) * m_aout_gain);
+ }
+ }
+
+ gaindelta /= static_cast(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(i) * gaindelta)) * (*m_audio_out_temp_buf_p);
+ m_audio_out_temp_buf_p++;
+ }
+
+ m_aout_gain += (static_cast(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(32760)){
+ *m_audio_out_temp_buf_p = static_cast(32760);
+ }
+ else if (*m_audio_out_temp_buf_p < static_cast(-32760)){
+ *m_audio_out_temp_buf_p = static_cast(-32760);
+ }
+ pcm[i] = static_cast(*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;
+ }
+}
+
diff --git a/iOS_OnlyFIles/audioengine.h b/iOS_OnlyFIles/audioengine.h
new file mode 100644
index 0000000..7796138
--- /dev/null
+++ b/iOS_OnlyFIles/audioengine.h
@@ -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 .
+*/
+#ifndef AUDIOENGINE_H
+#define AUDIOENGINE_H
+
+#include
+#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
+#include
+#include
+#include
+#else
+#include
+#include
+#include
+#include
+#endif
+#include
+#include
+
+#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 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