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