diff --git a/.gitattributes b/.gitattributes index 5bed1c4..d32c377 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ include/ linguist-vendored ffmpeg/ linguist-vendored *.h linguist-language=c++ +* text=auto diff --git a/src/About.cpp b/src/About.cpp index 27c569a..23d83d5 100644 --- a/src/About.cpp +++ b/src/About.cpp @@ -1,34 +1,34 @@ -// src/About.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#include "About.h" -#include "buildinfo.h" - -// Import from ffmpeg -extern "C" const char *av_version_info(); - - -AboutQt::AboutQt(QWidget *parent) : - QWidget(parent, Qt::Window | Qt::WindowTitleHint /*| Qt::CustomizeWindowHint*/ | Qt::WindowCloseButtonHint), - ui(new Ui::AboutQt) -{ - const char *ffmpeg_version = av_version_info(); - ui->setupUi(this); - ui->l_version->setText(QString(buildinfo_getPluginVersion()) + - "\nBuild on " + buildinfo_getBuildDate() + " " + buildinfo_getBuildTime() + - "\nFFmpeg Version: " + (ffmpeg_version ? ffmpeg_version : "unknown") + - "\nLinked against Qt " QT_VERSION_STR); - setFixedSize(size()); -} - - -AboutQt::~AboutQt() -{ - delete ui; -} +// src/About.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#include "About.h" +#include "buildinfo.h" + +// Import from ffmpeg +extern "C" const char *av_version_info(); + + +AboutQt::AboutQt(QWidget *parent) : + QWidget(parent, Qt::Window | Qt::WindowTitleHint /*| Qt::CustomizeWindowHint*/ | Qt::WindowCloseButtonHint), + ui(new Ui::AboutQt) +{ + const char *ffmpeg_version = av_version_info(); + ui->setupUi(this); + ui->l_version->setText(QString(buildinfo_getPluginVersion()) + + "\nBuild on " + buildinfo_getBuildDate() + " " + buildinfo_getBuildTime() + + "\nFFmpeg Version: " + (ffmpeg_version ? ffmpeg_version : "unknown") + + "\nLinked against Qt " QT_VERSION_STR); + setFixedSize(size()); +} + + +AboutQt::~AboutQt() +{ + delete ui; +} diff --git a/src/About.h b/src/About.h index 80c84e7..65c1389 100644 --- a/src/About.h +++ b/src/About.h @@ -1,31 +1,31 @@ -// src/About.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include - -#include "ui_About.h" - -namespace Ui { - class AboutQt; -} - - -class AboutQt : public QWidget -{ - Q_OBJECT - -public: - explicit AboutQt(QWidget *parent = 0); - ~AboutQt(); - -private: - Ui::AboutQt *ui; -}; - +// src/About.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include + +#include "ui_About.h" + +namespace Ui { + class AboutQt; +} + + +class AboutQt : public QWidget +{ + Q_OBJECT + +public: + explicit AboutQt(QWidget *parent = 0); + ~AboutQt(); + +private: + Ui::AboutQt *ui; +}; + diff --git a/src/ConfigModel.cpp b/src/ConfigModel.cpp index 9315551..cd8ceb9 100644 --- a/src/ConfigModel.cpp +++ b/src/ConfigModel.cpp @@ -1,392 +1,392 @@ -// src/ConfigModel.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#include "common.h" - -#include - -#include "ConfigModel.h" -#include "main.h" -#include "buildinfo.h" -#include "plugin.h" - - - -ConfigModel::ConfigModel() -{ - m_rows.fill(2); - m_cols.fill(5); - m_volumeLocal = 80; - m_volumeRemote = 80; - m_playbackLocal = true; - m_muteMyselfDuringPb = false; - m_windowWidth = 600; - m_windowHeight = 240; - - m_bubbleButtonsBuild = 0; - m_bubbleStopBuild = 0; - m_bubbleColsBuild = 0; - - m_showHotkeysOnButtons = false; - m_hotkeysEnabled = true; - - m_activeConfig = 0; - m_nextUpdateCheck = 0; -} - - -void ConfigModel::readConfig(const QString &file) -{ - QString path; - if (file.isEmpty()) - path = GetFullConfigPath(); - else - path = file; - - QSettings settings(path, QSettings::IniFormat); - - for (int i = 0; i < NUM_CONFIGS; i++) - m_sounds[i] = readConfiguration(settings, i == 0 ? QString("files") : QString("files%1").arg(i+1)); - - for (int i = 0; i < NUM_CONFIGS; i++) - { - m_rows[i] = settings.value(i == 0 ? QString("num_rows") : QString("num_rows%1").arg(i + 1), i == 0 ? 2 : m_rows[0]).toInt(); - m_cols[i] = settings.value(i == 0 ? QString("num_cols") : QString("num_cols%1").arg(i + 1), i == 0 ? 5 : m_cols[0]).toInt(); - } - int volume_old = settings.value("volume", 50).toInt(); - m_volumeLocal = settings.value("volumeLocal", volume_old).toInt(); - m_volumeRemote = settings.value("volumeRemote", volume_old).toInt(); - m_playbackLocal = settings.value("playback_local", true).toBool(); - m_muteMyselfDuringPb = settings.value("mute_myself_during_pb", false).toBool(); - m_windowWidth = settings.value("window_width", 600).toInt(); - m_windowHeight = settings.value("window_height", 240).toInt(); - m_bubbleButtonsBuild = settings.value("bubble_buttons_build", 0).toInt(); - m_bubbleStopBuild = settings.value("bubble_stop_build", 0).toInt(); - m_bubbleColsBuild = settings.value("bubble_cols_build", 0).toInt(); - m_showHotkeysOnButtons = settings.value("show_hotkeys_on_buttons", false).toBool(); - m_hotkeysEnabled = settings.value("hotkeys_enabled", true).toBool(); - m_nextUpdateCheck = settings.value("next_update_check", 0).toUInt(); - - notifyAllEvents(); -} - -void ConfigModel::writeConfig(const QString &file) -{ - QString path; - if (file.isEmpty()) - path = GetFullConfigPath(); - else - path = file; - - QSettings settings(path, QSettings::IniFormat); - - settings.setValue("config_build", buildinfo_getBuildNumber()); - settings.setValue("volumeLocal", m_volumeLocal); - settings.setValue("volumeRemote", m_volumeRemote); - settings.setValue("playback_local", m_playbackLocal); - settings.setValue("mute_myself_during_pb", m_muteMyselfDuringPb); - settings.setValue("window_width", m_windowWidth); - settings.setValue("window_height", m_windowHeight); - settings.setValue("bubble_buttons_build", m_bubbleButtonsBuild); - settings.setValue("bubble_stop_build", m_bubbleStopBuild); - settings.setValue("bubble_cols_build", m_bubbleColsBuild); - settings.setValue("show_hotkeys_on_buttons", m_showHotkeysOnButtons); - settings.setValue("hotkeys_enabled", m_hotkeysEnabled); - settings.setValue("next_update_check", m_nextUpdateCheck); - - for (int i = 0; i < NUM_CONFIGS; i++) - writeConfiguration(settings, i == 0 ? QString("files") : QString("files%1").arg(i + 1), m_sounds[i]); - for (int i = 0; i < NUM_CONFIGS; i++) - { - settings.setValue(i == 0 ? QString("num_rows") : QString("num_rows%1").arg(i + 1), m_rows[i]); - settings.setValue(i == 0 ? QString("num_cols") : QString("num_cols%1").arg(i + 1), m_cols[i]); - } -} - - -std::vector ConfigModel::readConfiguration(QSettings &settings, const QString &name) -{ - std::vector sounds; - int size = settings.beginReadArray(name); - if (size == 0) - sounds = getInitialSounds(); - else - { - sounds.resize(size); - for (int i = 0; i < size; i++) - { - settings.setArrayIndex(i); - sounds[i].readFromConfig(settings); - } - } - settings.endArray(); - return sounds; -} - - -void ConfigModel::writeConfiguration(QSettings & settings, const QString &name, const std::vector &sounds) -{ - settings.beginWriteArray(name); - for (int i = 0; i < (int)sounds.size(); i++) - { - settings.setArrayIndex(i); - sounds[i].saveToConfig(settings); - } - settings.endArray(); -} - - -void ConfigModel::setConfiguration(int config) -{ - m_activeConfig = config; - - /* Tell observers that our data changed */ - notifyAllEvents(); -} - -int ConfigModel::getConfiguration() -{ - return m_activeConfig; -} - -QString ConfigModel::getFileName( int itemId ) const -{ - if(itemId >= 0 && itemId < numSounds()) - return sounds()[itemId].filename; - return QString(); -} - - -void ConfigModel::setFileName( int itemId, const QString &fn ) -{ - if(itemId >= 0) - { - if(itemId < 1000 && itemId >= numSounds()) - sounds().resize(itemId + 1); - sounds()[itemId].filename = fn; - writeConfig(); - notify(NOTIFY_SET_SOUND, itemId); - } -} - - -const SoundInfo *ConfigModel::getSoundInfo(int itemId) const -{ - if(itemId >= 0 && itemId < numSounds()) - return &sounds()[itemId]; - return nullptr; -} - - -void ConfigModel::setSoundInfo( int itemId, const SoundInfo &info ) -{ - if(itemId < 1000 && itemId >= numSounds()) - sounds().resize(itemId + 1); - sounds()[itemId] = info; - writeConfig(); - notify(NOTIFY_SET_SOUND, itemId); -} - - -QString ConfigModel::GetConfigPath() -{ - // Find config path for config class - char configPath[PATH_BUFSIZE]; - ts3Functions.getConfigPath(configPath, PATH_BUFSIZE); - return QString::fromUtf8(configPath); -} - - -QString ConfigModel::GetFullConfigPath() -{ - QString fullPath = GetConfigPath(); - QChar last = fullPath[fullPath.count() - 1]; - if (last != '/' && last != '\\') - fullPath.append('/'); - fullPath.append("rp_soundboard.ini"); - return fullPath; -} - - -void ConfigModel::setRows( int n ) -{ - m_rows[m_activeConfig] = n; - writeConfig(); - notify(NOTIFY_SET_ROWS, n); -} - - -void ConfigModel::setCols( int n ) -{ - m_cols[m_activeConfig] = n; - writeConfig(); - notify(NOTIFY_SET_COLS, n); -} - - -void ConfigModel::setVolumeLocal( int val ) -{ - m_volumeLocal = val; - notify(NOTIFY_SET_VOLUME_LOCAL, val); -} - -void ConfigModel::setVolumeRemote( int val ) -{ - m_volumeRemote = val; - notify(NOTIFY_SET_VOLUME_REMOTE, val); -} - -void ConfigModel::setPlaybackLocal( bool val ) -{ - m_playbackLocal = val; - writeConfig(); - notify(NOTIFY_SET_PLAYBACK_LOCAL, val ? 1 : 0); -} - - -void ConfigModel::setMuteMyselfDuringPb(bool val) -{ - m_muteMyselfDuringPb = val; - writeConfig(); - notify(NOTIFY_SET_MUTE_MYSELF_DURING_PB, val ? 1 : 0); -} - - -void ConfigModel::getWindowSize(int *width, int *height) const -{ - if(width) - *width = m_windowWidth; - if(height) - *height = m_windowHeight; -} - - -void ConfigModel::setWindowSize(int width, int height) -{ - m_windowWidth = width; - m_windowHeight = height; - notify(NOTIFY_SET_WINDOW_SIZE, 0); -} - - -void ConfigModel::setNextUpdateCheck(uint time) -{ - m_nextUpdateCheck = time; - notify(NOTIFY_SET_NEXT_UPDATE_CHECK, (int)time); -} - - -void ConfigModel::notify(notifications_e what, int data) -{ - for(Observer *obs : m_obs) - obs->notify(*this, what, data); -} - - -void ConfigModel::setHotkeysEnabled(bool enabled) -{ - m_hotkeysEnabled = enabled; - writeConfig(); - notify(NOTIFY_SET_HOTKEYS_ENABLED, enabled ? 1 : 0); -} - - -void ConfigModel::addObserver(Observer *obs) -{ - m_obs.push_back(obs); -} - - -void ConfigModel::remObserver(Observer *obs) -{ - m_obs.erase(std::remove(m_obs.begin(), m_obs.end(), obs), m_obs.end()); -} - - -void ConfigModel::setBubbleButtonsBuild(int build) -{ - m_bubbleButtonsBuild = build; - writeConfig(); - notify(NOTIFY_SET_BUBBLE_BUTTONS_BUILD, build); -} - - -void ConfigModel::setBubbleStopBuild(int build) -{ - m_bubbleStopBuild = build; - writeConfig(); - notify(NOTIFY_SET_BUBBLE_STOP_BUILD, build); -} - - -void ConfigModel::setBubbleColsBuild(int build) -{ - m_bubbleColsBuild = build; - writeConfig(); - notify(NOTIFY_SET_BUBBLE_COLS_BUILD, build); -} - - -std::vector ConfigModel::getInitialSounds() -{ - char pluginPath[PATH_BUFSIZE]; - ts3Functions.getPluginPath(pluginPath, PATH_BUFSIZE, getPluginID()); - QString fullPath = QString::fromUtf8(pluginPath); - QChar last = fullPath[fullPath.count() - 1]; - if (last != '/' && last != '\\') - fullPath.append('/'); - fullPath.append("rp_soundboard/"); - - static const char* files[] = { - "Airhorn Sonata.mp3", - "Airhorn.mp3", - "Airporn.mp3", - "Peter Griffin Laugh.mp3", - "Spooky.mp3", - nullptr, - }; - - std::vector sounds; - for (int i = 0; files[i] != nullptr; i++) - { - SoundInfo info; - info.filename = fullPath + files[i]; - sounds.push_back(info); - } - return sounds; -} - - -void ConfigModel::notifyAllEvents() -{ - //Notify all changes - for(int i = 0; i < numSounds(); i++) - notify(NOTIFY_SET_SOUND, i); - notify(NOTIFY_SET_COLS, getCols()); - notify(NOTIFY_SET_ROWS, getRows()); - notify(NOTIFY_SET_VOLUME_LOCAL, m_volumeLocal); - notify(NOTIFY_SET_VOLUME_REMOTE, m_volumeRemote); - notify(NOTIFY_SET_PLAYBACK_LOCAL, m_playbackLocal); - notify(NOTIFY_SET_MUTE_MYSELF_DURING_PB, m_muteMyselfDuringPb); - notify(NOTIFY_SET_WINDOW_SIZE, 0); - notify(NOTIFY_SET_BUBBLE_BUTTONS_BUILD, m_bubbleButtonsBuild); - notify(NOTIFY_SET_BUBBLE_STOP_BUILD, m_bubbleStopBuild); - notify(NOTIFY_SET_BUBBLE_COLS_BUILD, m_bubbleColsBuild); - notify(NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS, m_showHotkeysOnButtons); - notify(NOTIFY_SET_HOTKEYS_ENABLED, m_hotkeysEnabled); -} - - -void ConfigModel::setShowHotkeysOnButtons(bool show) -{ - m_showHotkeysOnButtons = show; - writeConfig(); - notify(NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS, show ? 1 : 0); -} - +// src/ConfigModel.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#include "common.h" + +#include + +#include "ConfigModel.h" +#include "main.h" +#include "buildinfo.h" +#include "plugin.h" + + + +ConfigModel::ConfigModel() +{ + m_rows.fill(2); + m_cols.fill(5); + m_volumeLocal = 80; + m_volumeRemote = 80; + m_playbackLocal = true; + m_muteMyselfDuringPb = false; + m_windowWidth = 600; + m_windowHeight = 240; + + m_bubbleButtonsBuild = 0; + m_bubbleStopBuild = 0; + m_bubbleColsBuild = 0; + + m_showHotkeysOnButtons = false; + m_hotkeysEnabled = true; + + m_activeConfig = 0; + m_nextUpdateCheck = 0; +} + + +void ConfigModel::readConfig(const QString &file) +{ + QString path; + if (file.isEmpty()) + path = GetFullConfigPath(); + else + path = file; + + QSettings settings(path, QSettings::IniFormat); + + for (int i = 0; i < NUM_CONFIGS; i++) + m_sounds[i] = readConfiguration(settings, i == 0 ? QString("files") : QString("files%1").arg(i+1)); + + for (int i = 0; i < NUM_CONFIGS; i++) + { + m_rows[i] = settings.value(i == 0 ? QString("num_rows") : QString("num_rows%1").arg(i + 1), i == 0 ? 2 : m_rows[0]).toInt(); + m_cols[i] = settings.value(i == 0 ? QString("num_cols") : QString("num_cols%1").arg(i + 1), i == 0 ? 5 : m_cols[0]).toInt(); + } + int volume_old = settings.value("volume", 50).toInt(); + m_volumeLocal = settings.value("volumeLocal", volume_old).toInt(); + m_volumeRemote = settings.value("volumeRemote", volume_old).toInt(); + m_playbackLocal = settings.value("playback_local", true).toBool(); + m_muteMyselfDuringPb = settings.value("mute_myself_during_pb", false).toBool(); + m_windowWidth = settings.value("window_width", 600).toInt(); + m_windowHeight = settings.value("window_height", 240).toInt(); + m_bubbleButtonsBuild = settings.value("bubble_buttons_build", 0).toInt(); + m_bubbleStopBuild = settings.value("bubble_stop_build", 0).toInt(); + m_bubbleColsBuild = settings.value("bubble_cols_build", 0).toInt(); + m_showHotkeysOnButtons = settings.value("show_hotkeys_on_buttons", false).toBool(); + m_hotkeysEnabled = settings.value("hotkeys_enabled", true).toBool(); + m_nextUpdateCheck = settings.value("next_update_check", 0).toUInt(); + + notifyAllEvents(); +} + +void ConfigModel::writeConfig(const QString &file) +{ + QString path; + if (file.isEmpty()) + path = GetFullConfigPath(); + else + path = file; + + QSettings settings(path, QSettings::IniFormat); + + settings.setValue("config_build", buildinfo_getBuildNumber()); + settings.setValue("volumeLocal", m_volumeLocal); + settings.setValue("volumeRemote", m_volumeRemote); + settings.setValue("playback_local", m_playbackLocal); + settings.setValue("mute_myself_during_pb", m_muteMyselfDuringPb); + settings.setValue("window_width", m_windowWidth); + settings.setValue("window_height", m_windowHeight); + settings.setValue("bubble_buttons_build", m_bubbleButtonsBuild); + settings.setValue("bubble_stop_build", m_bubbleStopBuild); + settings.setValue("bubble_cols_build", m_bubbleColsBuild); + settings.setValue("show_hotkeys_on_buttons", m_showHotkeysOnButtons); + settings.setValue("hotkeys_enabled", m_hotkeysEnabled); + settings.setValue("next_update_check", m_nextUpdateCheck); + + for (int i = 0; i < NUM_CONFIGS; i++) + writeConfiguration(settings, i == 0 ? QString("files") : QString("files%1").arg(i + 1), m_sounds[i]); + for (int i = 0; i < NUM_CONFIGS; i++) + { + settings.setValue(i == 0 ? QString("num_rows") : QString("num_rows%1").arg(i + 1), m_rows[i]); + settings.setValue(i == 0 ? QString("num_cols") : QString("num_cols%1").arg(i + 1), m_cols[i]); + } +} + + +std::vector ConfigModel::readConfiguration(QSettings &settings, const QString &name) +{ + std::vector sounds; + int size = settings.beginReadArray(name); + if (size == 0) + sounds = getInitialSounds(); + else + { + sounds.resize(size); + for (int i = 0; i < size; i++) + { + settings.setArrayIndex(i); + sounds[i].readFromConfig(settings); + } + } + settings.endArray(); + return sounds; +} + + +void ConfigModel::writeConfiguration(QSettings & settings, const QString &name, const std::vector &sounds) +{ + settings.beginWriteArray(name); + for (int i = 0; i < (int)sounds.size(); i++) + { + settings.setArrayIndex(i); + sounds[i].saveToConfig(settings); + } + settings.endArray(); +} + + +void ConfigModel::setConfiguration(int config) +{ + m_activeConfig = config; + + /* Tell observers that our data changed */ + notifyAllEvents(); +} + +int ConfigModel::getConfiguration() +{ + return m_activeConfig; +} + +QString ConfigModel::getFileName( int itemId ) const +{ + if(itemId >= 0 && itemId < numSounds()) + return sounds()[itemId].filename; + return QString(); +} + + +void ConfigModel::setFileName( int itemId, const QString &fn ) +{ + if(itemId >= 0) + { + if(itemId < 1000 && itemId >= numSounds()) + sounds().resize(itemId + 1); + sounds()[itemId].filename = fn; + writeConfig(); + notify(NOTIFY_SET_SOUND, itemId); + } +} + + +const SoundInfo *ConfigModel::getSoundInfo(int itemId) const +{ + if(itemId >= 0 && itemId < numSounds()) + return &sounds()[itemId]; + return nullptr; +} + + +void ConfigModel::setSoundInfo( int itemId, const SoundInfo &info ) +{ + if(itemId < 1000 && itemId >= numSounds()) + sounds().resize(itemId + 1); + sounds()[itemId] = info; + writeConfig(); + notify(NOTIFY_SET_SOUND, itemId); +} + + +QString ConfigModel::GetConfigPath() +{ + // Find config path for config class + char configPath[PATH_BUFSIZE]; + ts3Functions.getConfigPath(configPath, PATH_BUFSIZE); + return QString::fromUtf8(configPath); +} + + +QString ConfigModel::GetFullConfigPath() +{ + QString fullPath = GetConfigPath(); + QChar last = fullPath[fullPath.count() - 1]; + if (last != '/' && last != '\\') + fullPath.append('/'); + fullPath.append("rp_soundboard.ini"); + return fullPath; +} + + +void ConfigModel::setRows( int n ) +{ + m_rows[m_activeConfig] = n; + writeConfig(); + notify(NOTIFY_SET_ROWS, n); +} + + +void ConfigModel::setCols( int n ) +{ + m_cols[m_activeConfig] = n; + writeConfig(); + notify(NOTIFY_SET_COLS, n); +} + + +void ConfigModel::setVolumeLocal( int val ) +{ + m_volumeLocal = val; + notify(NOTIFY_SET_VOLUME_LOCAL, val); +} + +void ConfigModel::setVolumeRemote( int val ) +{ + m_volumeRemote = val; + notify(NOTIFY_SET_VOLUME_REMOTE, val); +} + +void ConfigModel::setPlaybackLocal( bool val ) +{ + m_playbackLocal = val; + writeConfig(); + notify(NOTIFY_SET_PLAYBACK_LOCAL, val ? 1 : 0); +} + + +void ConfigModel::setMuteMyselfDuringPb(bool val) +{ + m_muteMyselfDuringPb = val; + writeConfig(); + notify(NOTIFY_SET_MUTE_MYSELF_DURING_PB, val ? 1 : 0); +} + + +void ConfigModel::getWindowSize(int *width, int *height) const +{ + if(width) + *width = m_windowWidth; + if(height) + *height = m_windowHeight; +} + + +void ConfigModel::setWindowSize(int width, int height) +{ + m_windowWidth = width; + m_windowHeight = height; + notify(NOTIFY_SET_WINDOW_SIZE, 0); +} + + +void ConfigModel::setNextUpdateCheck(uint time) +{ + m_nextUpdateCheck = time; + notify(NOTIFY_SET_NEXT_UPDATE_CHECK, (int)time); +} + + +void ConfigModel::notify(notifications_e what, int data) +{ + for(Observer *obs : m_obs) + obs->notify(*this, what, data); +} + + +void ConfigModel::setHotkeysEnabled(bool enabled) +{ + m_hotkeysEnabled = enabled; + writeConfig(); + notify(NOTIFY_SET_HOTKEYS_ENABLED, enabled ? 1 : 0); +} + + +void ConfigModel::addObserver(Observer *obs) +{ + m_obs.push_back(obs); +} + + +void ConfigModel::remObserver(Observer *obs) +{ + m_obs.erase(std::remove(m_obs.begin(), m_obs.end(), obs), m_obs.end()); +} + + +void ConfigModel::setBubbleButtonsBuild(int build) +{ + m_bubbleButtonsBuild = build; + writeConfig(); + notify(NOTIFY_SET_BUBBLE_BUTTONS_BUILD, build); +} + + +void ConfigModel::setBubbleStopBuild(int build) +{ + m_bubbleStopBuild = build; + writeConfig(); + notify(NOTIFY_SET_BUBBLE_STOP_BUILD, build); +} + + +void ConfigModel::setBubbleColsBuild(int build) +{ + m_bubbleColsBuild = build; + writeConfig(); + notify(NOTIFY_SET_BUBBLE_COLS_BUILD, build); +} + + +std::vector ConfigModel::getInitialSounds() +{ + char pluginPath[PATH_BUFSIZE]; + ts3Functions.getPluginPath(pluginPath, PATH_BUFSIZE, getPluginID()); + QString fullPath = QString::fromUtf8(pluginPath); + QChar last = fullPath[fullPath.count() - 1]; + if (last != '/' && last != '\\') + fullPath.append('/'); + fullPath.append("rp_soundboard/"); + + static const char* files[] = { + "Airhorn Sonata.mp3", + "Airhorn.mp3", + "Airporn.mp3", + "Peter Griffin Laugh.mp3", + "Spooky.mp3", + nullptr, + }; + + std::vector sounds; + for (int i = 0; files[i] != nullptr; i++) + { + SoundInfo info; + info.filename = fullPath + files[i]; + sounds.push_back(info); + } + return sounds; +} + + +void ConfigModel::notifyAllEvents() +{ + //Notify all changes + for(int i = 0; i < numSounds(); i++) + notify(NOTIFY_SET_SOUND, i); + notify(NOTIFY_SET_COLS, getCols()); + notify(NOTIFY_SET_ROWS, getRows()); + notify(NOTIFY_SET_VOLUME_LOCAL, m_volumeLocal); + notify(NOTIFY_SET_VOLUME_REMOTE, m_volumeRemote); + notify(NOTIFY_SET_PLAYBACK_LOCAL, m_playbackLocal); + notify(NOTIFY_SET_MUTE_MYSELF_DURING_PB, m_muteMyselfDuringPb); + notify(NOTIFY_SET_WINDOW_SIZE, 0); + notify(NOTIFY_SET_BUBBLE_BUTTONS_BUILD, m_bubbleButtonsBuild); + notify(NOTIFY_SET_BUBBLE_STOP_BUILD, m_bubbleStopBuild); + notify(NOTIFY_SET_BUBBLE_COLS_BUILD, m_bubbleColsBuild); + notify(NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS, m_showHotkeysOnButtons); + notify(NOTIFY_SET_HOTKEYS_ENABLED, m_hotkeysEnabled); +} + + +void ConfigModel::setShowHotkeysOnButtons(bool show) +{ + m_showHotkeysOnButtons = show; + writeConfig(); + notify(NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS, show ? 1 : 0); +} + diff --git a/src/ConfigModel.h b/src/ConfigModel.h index 8a58a0e..b61862f 100644 --- a/src/ConfigModel.h +++ b/src/ConfigModel.h @@ -1,142 +1,142 @@ -// src/ConfigModel.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#pragma once - -#include -#include -#include "SoundInfo.h" -#include -#include -#include -#include "common.h" - -class ConfigModel -{ -public: - enum notifications_e - { - NOTIFY_SET_SOUND, - NOTIFY_SET_ROWS, - NOTIFY_SET_COLS, - NOTIFY_SET_VOLUME_LOCAL, - NOTIFY_SET_VOLUME_REMOTE, - NOTIFY_SET_PLAYBACK_LOCAL, - NOTIFY_SET_MUTE_MYSELF_DURING_PB, - NOTIFY_SET_WINDOW_SIZE, - NOTIFY_SET_BUBBLE_BUTTONS_BUILD, - NOTIFY_SET_BUBBLE_STOP_BUILD, - NOTIFY_SET_BUBBLE_COLS_BUILD, - NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS, - NOTIFY_SET_HOTKEYS_ENABLED, - NOTIFY_SET_NEXT_UPDATE_CHECK, - }; - - class Observer - { - public: - virtual ~Observer() {} - virtual void notify(ConfigModel &model, notifications_e what, int data) = 0; - }; - -public: - ConfigModel(); - - void readConfig(const QString &file = QString()); - void writeConfig(const QString &file = QString()); - - void notifyAllEvents(); - - static QString GetConfigPath(); - static QString GetFullConfigPath(); - - QString getFileName(int itemId) const; - void setFileName(int itemId, const QString &fn); - - const SoundInfo *getSoundInfo(int itemId) const; - void setSoundInfo(int itemId, const SoundInfo &info); - - inline int getRows() const { return m_rows[m_activeConfig]; } - void setRows(int n); - - inline int getCols() const { return m_cols[m_activeConfig]; } - void setCols(int n); - - inline int getVolumeLocal() const { return m_volumeLocal; } - void setVolumeLocal(int val); - - inline int getVolumeRemote() const { return m_volumeRemote; } - void setVolumeRemote(int val); - - inline bool getPlaybackLocal() const { return m_playbackLocal; } - void setPlaybackLocal(bool val); - - inline bool getMuteMyselfDuringPb() const { return m_muteMyselfDuringPb; } - void setMuteMyselfDuringPb(bool val); - - void getWindowSize(int *width, int *height) const; - void setWindowSize(int width, int height); - - inline int getBubbleButtonsBuild() const { return m_bubbleButtonsBuild; } - void setBubbleButtonsBuild(int build); - - inline int getBubbleStopBuild() const { return m_bubbleStopBuild; } - void setBubbleStopBuild(int build); - - inline int getBubbleColsBuild() const { return m_bubbleColsBuild; } - void setBubbleColsBuild(int build); - - inline bool getShowHotkeysOnButtons() const { return m_showHotkeysOnButtons; } - void setShowHotkeysOnButtons(bool show); - - inline bool getHotkeysEnabled() const { return m_hotkeysEnabled; } - void setHotkeysEnabled(bool enabled); - - void addObserver(Observer *obs); - void remObserver(Observer *obs); - - void setConfiguration(int config); - int getConfiguration(); - - const std::vector &sounds() const { return m_sounds[m_activeConfig]; } - int numSounds() const { return (int)sounds().size(); } - - uint getNextUpdateCheck() const { return m_nextUpdateCheck; } - void setNextUpdateCheck(uint time); - -private: - std::vector &sounds() { return m_sounds[m_activeConfig]; } - void notify(notifications_e what, int data); - std::vector getInitialSounds(); - std::vector readConfiguration(QSettings & settings, const QString &name); - void writeConfiguration(QSettings & settings, const QString &name, const std::vector &sounds); - - std::vector m_obs; - std::array, NUM_CONFIGS> m_sounds; - int m_activeConfig; - - std::array m_rows; - std::array m_cols; - int m_volumeLocal; - int m_volumeRemote; - bool m_playbackLocal; - bool m_muteMyselfDuringPb; - int m_windowWidth; - int m_windowHeight; - - int m_bubbleButtonsBuild; - int m_bubbleStopBuild; - int m_bubbleColsBuild; - - bool m_showHotkeysOnButtons; - bool m_hotkeysEnabled; - - uint m_nextUpdateCheck; -}; - +// src/ConfigModel.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#pragma once + +#include +#include +#include "SoundInfo.h" +#include +#include +#include +#include "common.h" + +class ConfigModel +{ +public: + enum notifications_e + { + NOTIFY_SET_SOUND, + NOTIFY_SET_ROWS, + NOTIFY_SET_COLS, + NOTIFY_SET_VOLUME_LOCAL, + NOTIFY_SET_VOLUME_REMOTE, + NOTIFY_SET_PLAYBACK_LOCAL, + NOTIFY_SET_MUTE_MYSELF_DURING_PB, + NOTIFY_SET_WINDOW_SIZE, + NOTIFY_SET_BUBBLE_BUTTONS_BUILD, + NOTIFY_SET_BUBBLE_STOP_BUILD, + NOTIFY_SET_BUBBLE_COLS_BUILD, + NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS, + NOTIFY_SET_HOTKEYS_ENABLED, + NOTIFY_SET_NEXT_UPDATE_CHECK, + }; + + class Observer + { + public: + virtual ~Observer() {} + virtual void notify(ConfigModel &model, notifications_e what, int data) = 0; + }; + +public: + ConfigModel(); + + void readConfig(const QString &file = QString()); + void writeConfig(const QString &file = QString()); + + void notifyAllEvents(); + + static QString GetConfigPath(); + static QString GetFullConfigPath(); + + QString getFileName(int itemId) const; + void setFileName(int itemId, const QString &fn); + + const SoundInfo *getSoundInfo(int itemId) const; + void setSoundInfo(int itemId, const SoundInfo &info); + + inline int getRows() const { return m_rows[m_activeConfig]; } + void setRows(int n); + + inline int getCols() const { return m_cols[m_activeConfig]; } + void setCols(int n); + + inline int getVolumeLocal() const { return m_volumeLocal; } + void setVolumeLocal(int val); + + inline int getVolumeRemote() const { return m_volumeRemote; } + void setVolumeRemote(int val); + + inline bool getPlaybackLocal() const { return m_playbackLocal; } + void setPlaybackLocal(bool val); + + inline bool getMuteMyselfDuringPb() const { return m_muteMyselfDuringPb; } + void setMuteMyselfDuringPb(bool val); + + void getWindowSize(int *width, int *height) const; + void setWindowSize(int width, int height); + + inline int getBubbleButtonsBuild() const { return m_bubbleButtonsBuild; } + void setBubbleButtonsBuild(int build); + + inline int getBubbleStopBuild() const { return m_bubbleStopBuild; } + void setBubbleStopBuild(int build); + + inline int getBubbleColsBuild() const { return m_bubbleColsBuild; } + void setBubbleColsBuild(int build); + + inline bool getShowHotkeysOnButtons() const { return m_showHotkeysOnButtons; } + void setShowHotkeysOnButtons(bool show); + + inline bool getHotkeysEnabled() const { return m_hotkeysEnabled; } + void setHotkeysEnabled(bool enabled); + + void addObserver(Observer *obs); + void remObserver(Observer *obs); + + void setConfiguration(int config); + int getConfiguration(); + + const std::vector &sounds() const { return m_sounds[m_activeConfig]; } + int numSounds() const { return (int)sounds().size(); } + + uint getNextUpdateCheck() const { return m_nextUpdateCheck; } + void setNextUpdateCheck(uint time); + +private: + std::vector &sounds() { return m_sounds[m_activeConfig]; } + void notify(notifications_e what, int data); + std::vector getInitialSounds(); + std::vector readConfiguration(QSettings & settings, const QString &name); + void writeConfiguration(QSettings & settings, const QString &name, const std::vector &sounds); + + std::vector m_obs; + std::array, NUM_CONFIGS> m_sounds; + int m_activeConfig; + + std::array m_rows; + std::array m_cols; + int m_volumeLocal; + int m_volumeRemote; + bool m_playbackLocal; + bool m_muteMyselfDuringPb; + int m_windowWidth; + int m_windowHeight; + + int m_bubbleButtonsBuild; + int m_bubbleStopBuild; + int m_bubbleColsBuild; + + bool m_showHotkeysOnButtons; + bool m_hotkeysEnabled; + + uint m_nextUpdateCheck; +}; + diff --git a/src/HighResClock.cpp b/src/HighResClock.cpp index 2c6044f..6fc95a2 100644 --- a/src/HighResClock.cpp +++ b/src/HighResClock.cpp @@ -1,33 +1,33 @@ -// src/HighResClock.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#include "HighResClock.h" - -#ifdef _MSC_VER - -#include "Windows.h" - -namespace -{ - const long long g_Frequency = []() -> long long - { - LARGE_INTEGER frequency; - QueryPerformanceFrequency(&frequency); - return frequency.QuadPart; - }(); -} - -HighResClock::time_point HighResClock::now() -{ - LARGE_INTEGER count; - QueryPerformanceCounter(&count); - return time_point(duration(count.QuadPart * static_cast(period::den) / g_Frequency)); -} - -#endif +// src/HighResClock.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#include "HighResClock.h" + +#ifdef _MSC_VER + +#include "Windows.h" + +namespace +{ + const long long g_Frequency = []() -> long long + { + LARGE_INTEGER frequency; + QueryPerformanceFrequency(&frequency); + return frequency.QuadPart; + }(); +} + +HighResClock::time_point HighResClock::now() +{ + LARGE_INTEGER count; + QueryPerformanceCounter(&count); + return time_point(duration(count.QuadPart * static_cast(period::den) / g_Frequency)); +} + +#endif diff --git a/src/HighResClock.h b/src/HighResClock.h index 9d45bfb..a1d8c2a 100644 --- a/src/HighResClock.h +++ b/src/HighResClock.h @@ -1,27 +1,27 @@ -// src/HighResClock.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include - -#ifdef _MSC_VER -struct HighResClock -{ - typedef long long rep; - typedef std::nano period; - typedef std::chrono::duration duration; - typedef std::chrono::time_point time_point; - static const bool is_steady = true; - - static time_point now(); -}; -#else -typedef std::chrono::high_resolution_clock HighResClock; -#endif - +// src/HighResClock.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include + +#ifdef _MSC_VER +struct HighResClock +{ + typedef long long rep; + typedef std::nano period; + typedef std::chrono::duration duration; + typedef std::chrono::time_point time_point; + static const bool is_steady = true; + + static time_point now(); +}; +#else +typedef std::chrono::high_resolution_clock HighResClock; +#endif + diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 19810fb..79c5927 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,910 +1,910 @@ -// src/MainWindow.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#include "common.h" - -#include -#include -#include -#include -#include -#include - -#include "MainWindow.h" -#include "ConfigModel.h" -#include "main.h" -#include "SoundSettings.h" -#include "ts3log.h" -#include "SpeechBubble.h" -#include "buildinfo.h" -#include "plugin.h" -#include "ExpandableSection.h" -#include "samples.h" -#include "SoundButton.h" - -#ifdef _WIN32 -#include "Windows.h" -#endif - -enum button_choices_e { - BC_CHOOSE = 0, - BC_ADVANCED, - BC_SET_HOTKEY, - BC_SET_COLOR, - BC_DELETE, -}; - - -MainWindow::MainWindow( ConfigModel *model, QWidget *parent /*= 0*/ ) : - QWidget(parent), - ui(new Ui::MainWindow), - m_model(model), - m_modelObserver(*this), - m_buttonBubble(nullptr) -{ - /* Ensure resources are loaded */ - Q_INIT_RESOURCE(qtres); - - m_pauseIcon = QIcon(":/icon/img/pausebutton_32.png"); - m_playIcon = QIcon(":/icon/img/playarrow_32.png"); - - ui->setupUi(this); - //setAttribute(Qt::WA_DeleteOnClose); - - createConfigButtons(); - - settingsSection = new ExpandableSection("Settings", 200, this); - settingsSection->setContentLayout(*ui->settingsWidget->layout()); - layout()->addWidget(settingsSection); - - configsSection = new ExpandableSection("Configurations", 200, this); - configsSection->setContentLayout(*ui->configsWidget->layout()); - layout()->addWidget(configsSection); - - QAction *actChooseFile = new QAction("Choose File", this); - actChooseFile->setData((int)BC_CHOOSE); - m_buttonContextMenu.addAction(actChooseFile); - - QAction *actAdvancedOpts = new QAction("Advanced Options", this); - actAdvancedOpts->setData((int)BC_ADVANCED); - m_buttonContextMenu.addAction(actAdvancedOpts); - - actSetHotkey = new QAction("Set hotkey", this); - actSetHotkey->setData((int)BC_SET_HOTKEY); - m_buttonContextMenu.addAction(actSetHotkey); - - QAction *actSetColor = new QAction("Set color", this); - actSetColor->setData((int)BC_SET_COLOR); - m_buttonContextMenu.addAction(actSetColor); - - QAction *actDeleteButton = new QAction("Make button great again (delete)", this); - actDeleteButton->setData((int)BC_DELETE); - m_buttonContextMenu.addAction(actDeleteButton); - - createButtons(); - - ui->b_stop->setContextMenuPolicy(Qt::CustomContextMenu); - ui->b_pause->setContextMenuPolicy(Qt::CustomContextMenu); - ui->cb_mute_locally->setContextMenuPolicy(Qt::CustomContextMenu); - ui->cb_mute_myself->setContextMenuPolicy(Qt::CustomContextMenu); - ui->sl_volumeLocal->setContextMenuPolicy(Qt::CustomContextMenu); - ui->sl_volumeRemote->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(ui->b_stop, SIGNAL(clicked()), this, SLOT(onClickedStop())); - connect(ui->b_stop, SIGNAL(customContextMenuRequested(const QPoint&)), this, - SLOT(showStopButtonContextMenu(const QPoint&))); - connect(ui->b_pause, SIGNAL(clicked()), this, SLOT(onButtonPausePressed())); - connect(ui->b_pause, SIGNAL(customContextMenuRequested(const QPoint&)), this, - SLOT(showPauseButtonContextMenu(const QPoint&))); - connect(ui->sl_volumeLocal, SIGNAL(valueChanged(int)), this, SLOT(onUpdateVolumeLocal(int))); - connect(ui->sl_volumeRemote, SIGNAL(valueChanged(int)), this, SLOT(onUpdateVolumeRemote(int))); - connect(ui->cb_mute_locally, SIGNAL(clicked(bool)), this, SLOT(onUpdateMuteLocally(bool))); - connect(ui->sb_rows, SIGNAL(valueChanged(int)), this, SLOT(onUpdateRows(int))); - connect(ui->sb_cols, SIGNAL(valueChanged(int)), this, SLOT(onUpdateCols(int))); - connect(ui->cb_mute_myself, SIGNAL(clicked(bool)), this, SLOT(onUpdateMuteMyself(bool))); - connect(ui->cb_show_hotkeys_on_buttons, SIGNAL(clicked(bool)), this, SLOT(onUpdateShowHotkeysOnButtons(bool))); - connect(ui->cb_disable_hotkeys, SIGNAL(clicked(bool)), this, SLOT(onUpdateHotkeysDisabled(bool))); - connect(ui->filterEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onFilterEditTextChanged(const QString&))); - connect(ui->cb_mute_locally, &QCheckBox::customContextMenuRequested, - [this](const QPoint &point) {this->showSetHotkeyMenu(HOTKEY_MUTE_ON_MY_CLIENT, ui->cb_mute_locally->mapToGlobal(point));}); - connect(ui->cb_mute_myself, &QCheckBox::customContextMenuRequested, - [this](const QPoint &point) {this->showSetHotkeyMenu(HOTKEY_MUTE_MYSELF, ui->cb_mute_myself->mapToGlobal(point));}); - connect(ui->sl_volumeLocal, &QSlider::customContextMenuRequested, this, &MainWindow::onVolumeSliderContextMenuLocal); - connect(ui->sl_volumeRemote, &QSlider::customContextMenuRequested, this, &MainWindow::onVolumeSliderContextMenuRemote); - - /* Load/Save Model */ - connect(ui->pushLoad, SIGNAL(released()), this, SLOT(onLoadModel())); - connect(ui->pushSave, SIGNAL(released()), this, SLOT(onSaveModel())); - - ui->playingIconLabel->hide(); - ui->playingLabel->setText(""); - playingIconTimer = new QTimer(this); - playingIconTimer->setInterval(150); - connect(playingIconTimer, SIGNAL(timeout()), this, SLOT(onPlayingIconTimer())); - - Sampler *sampler = sb_getSampler(); - connect(sampler, SIGNAL(onStartPlaying(bool, QString)), this, SLOT(onStartPlayingSound(bool, QString)), Qt::QueuedConnection); - connect(sampler, SIGNAL(onStopPlaying()), this, SLOT(onStopPlayingSound()), Qt::QueuedConnection); - connect(sampler, SIGNAL(onPausePlaying()), this, SLOT(onPausePlayingSound())); // No queued connection since signal is emitted from GUI Thread - connect(sampler, SIGNAL(onUnpausePlaying()), this, SLOT(onUnpausePlayingSound())); // No queued connection since signal is emitted from GUI Thread - - createBubbles(); - - m_model->addObserver(&m_modelObserver); - - /* Force configuration 0 */ - setConfiguration(0); - - ui->gridWidget->setStyleSheet( - "QPushButton {" - " padding: 4px;" - " border: 1px solid rgb(173, 173, 173);" - " color: black;" - " background-color: rgb(225, 225, 225);" - "}" - "QPushButton:disabled {" - " background-color: rgb(204, 204, 204);" - " border-color: rgb(191, 191, 191);" - " color: rgb(120, 120, 120);" - "}" - "QPushButton:hover {" - " background-color: rgb(228, 239, 249);" - " border-color: rgb(11, 123, 212);" - "}" - "QPushButton:pressed {" - " background-color: rgb(204, 228, 247);" - " border-color: rgb(0, 85, 155);" - "}"); -} - -void MainWindow::setConfiguration(int cfg) -{ - if (cfg < 0 || cfg >= NUM_CONFIGS) - { - logError("Invalid config id: %i", cfg); - return; - } - - m_configRadioButtons[cfg]->setChecked(true); - m_model->setConfiguration(cfg); - ui->labelStatus->setText(QString("Configuration %1").arg(cfg + 1)); -} - -void MainWindow::onSetConfig() -{ - QRadioButton *button = qobject_cast(sender()); - int configId = button->property("configId").toInt(); - setConfiguration(configId); -} - -void MainWindow::onConfigHotkey() -{ - QPushButton *button = qobject_cast(sender()); - int configId = button->property("configId").toInt(); - - char buf[16]; - sb_getInternalConfigHotkeyName(configId, buf); - ts3Functions.requestHotkeyInputDialog(getPluginID(), buf, configId, this); -} - - -MainWindow::~MainWindow() -{ - m_model->remObserver(&m_modelObserver); - delete ui; -} - -void MainWindow::onSaveModel() -{ - QString fn = QFileDialog::getSaveFileName(this, tr("Choose File to Save"), QString(), tr("Ini Files (*.ini)")); - if (fn.isNull()) - return; - m_model->writeConfig(fn); -} - -void MainWindow::onLoadModel() -{ - QString fn = QFileDialog::getOpenFileName(this, tr("Choose File to Load"), QString(), tr("Ini Files (*.ini)")); - if (fn.isNull()) - return; - m_model->readConfig(fn); -} - -void MainWindow::closeEvent(QCloseEvent *) -{ - m_model->setWindowSize(size().width(), size().height()); -} - -void MainWindow::onClickedPlay() -{ - QPushButton *button = dynamic_cast(sender()); - size_t buttonId = std::find_if(m_buttons.begin(), m_buttons.end(), [button](SoundButton *b){return b == button;}) - m_buttons.begin(); - - playSound(buttonId); -} - - -void MainWindow::onClickedStop() -{ - sb_stopPlayback(); -} - - -void MainWindow::onUpdateVolumeLocal(int val) -{ - m_model->setVolumeLocal(val); -} - - -void MainWindow::onUpdateVolumeRemote(int val) -{ - m_model->setVolumeRemote(val); -} - - -void MainWindow::onUpdateMuteLocally(bool val) -{ - m_model->setPlaybackLocal(!val); -} - - -void MainWindow::onUpdateCols(int val) -{ - m_model->setCols(val); -} - - -void MainWindow::onUpdateRows(int val) -{ - m_model->setRows(val); -} - - -void MainWindow::onUpdateMuteMyself(bool val) -{ - m_model->setMuteMyselfDuringPb(val); -} - - -void MainWindow::createButtons() -{ - for(SoundButton *button : m_buttons) - delete button; - m_buttons.clear(); - - int numRows = m_model->getRows(); - int numCols = m_model->getCols(); - - for(int i = 0; i < numRows; i++) - { - for(int j = 0; j < numCols; j++) - { - SoundButton *elem = new SoundButton(this); - elem->setProperty("buttonId", (int)m_buttons.size()); - elem->setText("(no file)"); - elem->setEnabled(true); - QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Expanding); - policy.setRetainSizeWhenHidden(true); - elem->setSizePolicy(policy); - ui->gridLayout->addWidget(elem, i, j); - connect(elem, SIGNAL(clicked()), this, SLOT(onClickedPlay())); - elem->setContextMenuPolicy(Qt::CustomContextMenu); - connect(elem, SIGNAL(customContextMenuRequested(const QPoint&)), this, - SLOT(showButtonContextMenu(const QPoint&))); - connect(elem, SIGNAL(fileDropped(QList)), this, SLOT(onButtonFileDropped(QList))); - connect(elem, SIGNAL(buttonDropped(SoundButton*)), this, SLOT(onButtonDroppedOnButton(SoundButton*))); - - elem->updateGeometry(); - m_buttons.push_back(elem); - } - } - - for(int i = 0; i < (int)m_buttons.size(); i++) - updateButtonText(i); - - if(m_buttonBubble) - m_buttonBubble->attachTo(m_buttons[0]); -} - - -void MainWindow::createConfigButtons() -{ - for (int i = 0; i < NUM_CONFIGS; i++) - { - m_configRadioButtons[i] = new QRadioButton(this); - m_configRadioButtons[i]->setText(QString("Config %1").arg(i + 1)); - m_configRadioButtons[i]->setProperty("configId", i); - m_configRadioButtons[i]->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - ui->configsGrid->addWidget(m_configRadioButtons[i], 0, i, Qt::AlignCenter); - connect(m_configRadioButtons[i], SIGNAL(clicked()), this, SLOT(onSetConfig())); - - m_configHotkeyButtons[i] = new QPushButton(this); - m_configHotkeyButtons[i]->setText("Set hotkey"); - m_configHotkeyButtons[i]->setProperty("configId", i); - m_configHotkeyButtons[i]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - m_configHotkeyButtons[i]->setToolTipDuration(0); - m_configHotkeyButtons[i]->setToolTip(getConfigShortcutString(i)); - ui->configsGrid->addWidget(m_configHotkeyButtons[i], 1, i, Qt::AlignCenter); - connect(m_configHotkeyButtons[i], SIGNAL(clicked()), this, SLOT(onConfigHotkey())); - - ui->configsWidget->updateGeometry(); - } -} - - -bool MainWindow::hotkeysEnabled() -{ - return m_model->getHotkeysEnabled(); -} - -void MainWindow::updateButtonText(int i) -{ - if(i >= (int)m_buttons.size()) - return; - - QString text; - const SoundInfo *info = m_model->getSoundInfo(i); - if (info && !info->filename.isEmpty()) - { - if (!info->customText.isEmpty()) - text = unescapeCustomText(info->customText); - else - text = QFileInfo(info->filename).baseName(); - } - else - text = "(no file)"; - - if (m_model->getShowHotkeysOnButtons()) - { - QString shortcut = getShortcutString(i); - if (shortcut.length() > 0) - text = text + "\n" + shortcut; - } - m_buttons[i]->setText(text); - m_buttons[i]->setBackgroundColor(info ? info->customColor : QColor(0, 0, 0, 0)); -} - - -void MainWindow::showButtonContextMenu( const QPoint &point ) -{ - QPushButton *button = dynamic_cast(sender()); - size_t buttonId = std::find_if(m_buttons.begin(), m_buttons.end(), [button](SoundButton *b){return b == button;}) - m_buttons.begin(); - - QString shortcutName = getShortcutString(buttonId); - QString hotkeyText = "Set hotkey (Current: " + - (shortcutName.isEmpty() ? QString("None") : shortcutName) + ")"; - actSetHotkey->setText(hotkeyText); - - QPoint globalPos = m_buttons[buttonId]->mapToGlobal(point); - QAction *action = m_buttonContextMenu.exec(globalPos); - if(action) - { - bool ok = false; - int choice = action->data().toInt(&ok); - if(ok) - { - switch(choice) - { - case BC_CHOOSE: - chooseFile(buttonId); - break; - case BC_ADVANCED: - openAdvanced(buttonId); - break; - case BC_SET_HOTKEY: - openHotkeySetDialog(buttonId); - break; - case BC_SET_COLOR: - openButtonColorDialog(buttonId); - break; - case BC_DELETE: - deleteButton(buttonId); - break; - default: break; - } - } - else - logError("Invalid user data in context menu"); - } -} - - -void MainWindow::setPlayingLabelIcon(int index) -{ - ui->playingIconLabel->setPixmap(QPixmap(QString(":/icon/img/speaker_icon_%1_64.png").arg(index))); -} - - - -void MainWindow::playSound( size_t buttonId ) -{ - const SoundInfo *info = m_model->getSoundInfo(buttonId); - if(info) - sb_playFile(*info); -} - - -void MainWindow::chooseFile( size_t buttonId ) -{ - QString filePath = m_model->getFileName(buttonId); - QString fn = QFileDialog::getOpenFileName(this, tr("Choose File"), filePath, tr("Files (*.*)")); - if (fn.isNull()) - return; - setButtonFile(buttonId, fn); - -} - - -void MainWindow::openAdvanced( size_t buttonId ) -{ - const SoundInfo *buttonInfo = m_model->getSoundInfo(buttonId); - SoundInfo defaultInfo; - const SoundInfo &info = buttonInfo ? *buttonInfo : defaultInfo; - SoundSettingsQt dlg(info, buttonId, this); - dlg.setWindowTitle(QString("Sound %1 Settings").arg(QString::number(buttonId + 1))); - if(dlg.exec() == QDialog::Accepted) - m_model->setSoundInfo(buttonId, dlg.getSoundInfo()); -} - - -void MainWindow::deleteButton(size_t buttonId) -{ - const SoundInfo *info = m_model->getSoundInfo(buttonId); - if (info) - m_model->setSoundInfo(buttonId, SoundInfo()); -} - - -void MainWindow::createBubbles() -{ - if(m_model->getBubbleButtonsBuild() == 0) - { - m_buttonBubble = new SpeechBubble(this); - m_buttonBubble->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - m_buttonBubble->setFixedSize(180, 80); - m_buttonBubble->setText("Right click to choose sound file\nor open advanced options."); - m_buttonBubble->attachTo(m_buttons[0]); - connect(m_buttonBubble, SIGNAL(closePressed()), this, SLOT(onButtonBubbleFinished())); - } - - if(m_model->getBubbleStopBuild() == 0) - { - SpeechBubble *stopBubble = new SpeechBubble(this); - stopBubble->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - stopBubble->setFixedSize(180, 60); - stopBubble->setText("Stop the currently playing sound."); - stopBubble->attachTo(ui->b_stop); - connect(stopBubble, SIGNAL(closePressed()), this, SLOT(onStopBubbleFinished())); - } - - if(m_model->getBubbleColsBuild() == 0) - { - settingsSection->setExpanded(true); - SpeechBubble *colsBubble = new SpeechBubble(this); - colsBubble->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - colsBubble->setFixedSize(180, 80); - colsBubble->setText("Change the number of buttons\non the soundboard."); - colsBubble->attachTo(ui->sb_cols); - connect(colsBubble, SIGNAL(closePressed()), this, SLOT(onColsBubbleFinished())); - } -} - - -void MainWindow::onStopBubbleFinished() -{ - m_model->setBubbleStopBuild(buildinfo_getBuildNumber()); -} - - -void MainWindow::onButtonBubbleFinished() -{ - m_model->setBubbleButtonsBuild(buildinfo_getBuildNumber()); -} - - -void MainWindow::onColsBubbleFinished() -{ - m_model->setBubbleColsBuild(buildinfo_getBuildNumber()); -} - - -void MainWindow::showStopButtonContextMenu(const QPoint &point) -{ - showSetHotkeyMenu(HOTKEY_STOP_ALL, ui->b_stop->mapToGlobal(point)); -} - - -void MainWindow::showPauseButtonContextMenu(const QPoint &point) -{ - showSetHotkeyMenu(HOTKEY_PAUSE_ALL, ui->b_pause->mapToGlobal(point)); -} - - -void MainWindow::showSetHotkeyMenu(const char *hotkeyName, const QPoint &point) -{ - QString hotkeyString = getShortcutString(hotkeyName); - QString hotkeyText = "Set hotkey (Current: " + - (hotkeyString.isEmpty() ? QString("None") : hotkeyString) + ")"; - - QMenu menu; - menu.addAction(hotkeyText); - QAction *action = menu.exec(point); - if (action) - ts3Functions.requestHotkeyInputDialog(getPluginID(), hotkeyName, 0, this); -} - - -void MainWindow::onStartPlayingSound(bool preview, QString filename) -{ - QFileInfo info(filename); - ui->playingLabel->setText(info.fileName()); - setPlayingLabelIcon(0); - ui->playingIconLabel->show(); - playingIconIndex = 1; - playingIconTimer->start(); - ui->b_stop->setEnabled(true); - ui->b_pause->setEnabled(true); - ui->b_pause->setIcon(m_pauseIcon); -} - - -void MainWindow::onStopPlayingSound() -{ - playingIconTimer->stop(); - ui->playingLabel->setText(""); - ui->playingIconLabel->hide(); - ui->b_pause->setIcon(m_pauseIcon); -} - - -void MainWindow::onPausePlayingSound() -{ - playingIconTimer->stop(); - ui->b_pause->setIcon(m_playIcon); -} - - -void MainWindow::onUnpausePlayingSound() -{ - playingIconTimer->start(); - ui->b_pause->setIcon(m_pauseIcon); -} - - -void MainWindow::onPlayingIconTimer() -{ - setPlayingLabelIcon(playingIconIndex); - ++playingIconIndex %= 4; -} - - -void MainWindow::openHotkeySetDialog(size_t buttonId) -{ - openHotkeySetDialog(buttonId, this); -} - - -void MainWindow::openButtonColorDialog(size_t buttonId) -{ - QColorDialog dialog; - const SoundInfo *pinfo = m_model->getSoundInfo(buttonId); - SoundInfo info = pinfo ? *pinfo : SoundInfo(); - dialog.setCurrentColor(info.customColor); - dialog.setWindowTitle("Choose button color"); - if (dialog.exec()) - { - info.customColor = dialog.currentColor(); - m_model->setSoundInfo(buttonId, info); - } -} - - -QString MainWindow::unescapeCustomText(const QString &text) -{ - QString cpy = text; - return cpy.replace("\\n", "\n"); -} - - -void MainWindow::openHotkeySetDialog( size_t buttonId, QWidget *parent ) -{ - char intName[16]; - sb_getInternalHotkeyName((int)buttonId, intName); - ts3Functions.requestHotkeyInputDialog(getPluginID(), intName, 0, parent); -} - - -QString MainWindow::getShortcutString(const char *internalName) -{ - std::vector name(128, 0); - char *namePtr = name.data(); - unsigned int res = ts3Functions.getHotkeyFromKeyword( - getPluginID(), &internalName, &namePtr, 1, 128); - QString str = res == 0 ? QString(name.data()) : QString(); - return str; -} - - - -QString MainWindow::getShortcutString(size_t buttonId) -{ - char intName[16]; - sb_getInternalHotkeyName((int)buttonId, intName); - return getShortcutString(intName); -} - -QString MainWindow::getConfigShortcutString(int cfg) -{ - char buf[16]; - sb_getInternalConfigHotkeyName(cfg, buf); - QString shortcut = getShortcutString(buf); - if (!shortcut.isEmpty()) - return shortcut; - - return QString("no hotkey"); -} - - -void MainWindow::onHotkeyRecordedEvent(const char *keyword, const char *key) -{ - QString sKey = key; - - int configId = -1; - if (sscanf(keyword, "config_%i", &configId) == 1) - { - if (configId >= 0 && configId < NUM_CONFIGS) - m_configHotkeyButtons[configId]->setToolTip(sKey); - else - logError("Invalid hotkey keyword: %s", keyword); - } - else - { - QString sKeyword = keyword; - - emit hotkeyRecordedEvent(sKey, sKeyword); - - if (m_model->getShowHotkeysOnButtons()) - for (size_t i = 0; i < m_buttons.size(); i++) - updateButtonText(i); - } -} - - -void MainWindow::onUpdateShowHotkeysOnButtons(bool val) -{ - m_model->setShowHotkeysOnButtons(val); -} - - -void MainWindow::onUpdateHotkeysDisabled(bool val) -{ - m_model->setHotkeysEnabled(!val); -} - - -void MainWindow::showEvent(QShowEvent *evt) -{ - QWidget::showEvent(evt); - - for(size_t i = 0; i < m_buttons.size(); i++) - updateButtonText(i); -} - - -void MainWindow::onButtonFileDropped(const QList &urls) -{ - int buttonId = sender()->property("buttonId").toInt(); - int buttonNr = 0; - - for(int i = 0; i < urls.size(); i++) - { - if (buttonNr == 1) - { - QMessageBox msgbox(QMessageBox::Icon::Question, "Fill buttons?", - "You dropped multiple files. Consecutively apply them to the buttons following the one you dropped your files on?", - QMessageBox::Yes | QMessageBox::No, this); - if (msgbox.exec() == QMessageBox::No) - break; - } - - if (urls[i].isLocalFile()) - { - QFileInfo info(urls[i].toLocalFile()); - if (info.isFile()) - { - setButtonFile(buttonId + buttonNr, urls[i].toLocalFile(), buttonNr == 0); - ++buttonNr; - } - else if (buttonNr == 0) - { - QMessageBox msgBox(QMessageBox::Icon::Critical, "Unsupported drop type", "Some things could not be dropped here :(", QMessageBox::Ok, this); - msgBox.exec(); - } - } - } -} - - -void MainWindow::setButtonFile(size_t buttonId, const QString &fn, bool askForDisablingCrop) -{ - const SoundInfo *info = m_model->getSoundInfo(buttonId); - if (askForDisablingCrop && info && info->cropEnabled && info->filename != fn) - { - QMessageBox mb(QMessageBox::Question, "Keep crop settings?", - "You selected a new file for a button that has 'crop sound' enabled.", QMessageBox::NoButton, this); - QPushButton *btnDisable = mb.addButton("Disable cropping (recommended)", QMessageBox::YesRole); - QPushButton *btnKeep = mb.addButton("Keep old crop settings", QMessageBox::NoRole); - mb.setDefaultButton(btnDisable); - mb.exec(); - if (mb.clickedButton() != btnKeep) - { - SoundInfo newInfo(*info); - newInfo.cropEnabled = false; - m_model->setSoundInfo(buttonId, newInfo); - } - } - m_model->setFileName(buttonId, fn); -} - - -void MainWindow::onButtonPausePressed() -{ - sb_pauseButtonPressed(); -} - - -void MainWindow::onButtonDroppedOnButton(SoundButton *button) -{ - SoundButton *btn0 = button; - SoundButton *btn1 = qobject_cast(sender()); - int bid0 = btn0->property("buttonId").toInt(); - int bid1 = btn1->property("buttonId").toInt(); - const SoundInfo *info0 = m_model->getSoundInfo(bid0); - const SoundInfo *info1 = m_model->getSoundInfo(bid1); - - // Copy sound info - SoundInfo infoCopy0; - SoundInfo infoCopy1; - if (info0) - infoCopy0 = *info0; - if (info1) - infoCopy1 = *info1; - - // And switch em - m_model->setSoundInfo(bid0, infoCopy1); - m_model->setSoundInfo(bid1, infoCopy0); - - // Switch button position and then animate the buttons to slide into place - const int animDuration = 300; - QPropertyAnimation *anim0 = new QPropertyAnimation(btn0, "pos"); - anim0->setStartValue(btn1->pos()); - anim0->setEndValue(btn0->pos()); - anim0->setDuration(animDuration); - - QPropertyAnimation *anim1 = new QPropertyAnimation(btn1, "pos"); - anim1->setStartValue(btn0->pos()); - anim1->setEndValue(btn1->pos()); - anim1->setDuration(animDuration); - - anim0->start(); - anim1->start(); - btn0->raise(); - btn1->raise(); -} - - -void MainWindow::onFilterEditTextChanged(const QString &text) -{ - QString filter = text.trimmed(); - for (auto &button : m_buttons) - { - int buttonId = button->property("buttonId").toInt(); - const SoundInfo *info = m_model->getSoundInfo(buttonId); - bool hasFile = info && !info->filename.isEmpty(); - QString buttonText = button->text(); - bool pass = filter.length() == 0 || (hasFile && buttonText.contains(filter, Qt::CaseInsensitive)); - button->setVisible(pass); - } -} - - -void MainWindow::onVolumeSliderContextMenuLocal(const QPoint &point) -{ - QString hotkeyStringIncr = getShortcutString(HOTKEY_VOLUME_INCREASE); - QString hotkeyTextIncr = "Set 'increase 20%' hotkey (Current: " + - (hotkeyStringIncr.isEmpty() ? QString("None") : hotkeyStringIncr) + ")"; - - QString hotkeyStringDecr = getShortcutString(HOTKEY_VOLUME_DECREASE); - QString hotkeyTextDecr = "Set 'decrease 20%' hotkey (Current: " + - (hotkeyStringDecr.isEmpty() ? QString("None") : hotkeyStringDecr) + ")"; - - QMenu menu; - QAction *actIncr = menu.addAction(hotkeyTextIncr); - QAction *actDecr = menu.addAction(hotkeyTextDecr); - QAction *action = menu.exec(ui->sl_volumeLocal->mapToGlobal(point)); - if (action == actIncr) - ts3Functions.requestHotkeyInputDialog(getPluginID(), HOTKEY_VOLUME_INCREASE, 0, this); - else if (action == actDecr) - ts3Functions.requestHotkeyInputDialog(getPluginID(), HOTKEY_VOLUME_DECREASE, 0, this); -} - - -void MainWindow::onVolumeSliderContextMenuRemote(const QPoint &point) -{ - QString hotkeyStringIncr = getShortcutString(HOTKEY_VOLUME_INCREASE); - QString hotkeyTextIncr = "Set 'increase 20%' hotkey (Current: " + - (hotkeyStringIncr.isEmpty() ? QString("None") : hotkeyStringIncr) + ")"; - - QString hotkeyStringDecr = getShortcutString(HOTKEY_VOLUME_DECREASE); - QString hotkeyTextDecr = "Set 'decrease 20%' hotkey (Current: " + - (hotkeyStringDecr.isEmpty() ? QString("None") : hotkeyStringDecr) + ")"; - - QMenu menu; - QAction *actIncr = menu.addAction(hotkeyTextIncr); - QAction *actDecr = menu.addAction(hotkeyTextDecr); - QAction *action = menu.exec(ui->sl_volumeRemote->mapToGlobal(point)); - if (action == actIncr) - ts3Functions.requestHotkeyInputDialog(getPluginID(), HOTKEY_VOLUME_INCREASE, 0, this); - else if (action == actDecr) - ts3Functions.requestHotkeyInputDialog(getPluginID(), HOTKEY_VOLUME_DECREASE, 0, this); -} - -void MainWindow::ModelObserver::notify(ConfigModel &model, ConfigModel::notifications_e what, int data) -{ - switch(what) - { - case ConfigModel::NOTIFY_SET_ROWS: - p.ui->sb_rows->setValue(model.getRows()); - p.createButtons(); - break; - case ConfigModel::NOTIFY_SET_COLS: - p.ui->sb_cols->setValue(model.getCols()); - p.createButtons(); - break; - case ConfigModel::NOTIFY_SET_VOLUME_LOCAL: - if (p.ui->sl_volumeLocal->value() != model.getVolumeLocal()) - p.ui->sl_volumeLocal->setValue(model.getVolumeLocal()); - break; - case ConfigModel::NOTIFY_SET_VOLUME_REMOTE: - if (p.ui->sl_volumeRemote->value() != model.getVolumeRemote()) - p.ui->sl_volumeRemote->setValue(model.getVolumeRemote()); - break; - case ConfigModel::NOTIFY_SET_PLAYBACK_LOCAL: - if (p.ui->cb_mute_locally->isChecked() != !model.getPlaybackLocal()) - p.ui->cb_mute_locally->setChecked(!model.getPlaybackLocal()); - break; - case ConfigModel::NOTIFY_SET_SOUND: - p.updateButtonText(data); - break; - case ConfigModel::NOTIFY_SET_MUTE_MYSELF_DURING_PB: - if (p.ui->cb_mute_myself->isChecked() != model.getMuteMyselfDuringPb()) - p.ui->cb_mute_myself->setChecked(model.getMuteMyselfDuringPb()); - break; - case ConfigModel::NOTIFY_SET_WINDOW_SIZE: - { - QSize s = p.size(); - int w = 0, h = 0; - model.getWindowSize(&w, &h); - if(s.width() != w || s.height() != h) - p.resize(w, h); - } - break; - case ConfigModel::NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS: - if (p.ui->cb_show_hotkeys_on_buttons->isChecked() != model.getShowHotkeysOnButtons()) - p.ui->cb_show_hotkeys_on_buttons->setChecked(model.getShowHotkeysOnButtons()); - for(size_t i = 0; i < p.m_buttons.size(); i++) - p.updateButtonText(i); - break; - case ConfigModel::NOTIFY_SET_HOTKEYS_ENABLED: - if (p.ui->cb_disable_hotkeys->isChecked() == model.getHotkeysEnabled()) - p.ui->cb_disable_hotkeys->setChecked(!model.getHotkeysEnabled()); - break; - default: - break; - } -} +// src/MainWindow.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#include "common.h" + +#include +#include +#include +#include +#include +#include + +#include "MainWindow.h" +#include "ConfigModel.h" +#include "main.h" +#include "SoundSettings.h" +#include "ts3log.h" +#include "SpeechBubble.h" +#include "buildinfo.h" +#include "plugin.h" +#include "ExpandableSection.h" +#include "samples.h" +#include "SoundButton.h" + +#ifdef _WIN32 +#include "Windows.h" +#endif + +enum button_choices_e { + BC_CHOOSE = 0, + BC_ADVANCED, + BC_SET_HOTKEY, + BC_SET_COLOR, + BC_DELETE, +}; + + +MainWindow::MainWindow( ConfigModel *model, QWidget *parent /*= 0*/ ) : + QWidget(parent), + ui(new Ui::MainWindow), + m_model(model), + m_modelObserver(*this), + m_buttonBubble(nullptr) +{ + /* Ensure resources are loaded */ + Q_INIT_RESOURCE(qtres); + + m_pauseIcon = QIcon(":/icon/img/pausebutton_32.png"); + m_playIcon = QIcon(":/icon/img/playarrow_32.png"); + + ui->setupUi(this); + //setAttribute(Qt::WA_DeleteOnClose); + + createConfigButtons(); + + settingsSection = new ExpandableSection("Settings", 200, this); + settingsSection->setContentLayout(*ui->settingsWidget->layout()); + layout()->addWidget(settingsSection); + + configsSection = new ExpandableSection("Configurations", 200, this); + configsSection->setContentLayout(*ui->configsWidget->layout()); + layout()->addWidget(configsSection); + + QAction *actChooseFile = new QAction("Choose File", this); + actChooseFile->setData((int)BC_CHOOSE); + m_buttonContextMenu.addAction(actChooseFile); + + QAction *actAdvancedOpts = new QAction("Advanced Options", this); + actAdvancedOpts->setData((int)BC_ADVANCED); + m_buttonContextMenu.addAction(actAdvancedOpts); + + actSetHotkey = new QAction("Set hotkey", this); + actSetHotkey->setData((int)BC_SET_HOTKEY); + m_buttonContextMenu.addAction(actSetHotkey); + + QAction *actSetColor = new QAction("Set color", this); + actSetColor->setData((int)BC_SET_COLOR); + m_buttonContextMenu.addAction(actSetColor); + + QAction *actDeleteButton = new QAction("Make button great again (delete)", this); + actDeleteButton->setData((int)BC_DELETE); + m_buttonContextMenu.addAction(actDeleteButton); + + createButtons(); + + ui->b_stop->setContextMenuPolicy(Qt::CustomContextMenu); + ui->b_pause->setContextMenuPolicy(Qt::CustomContextMenu); + ui->cb_mute_locally->setContextMenuPolicy(Qt::CustomContextMenu); + ui->cb_mute_myself->setContextMenuPolicy(Qt::CustomContextMenu); + ui->sl_volumeLocal->setContextMenuPolicy(Qt::CustomContextMenu); + ui->sl_volumeRemote->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(ui->b_stop, SIGNAL(clicked()), this, SLOT(onClickedStop())); + connect(ui->b_stop, SIGNAL(customContextMenuRequested(const QPoint&)), this, + SLOT(showStopButtonContextMenu(const QPoint&))); + connect(ui->b_pause, SIGNAL(clicked()), this, SLOT(onButtonPausePressed())); + connect(ui->b_pause, SIGNAL(customContextMenuRequested(const QPoint&)), this, + SLOT(showPauseButtonContextMenu(const QPoint&))); + connect(ui->sl_volumeLocal, SIGNAL(valueChanged(int)), this, SLOT(onUpdateVolumeLocal(int))); + connect(ui->sl_volumeRemote, SIGNAL(valueChanged(int)), this, SLOT(onUpdateVolumeRemote(int))); + connect(ui->cb_mute_locally, SIGNAL(clicked(bool)), this, SLOT(onUpdateMuteLocally(bool))); + connect(ui->sb_rows, SIGNAL(valueChanged(int)), this, SLOT(onUpdateRows(int))); + connect(ui->sb_cols, SIGNAL(valueChanged(int)), this, SLOT(onUpdateCols(int))); + connect(ui->cb_mute_myself, SIGNAL(clicked(bool)), this, SLOT(onUpdateMuteMyself(bool))); + connect(ui->cb_show_hotkeys_on_buttons, SIGNAL(clicked(bool)), this, SLOT(onUpdateShowHotkeysOnButtons(bool))); + connect(ui->cb_disable_hotkeys, SIGNAL(clicked(bool)), this, SLOT(onUpdateHotkeysDisabled(bool))); + connect(ui->filterEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onFilterEditTextChanged(const QString&))); + connect(ui->cb_mute_locally, &QCheckBox::customContextMenuRequested, + [this](const QPoint &point) {this->showSetHotkeyMenu(HOTKEY_MUTE_ON_MY_CLIENT, ui->cb_mute_locally->mapToGlobal(point));}); + connect(ui->cb_mute_myself, &QCheckBox::customContextMenuRequested, + [this](const QPoint &point) {this->showSetHotkeyMenu(HOTKEY_MUTE_MYSELF, ui->cb_mute_myself->mapToGlobal(point));}); + connect(ui->sl_volumeLocal, &QSlider::customContextMenuRequested, this, &MainWindow::onVolumeSliderContextMenuLocal); + connect(ui->sl_volumeRemote, &QSlider::customContextMenuRequested, this, &MainWindow::onVolumeSliderContextMenuRemote); + + /* Load/Save Model */ + connect(ui->pushLoad, SIGNAL(released()), this, SLOT(onLoadModel())); + connect(ui->pushSave, SIGNAL(released()), this, SLOT(onSaveModel())); + + ui->playingIconLabel->hide(); + ui->playingLabel->setText(""); + playingIconTimer = new QTimer(this); + playingIconTimer->setInterval(150); + connect(playingIconTimer, SIGNAL(timeout()), this, SLOT(onPlayingIconTimer())); + + Sampler *sampler = sb_getSampler(); + connect(sampler, SIGNAL(onStartPlaying(bool, QString)), this, SLOT(onStartPlayingSound(bool, QString)), Qt::QueuedConnection); + connect(sampler, SIGNAL(onStopPlaying()), this, SLOT(onStopPlayingSound()), Qt::QueuedConnection); + connect(sampler, SIGNAL(onPausePlaying()), this, SLOT(onPausePlayingSound())); // No queued connection since signal is emitted from GUI Thread + connect(sampler, SIGNAL(onUnpausePlaying()), this, SLOT(onUnpausePlayingSound())); // No queued connection since signal is emitted from GUI Thread + + createBubbles(); + + m_model->addObserver(&m_modelObserver); + + /* Force configuration 0 */ + setConfiguration(0); + + ui->gridWidget->setStyleSheet( + "QPushButton {" + " padding: 4px;" + " border: 1px solid rgb(173, 173, 173);" + " color: black;" + " background-color: rgb(225, 225, 225);" + "}" + "QPushButton:disabled {" + " background-color: rgb(204, 204, 204);" + " border-color: rgb(191, 191, 191);" + " color: rgb(120, 120, 120);" + "}" + "QPushButton:hover {" + " background-color: rgb(228, 239, 249);" + " border-color: rgb(11, 123, 212);" + "}" + "QPushButton:pressed {" + " background-color: rgb(204, 228, 247);" + " border-color: rgb(0, 85, 155);" + "}"); +} + +void MainWindow::setConfiguration(int cfg) +{ + if (cfg < 0 || cfg >= NUM_CONFIGS) + { + logError("Invalid config id: %i", cfg); + return; + } + + m_configRadioButtons[cfg]->setChecked(true); + m_model->setConfiguration(cfg); + ui->labelStatus->setText(QString("Configuration %1").arg(cfg + 1)); +} + +void MainWindow::onSetConfig() +{ + QRadioButton *button = qobject_cast(sender()); + int configId = button->property("configId").toInt(); + setConfiguration(configId); +} + +void MainWindow::onConfigHotkey() +{ + QPushButton *button = qobject_cast(sender()); + int configId = button->property("configId").toInt(); + + char buf[16]; + sb_getInternalConfigHotkeyName(configId, buf); + ts3Functions.requestHotkeyInputDialog(getPluginID(), buf, configId, this); +} + + +MainWindow::~MainWindow() +{ + m_model->remObserver(&m_modelObserver); + delete ui; +} + +void MainWindow::onSaveModel() +{ + QString fn = QFileDialog::getSaveFileName(this, tr("Choose File to Save"), QString(), tr("Ini Files (*.ini)")); + if (fn.isNull()) + return; + m_model->writeConfig(fn); +} + +void MainWindow::onLoadModel() +{ + QString fn = QFileDialog::getOpenFileName(this, tr("Choose File to Load"), QString(), tr("Ini Files (*.ini)")); + if (fn.isNull()) + return; + m_model->readConfig(fn); +} + +void MainWindow::closeEvent(QCloseEvent *) +{ + m_model->setWindowSize(size().width(), size().height()); +} + +void MainWindow::onClickedPlay() +{ + QPushButton *button = dynamic_cast(sender()); + size_t buttonId = std::find_if(m_buttons.begin(), m_buttons.end(), [button](SoundButton *b){return b == button;}) - m_buttons.begin(); + + playSound(buttonId); +} + + +void MainWindow::onClickedStop() +{ + sb_stopPlayback(); +} + + +void MainWindow::onUpdateVolumeLocal(int val) +{ + m_model->setVolumeLocal(val); +} + + +void MainWindow::onUpdateVolumeRemote(int val) +{ + m_model->setVolumeRemote(val); +} + + +void MainWindow::onUpdateMuteLocally(bool val) +{ + m_model->setPlaybackLocal(!val); +} + + +void MainWindow::onUpdateCols(int val) +{ + m_model->setCols(val); +} + + +void MainWindow::onUpdateRows(int val) +{ + m_model->setRows(val); +} + + +void MainWindow::onUpdateMuteMyself(bool val) +{ + m_model->setMuteMyselfDuringPb(val); +} + + +void MainWindow::createButtons() +{ + for(SoundButton *button : m_buttons) + delete button; + m_buttons.clear(); + + int numRows = m_model->getRows(); + int numCols = m_model->getCols(); + + for(int i = 0; i < numRows; i++) + { + for(int j = 0; j < numCols; j++) + { + SoundButton *elem = new SoundButton(this); + elem->setProperty("buttonId", (int)m_buttons.size()); + elem->setText("(no file)"); + elem->setEnabled(true); + QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Expanding); + policy.setRetainSizeWhenHidden(true); + elem->setSizePolicy(policy); + ui->gridLayout->addWidget(elem, i, j); + connect(elem, SIGNAL(clicked()), this, SLOT(onClickedPlay())); + elem->setContextMenuPolicy(Qt::CustomContextMenu); + connect(elem, SIGNAL(customContextMenuRequested(const QPoint&)), this, + SLOT(showButtonContextMenu(const QPoint&))); + connect(elem, SIGNAL(fileDropped(QList)), this, SLOT(onButtonFileDropped(QList))); + connect(elem, SIGNAL(buttonDropped(SoundButton*)), this, SLOT(onButtonDroppedOnButton(SoundButton*))); + + elem->updateGeometry(); + m_buttons.push_back(elem); + } + } + + for(int i = 0; i < (int)m_buttons.size(); i++) + updateButtonText(i); + + if(m_buttonBubble) + m_buttonBubble->attachTo(m_buttons[0]); +} + + +void MainWindow::createConfigButtons() +{ + for (int i = 0; i < NUM_CONFIGS; i++) + { + m_configRadioButtons[i] = new QRadioButton(this); + m_configRadioButtons[i]->setText(QString("Config %1").arg(i + 1)); + m_configRadioButtons[i]->setProperty("configId", i); + m_configRadioButtons[i]->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + ui->configsGrid->addWidget(m_configRadioButtons[i], 0, i, Qt::AlignCenter); + connect(m_configRadioButtons[i], SIGNAL(clicked()), this, SLOT(onSetConfig())); + + m_configHotkeyButtons[i] = new QPushButton(this); + m_configHotkeyButtons[i]->setText("Set hotkey"); + m_configHotkeyButtons[i]->setProperty("configId", i); + m_configHotkeyButtons[i]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + m_configHotkeyButtons[i]->setToolTipDuration(0); + m_configHotkeyButtons[i]->setToolTip(getConfigShortcutString(i)); + ui->configsGrid->addWidget(m_configHotkeyButtons[i], 1, i, Qt::AlignCenter); + connect(m_configHotkeyButtons[i], SIGNAL(clicked()), this, SLOT(onConfigHotkey())); + + ui->configsWidget->updateGeometry(); + } +} + + +bool MainWindow::hotkeysEnabled() +{ + return m_model->getHotkeysEnabled(); +} + +void MainWindow::updateButtonText(int i) +{ + if(i >= (int)m_buttons.size()) + return; + + QString text; + const SoundInfo *info = m_model->getSoundInfo(i); + if (info && !info->filename.isEmpty()) + { + if (!info->customText.isEmpty()) + text = unescapeCustomText(info->customText); + else + text = QFileInfo(info->filename).baseName(); + } + else + text = "(no file)"; + + if (m_model->getShowHotkeysOnButtons()) + { + QString shortcut = getShortcutString(i); + if (shortcut.length() > 0) + text = text + "\n" + shortcut; + } + m_buttons[i]->setText(text); + m_buttons[i]->setBackgroundColor(info ? info->customColor : QColor(0, 0, 0, 0)); +} + + +void MainWindow::showButtonContextMenu( const QPoint &point ) +{ + QPushButton *button = dynamic_cast(sender()); + size_t buttonId = std::find_if(m_buttons.begin(), m_buttons.end(), [button](SoundButton *b){return b == button;}) - m_buttons.begin(); + + QString shortcutName = getShortcutString(buttonId); + QString hotkeyText = "Set hotkey (Current: " + + (shortcutName.isEmpty() ? QString("None") : shortcutName) + ")"; + actSetHotkey->setText(hotkeyText); + + QPoint globalPos = m_buttons[buttonId]->mapToGlobal(point); + QAction *action = m_buttonContextMenu.exec(globalPos); + if(action) + { + bool ok = false; + int choice = action->data().toInt(&ok); + if(ok) + { + switch(choice) + { + case BC_CHOOSE: + chooseFile(buttonId); + break; + case BC_ADVANCED: + openAdvanced(buttonId); + break; + case BC_SET_HOTKEY: + openHotkeySetDialog(buttonId); + break; + case BC_SET_COLOR: + openButtonColorDialog(buttonId); + break; + case BC_DELETE: + deleteButton(buttonId); + break; + default: break; + } + } + else + logError("Invalid user data in context menu"); + } +} + + +void MainWindow::setPlayingLabelIcon(int index) +{ + ui->playingIconLabel->setPixmap(QPixmap(QString(":/icon/img/speaker_icon_%1_64.png").arg(index))); +} + + + +void MainWindow::playSound( size_t buttonId ) +{ + const SoundInfo *info = m_model->getSoundInfo(buttonId); + if(info) + sb_playFile(*info); +} + + +void MainWindow::chooseFile( size_t buttonId ) +{ + QString filePath = m_model->getFileName(buttonId); + QString fn = QFileDialog::getOpenFileName(this, tr("Choose File"), filePath, tr("Files (*.*)")); + if (fn.isNull()) + return; + setButtonFile(buttonId, fn); + +} + + +void MainWindow::openAdvanced( size_t buttonId ) +{ + const SoundInfo *buttonInfo = m_model->getSoundInfo(buttonId); + SoundInfo defaultInfo; + const SoundInfo &info = buttonInfo ? *buttonInfo : defaultInfo; + SoundSettingsQt dlg(info, buttonId, this); + dlg.setWindowTitle(QString("Sound %1 Settings").arg(QString::number(buttonId + 1))); + if(dlg.exec() == QDialog::Accepted) + m_model->setSoundInfo(buttonId, dlg.getSoundInfo()); +} + + +void MainWindow::deleteButton(size_t buttonId) +{ + const SoundInfo *info = m_model->getSoundInfo(buttonId); + if (info) + m_model->setSoundInfo(buttonId, SoundInfo()); +} + + +void MainWindow::createBubbles() +{ + if(m_model->getBubbleButtonsBuild() == 0) + { + m_buttonBubble = new SpeechBubble(this); + m_buttonBubble->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + m_buttonBubble->setFixedSize(180, 80); + m_buttonBubble->setText("Right click to choose sound file\nor open advanced options."); + m_buttonBubble->attachTo(m_buttons[0]); + connect(m_buttonBubble, SIGNAL(closePressed()), this, SLOT(onButtonBubbleFinished())); + } + + if(m_model->getBubbleStopBuild() == 0) + { + SpeechBubble *stopBubble = new SpeechBubble(this); + stopBubble->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + stopBubble->setFixedSize(180, 60); + stopBubble->setText("Stop the currently playing sound."); + stopBubble->attachTo(ui->b_stop); + connect(stopBubble, SIGNAL(closePressed()), this, SLOT(onStopBubbleFinished())); + } + + if(m_model->getBubbleColsBuild() == 0) + { + settingsSection->setExpanded(true); + SpeechBubble *colsBubble = new SpeechBubble(this); + colsBubble->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + colsBubble->setFixedSize(180, 80); + colsBubble->setText("Change the number of buttons\non the soundboard."); + colsBubble->attachTo(ui->sb_cols); + connect(colsBubble, SIGNAL(closePressed()), this, SLOT(onColsBubbleFinished())); + } +} + + +void MainWindow::onStopBubbleFinished() +{ + m_model->setBubbleStopBuild(buildinfo_getBuildNumber()); +} + + +void MainWindow::onButtonBubbleFinished() +{ + m_model->setBubbleButtonsBuild(buildinfo_getBuildNumber()); +} + + +void MainWindow::onColsBubbleFinished() +{ + m_model->setBubbleColsBuild(buildinfo_getBuildNumber()); +} + + +void MainWindow::showStopButtonContextMenu(const QPoint &point) +{ + showSetHotkeyMenu(HOTKEY_STOP_ALL, ui->b_stop->mapToGlobal(point)); +} + + +void MainWindow::showPauseButtonContextMenu(const QPoint &point) +{ + showSetHotkeyMenu(HOTKEY_PAUSE_ALL, ui->b_pause->mapToGlobal(point)); +} + + +void MainWindow::showSetHotkeyMenu(const char *hotkeyName, const QPoint &point) +{ + QString hotkeyString = getShortcutString(hotkeyName); + QString hotkeyText = "Set hotkey (Current: " + + (hotkeyString.isEmpty() ? QString("None") : hotkeyString) + ")"; + + QMenu menu; + menu.addAction(hotkeyText); + QAction *action = menu.exec(point); + if (action) + ts3Functions.requestHotkeyInputDialog(getPluginID(), hotkeyName, 0, this); +} + + +void MainWindow::onStartPlayingSound(bool preview, QString filename) +{ + QFileInfo info(filename); + ui->playingLabel->setText(info.fileName()); + setPlayingLabelIcon(0); + ui->playingIconLabel->show(); + playingIconIndex = 1; + playingIconTimer->start(); + ui->b_stop->setEnabled(true); + ui->b_pause->setEnabled(true); + ui->b_pause->setIcon(m_pauseIcon); +} + + +void MainWindow::onStopPlayingSound() +{ + playingIconTimer->stop(); + ui->playingLabel->setText(""); + ui->playingIconLabel->hide(); + ui->b_pause->setIcon(m_pauseIcon); +} + + +void MainWindow::onPausePlayingSound() +{ + playingIconTimer->stop(); + ui->b_pause->setIcon(m_playIcon); +} + + +void MainWindow::onUnpausePlayingSound() +{ + playingIconTimer->start(); + ui->b_pause->setIcon(m_pauseIcon); +} + + +void MainWindow::onPlayingIconTimer() +{ + setPlayingLabelIcon(playingIconIndex); + ++playingIconIndex %= 4; +} + + +void MainWindow::openHotkeySetDialog(size_t buttonId) +{ + openHotkeySetDialog(buttonId, this); +} + + +void MainWindow::openButtonColorDialog(size_t buttonId) +{ + QColorDialog dialog; + const SoundInfo *pinfo = m_model->getSoundInfo(buttonId); + SoundInfo info = pinfo ? *pinfo : SoundInfo(); + dialog.setCurrentColor(info.customColor); + dialog.setWindowTitle("Choose button color"); + if (dialog.exec()) + { + info.customColor = dialog.currentColor(); + m_model->setSoundInfo(buttonId, info); + } +} + + +QString MainWindow::unescapeCustomText(const QString &text) +{ + QString cpy = text; + return cpy.replace("\\n", "\n"); +} + + +void MainWindow::openHotkeySetDialog( size_t buttonId, QWidget *parent ) +{ + char intName[16]; + sb_getInternalHotkeyName((int)buttonId, intName); + ts3Functions.requestHotkeyInputDialog(getPluginID(), intName, 0, parent); +} + + +QString MainWindow::getShortcutString(const char *internalName) +{ + std::vector name(128, 0); + char *namePtr = name.data(); + unsigned int res = ts3Functions.getHotkeyFromKeyword( + getPluginID(), &internalName, &namePtr, 1, 128); + QString str = res == 0 ? QString(name.data()) : QString(); + return str; +} + + + +QString MainWindow::getShortcutString(size_t buttonId) +{ + char intName[16]; + sb_getInternalHotkeyName((int)buttonId, intName); + return getShortcutString(intName); +} + +QString MainWindow::getConfigShortcutString(int cfg) +{ + char buf[16]; + sb_getInternalConfigHotkeyName(cfg, buf); + QString shortcut = getShortcutString(buf); + if (!shortcut.isEmpty()) + return shortcut; + + return QString("no hotkey"); +} + + +void MainWindow::onHotkeyRecordedEvent(const char *keyword, const char *key) +{ + QString sKey = key; + + int configId = -1; + if (sscanf(keyword, "config_%i", &configId) == 1) + { + if (configId >= 0 && configId < NUM_CONFIGS) + m_configHotkeyButtons[configId]->setToolTip(sKey); + else + logError("Invalid hotkey keyword: %s", keyword); + } + else + { + QString sKeyword = keyword; + + emit hotkeyRecordedEvent(sKey, sKeyword); + + if (m_model->getShowHotkeysOnButtons()) + for (size_t i = 0; i < m_buttons.size(); i++) + updateButtonText(i); + } +} + + +void MainWindow::onUpdateShowHotkeysOnButtons(bool val) +{ + m_model->setShowHotkeysOnButtons(val); +} + + +void MainWindow::onUpdateHotkeysDisabled(bool val) +{ + m_model->setHotkeysEnabled(!val); +} + + +void MainWindow::showEvent(QShowEvent *evt) +{ + QWidget::showEvent(evt); + + for(size_t i = 0; i < m_buttons.size(); i++) + updateButtonText(i); +} + + +void MainWindow::onButtonFileDropped(const QList &urls) +{ + int buttonId = sender()->property("buttonId").toInt(); + int buttonNr = 0; + + for(int i = 0; i < urls.size(); i++) + { + if (buttonNr == 1) + { + QMessageBox msgbox(QMessageBox::Icon::Question, "Fill buttons?", + "You dropped multiple files. Consecutively apply them to the buttons following the one you dropped your files on?", + QMessageBox::Yes | QMessageBox::No, this); + if (msgbox.exec() == QMessageBox::No) + break; + } + + if (urls[i].isLocalFile()) + { + QFileInfo info(urls[i].toLocalFile()); + if (info.isFile()) + { + setButtonFile(buttonId + buttonNr, urls[i].toLocalFile(), buttonNr == 0); + ++buttonNr; + } + else if (buttonNr == 0) + { + QMessageBox msgBox(QMessageBox::Icon::Critical, "Unsupported drop type", "Some things could not be dropped here :(", QMessageBox::Ok, this); + msgBox.exec(); + } + } + } +} + + +void MainWindow::setButtonFile(size_t buttonId, const QString &fn, bool askForDisablingCrop) +{ + const SoundInfo *info = m_model->getSoundInfo(buttonId); + if (askForDisablingCrop && info && info->cropEnabled && info->filename != fn) + { + QMessageBox mb(QMessageBox::Question, "Keep crop settings?", + "You selected a new file for a button that has 'crop sound' enabled.", QMessageBox::NoButton, this); + QPushButton *btnDisable = mb.addButton("Disable cropping (recommended)", QMessageBox::YesRole); + QPushButton *btnKeep = mb.addButton("Keep old crop settings", QMessageBox::NoRole); + mb.setDefaultButton(btnDisable); + mb.exec(); + if (mb.clickedButton() != btnKeep) + { + SoundInfo newInfo(*info); + newInfo.cropEnabled = false; + m_model->setSoundInfo(buttonId, newInfo); + } + } + m_model->setFileName(buttonId, fn); +} + + +void MainWindow::onButtonPausePressed() +{ + sb_pauseButtonPressed(); +} + + +void MainWindow::onButtonDroppedOnButton(SoundButton *button) +{ + SoundButton *btn0 = button; + SoundButton *btn1 = qobject_cast(sender()); + int bid0 = btn0->property("buttonId").toInt(); + int bid1 = btn1->property("buttonId").toInt(); + const SoundInfo *info0 = m_model->getSoundInfo(bid0); + const SoundInfo *info1 = m_model->getSoundInfo(bid1); + + // Copy sound info + SoundInfo infoCopy0; + SoundInfo infoCopy1; + if (info0) + infoCopy0 = *info0; + if (info1) + infoCopy1 = *info1; + + // And switch em + m_model->setSoundInfo(bid0, infoCopy1); + m_model->setSoundInfo(bid1, infoCopy0); + + // Switch button position and then animate the buttons to slide into place + const int animDuration = 300; + QPropertyAnimation *anim0 = new QPropertyAnimation(btn0, "pos"); + anim0->setStartValue(btn1->pos()); + anim0->setEndValue(btn0->pos()); + anim0->setDuration(animDuration); + + QPropertyAnimation *anim1 = new QPropertyAnimation(btn1, "pos"); + anim1->setStartValue(btn0->pos()); + anim1->setEndValue(btn1->pos()); + anim1->setDuration(animDuration); + + anim0->start(); + anim1->start(); + btn0->raise(); + btn1->raise(); +} + + +void MainWindow::onFilterEditTextChanged(const QString &text) +{ + QString filter = text.trimmed(); + for (auto &button : m_buttons) + { + int buttonId = button->property("buttonId").toInt(); + const SoundInfo *info = m_model->getSoundInfo(buttonId); + bool hasFile = info && !info->filename.isEmpty(); + QString buttonText = button->text(); + bool pass = filter.length() == 0 || (hasFile && buttonText.contains(filter, Qt::CaseInsensitive)); + button->setVisible(pass); + } +} + + +void MainWindow::onVolumeSliderContextMenuLocal(const QPoint &point) +{ + QString hotkeyStringIncr = getShortcutString(HOTKEY_VOLUME_INCREASE); + QString hotkeyTextIncr = "Set 'increase 20%' hotkey (Current: " + + (hotkeyStringIncr.isEmpty() ? QString("None") : hotkeyStringIncr) + ")"; + + QString hotkeyStringDecr = getShortcutString(HOTKEY_VOLUME_DECREASE); + QString hotkeyTextDecr = "Set 'decrease 20%' hotkey (Current: " + + (hotkeyStringDecr.isEmpty() ? QString("None") : hotkeyStringDecr) + ")"; + + QMenu menu; + QAction *actIncr = menu.addAction(hotkeyTextIncr); + QAction *actDecr = menu.addAction(hotkeyTextDecr); + QAction *action = menu.exec(ui->sl_volumeLocal->mapToGlobal(point)); + if (action == actIncr) + ts3Functions.requestHotkeyInputDialog(getPluginID(), HOTKEY_VOLUME_INCREASE, 0, this); + else if (action == actDecr) + ts3Functions.requestHotkeyInputDialog(getPluginID(), HOTKEY_VOLUME_DECREASE, 0, this); +} + + +void MainWindow::onVolumeSliderContextMenuRemote(const QPoint &point) +{ + QString hotkeyStringIncr = getShortcutString(HOTKEY_VOLUME_INCREASE); + QString hotkeyTextIncr = "Set 'increase 20%' hotkey (Current: " + + (hotkeyStringIncr.isEmpty() ? QString("None") : hotkeyStringIncr) + ")"; + + QString hotkeyStringDecr = getShortcutString(HOTKEY_VOLUME_DECREASE); + QString hotkeyTextDecr = "Set 'decrease 20%' hotkey (Current: " + + (hotkeyStringDecr.isEmpty() ? QString("None") : hotkeyStringDecr) + ")"; + + QMenu menu; + QAction *actIncr = menu.addAction(hotkeyTextIncr); + QAction *actDecr = menu.addAction(hotkeyTextDecr); + QAction *action = menu.exec(ui->sl_volumeRemote->mapToGlobal(point)); + if (action == actIncr) + ts3Functions.requestHotkeyInputDialog(getPluginID(), HOTKEY_VOLUME_INCREASE, 0, this); + else if (action == actDecr) + ts3Functions.requestHotkeyInputDialog(getPluginID(), HOTKEY_VOLUME_DECREASE, 0, this); +} + +void MainWindow::ModelObserver::notify(ConfigModel &model, ConfigModel::notifications_e what, int data) +{ + switch(what) + { + case ConfigModel::NOTIFY_SET_ROWS: + p.ui->sb_rows->setValue(model.getRows()); + p.createButtons(); + break; + case ConfigModel::NOTIFY_SET_COLS: + p.ui->sb_cols->setValue(model.getCols()); + p.createButtons(); + break; + case ConfigModel::NOTIFY_SET_VOLUME_LOCAL: + if (p.ui->sl_volumeLocal->value() != model.getVolumeLocal()) + p.ui->sl_volumeLocal->setValue(model.getVolumeLocal()); + break; + case ConfigModel::NOTIFY_SET_VOLUME_REMOTE: + if (p.ui->sl_volumeRemote->value() != model.getVolumeRemote()) + p.ui->sl_volumeRemote->setValue(model.getVolumeRemote()); + break; + case ConfigModel::NOTIFY_SET_PLAYBACK_LOCAL: + if (p.ui->cb_mute_locally->isChecked() != !model.getPlaybackLocal()) + p.ui->cb_mute_locally->setChecked(!model.getPlaybackLocal()); + break; + case ConfigModel::NOTIFY_SET_SOUND: + p.updateButtonText(data); + break; + case ConfigModel::NOTIFY_SET_MUTE_MYSELF_DURING_PB: + if (p.ui->cb_mute_myself->isChecked() != model.getMuteMyselfDuringPb()) + p.ui->cb_mute_myself->setChecked(model.getMuteMyselfDuringPb()); + break; + case ConfigModel::NOTIFY_SET_WINDOW_SIZE: + { + QSize s = p.size(); + int w = 0, h = 0; + model.getWindowSize(&w, &h); + if(s.width() != w || s.height() != h) + p.resize(w, h); + } + break; + case ConfigModel::NOTIFY_SET_SHOW_HOTKEYS_ON_BUTTONS: + if (p.ui->cb_show_hotkeys_on_buttons->isChecked() != model.getShowHotkeysOnButtons()) + p.ui->cb_show_hotkeys_on_buttons->setChecked(model.getShowHotkeysOnButtons()); + for(size_t i = 0; i < p.m_buttons.size(); i++) + p.updateButtonText(i); + break; + case ConfigModel::NOTIFY_SET_HOTKEYS_ENABLED: + if (p.ui->cb_disable_hotkeys->isChecked() == model.getHotkeysEnabled()) + p.ui->cb_disable_hotkeys->setChecked(!model.getHotkeysEnabled()); + break; + default: + break; + } +} diff --git a/src/MainWindow.h b/src/MainWindow.h index 38eb387..99cf246 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -1,141 +1,141 @@ -// src/MainWindow.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -#include "ui_MainWindow.h" -#include "ConfigModel.h" - -class SpeechBubble; -class ExpandableSection; -class SoundButton; - -namespace Ui { - class MainWindow; -} - -class MainWindow : public QWidget -{ - Q_OBJECT - -public: - explicit MainWindow(ConfigModel *model, QWidget *parent = 0); - - void createBubbles(); - - virtual ~MainWindow(); - - static QString getShortcutString(const char *internalName); - static QString getShortcutString(size_t buttonId); - static QString getConfigShortcutString(int cfg); - static void openHotkeySetDialog(size_t buttonId, QWidget *parent); - void onHotkeyRecordedEvent(const char *keyword, const char *key); - - void setConfiguration(int cfg); - bool hotkeysEnabled(); - -protected: - virtual void closeEvent(QCloseEvent * evt) override; - virtual void showEvent(QShowEvent *evt) override; - -private slots: - void onClickedPlay(); - void onClickedStop(); - void onUpdateVolumeLocal(int val); - void onUpdateVolumeRemote(int val); - void onUpdateMuteLocally(bool val); - void onUpdateCols(int val); - void onUpdateRows(int val); - void onUpdateMuteMyself(bool val); - void showButtonContextMenu(const QPoint &point); - void onStopBubbleFinished(); - void onButtonBubbleFinished(); - void onColsBubbleFinished(); - void showStopButtonContextMenu(const QPoint &point); - void showPauseButtonContextMenu(const QPoint &point); - void onStartPlayingSound(bool preview, QString filename); - void onStopPlayingSound(); - void onPausePlayingSound(); - void onUnpausePlayingSound(); - void onPlayingIconTimer(); - void onUpdateShowHotkeysOnButtons(bool val); - void onUpdateHotkeysDisabled(bool val); - void onButtonFileDropped(const QList &urls); - void onButtonPausePressed(); - void onButtonDroppedOnButton(SoundButton *button); - void onFilterEditTextChanged(const QString &filter); - void onVolumeSliderContextMenuLocal(const QPoint &point); - void onVolumeSliderContextMenuRemote(const QPoint &point); - - void onSetConfig(); - void onConfigHotkey(); - - - void onSaveModel(); - void onLoadModel(); - -signals: - void hotkeyRecordedEvent(QString keyword, QString key); - -private: - void showSetHotkeyMenu(const char *hotkeyName, const QPoint &point); - void setPlayingLabelIcon(int index); - void playSound(size_t buttonId); - void chooseFile(size_t buttonId); - - void setButtonFile(size_t buttonId, const QString &fn, bool askForDisablingCrop = true); - - void openAdvanced(size_t buttonId); - void deleteButton(size_t buttonId); - void createButtons(); - void createConfigButtons(); - void updateButtonText(int i); - void openHotkeySetDialog(size_t buttonId); - void openButtonColorDialog(size_t buttonId); - QString unescapeCustomText(const QString &text); - - class ModelObserver : public ConfigModel::Observer - { - public: - ModelObserver(MainWindow &parent) : p(parent) {} - void notify(ConfigModel &model, ConfigModel::notifications_e what, int data) override; - private: - MainWindow &p; - }; - - Ui::MainWindow *ui; - std::vector m_buttons; - ConfigModel *m_model; - QBoxLayout *m_configArea; - ModelObserver m_modelObserver; - QMenu m_buttonContextMenu; - QPointer m_buttonBubble; - QAction *actSetHotkey; - ExpandableSection *settingsSection; - ExpandableSection *configsSection; - QTimer *playingIconTimer; - int playingIconIndex; - QIcon m_pauseIcon; - QIcon m_playIcon; - std::array m_configRadioButtons; - std::array m_configHotkeyButtons; -}; - +// src/MainWindow.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "ui_MainWindow.h" +#include "ConfigModel.h" + +class SpeechBubble; +class ExpandableSection; +class SoundButton; + +namespace Ui { + class MainWindow; +} + +class MainWindow : public QWidget +{ + Q_OBJECT + +public: + explicit MainWindow(ConfigModel *model, QWidget *parent = 0); + + void createBubbles(); + + virtual ~MainWindow(); + + static QString getShortcutString(const char *internalName); + static QString getShortcutString(size_t buttonId); + static QString getConfigShortcutString(int cfg); + static void openHotkeySetDialog(size_t buttonId, QWidget *parent); + void onHotkeyRecordedEvent(const char *keyword, const char *key); + + void setConfiguration(int cfg); + bool hotkeysEnabled(); + +protected: + virtual void closeEvent(QCloseEvent * evt) override; + virtual void showEvent(QShowEvent *evt) override; + +private slots: + void onClickedPlay(); + void onClickedStop(); + void onUpdateVolumeLocal(int val); + void onUpdateVolumeRemote(int val); + void onUpdateMuteLocally(bool val); + void onUpdateCols(int val); + void onUpdateRows(int val); + void onUpdateMuteMyself(bool val); + void showButtonContextMenu(const QPoint &point); + void onStopBubbleFinished(); + void onButtonBubbleFinished(); + void onColsBubbleFinished(); + void showStopButtonContextMenu(const QPoint &point); + void showPauseButtonContextMenu(const QPoint &point); + void onStartPlayingSound(bool preview, QString filename); + void onStopPlayingSound(); + void onPausePlayingSound(); + void onUnpausePlayingSound(); + void onPlayingIconTimer(); + void onUpdateShowHotkeysOnButtons(bool val); + void onUpdateHotkeysDisabled(bool val); + void onButtonFileDropped(const QList &urls); + void onButtonPausePressed(); + void onButtonDroppedOnButton(SoundButton *button); + void onFilterEditTextChanged(const QString &filter); + void onVolumeSliderContextMenuLocal(const QPoint &point); + void onVolumeSliderContextMenuRemote(const QPoint &point); + + void onSetConfig(); + void onConfigHotkey(); + + + void onSaveModel(); + void onLoadModel(); + +signals: + void hotkeyRecordedEvent(QString keyword, QString key); + +private: + void showSetHotkeyMenu(const char *hotkeyName, const QPoint &point); + void setPlayingLabelIcon(int index); + void playSound(size_t buttonId); + void chooseFile(size_t buttonId); + + void setButtonFile(size_t buttonId, const QString &fn, bool askForDisablingCrop = true); + + void openAdvanced(size_t buttonId); + void deleteButton(size_t buttonId); + void createButtons(); + void createConfigButtons(); + void updateButtonText(int i); + void openHotkeySetDialog(size_t buttonId); + void openButtonColorDialog(size_t buttonId); + QString unescapeCustomText(const QString &text); + + class ModelObserver : public ConfigModel::Observer + { + public: + ModelObserver(MainWindow &parent) : p(parent) {} + void notify(ConfigModel &model, ConfigModel::notifications_e what, int data) override; + private: + MainWindow &p; + }; + + Ui::MainWindow *ui; + std::vector m_buttons; + ConfigModel *m_model; + QBoxLayout *m_configArea; + ModelObserver m_modelObserver; + QMenu m_buttonContextMenu; + QPointer m_buttonBubble; + QAction *actSetHotkey; + ExpandableSection *settingsSection; + ExpandableSection *configsSection; + QTimer *playingIconTimer; + int playingIconIndex; + QIcon m_pauseIcon; + QIcon m_playIcon; + std::array m_configRadioButtons; + std::array m_configHotkeyButtons; +}; + diff --git a/src/MainWindow.ui b/src/MainWindow.ui index a24249e..2533302 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -1,463 +1,463 @@ - - - MainWindow - - - Qt::NonModal - - - - 0 - 0 - 600 - 316 - - - - - 600 - 0 - - - - RP Soundboard - - - - :/icon/img/rpmb_icon_64.png:/icon/img/rpmb_icon_64.png - - - - - - - 0 - - - - - - 3 - - - 3 - - - - - - - - QFrame::HLine - - - QFrame::Raised - - - - - - - - 6 - - - 3 - - - 3 - - - - - 1 - - - - - true - - - - 0 - 0 - - - - - - - - :/icon/img/stoparrow_32.png:/icon/img/stoparrow_32.png - - - - - - - true - - - - 0 - 0 - - - - - - - - :/icon/img/playarrow_32.png:/icon/img/playarrow_32.png - - - false - - - - - - - - - - 0 - 0 - - - - - 23 - 23 - - - - - - - :/icon/img/speaker_icon_3_64.png - - - true - - - - - - - - 0 - 0 - - - - playing label - - - - - - - - 50 - 16777215 - - - - filter - - - - - - - TextLabel - - - - - - - - - - - 15 - - - 0 - - - 3 - - - 3 - - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - Volume Remote: - - - - - - - 100 - - - 10 - - - Qt::Horizontal - - - QSlider::NoTicks - - - 0 - - - - - - - - - - - Volume Local: - - - - - - - 100 - - - 10 - - - Qt::Horizontal - - - QSlider::NoTicks - - - 0 - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 0 - - - - - - - - 2 - - - QLayout::SetDefaultConstraint - - - - - - 0 - 0 - - - - Mute on my client - - - - - - - Mute myself during playback - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 0 - - - - - - - - 3 - - - - - - - Rows: - - - - - - - 1 - - - 50 - - - - - - - Columns: - - - - - - - 1 - - - 50 - - - - - - - - - Show hotkeys on buttons - - - - - - - Disable hotkeys - - - true - - - - - - - - - - - - - 0 - 0 - - - - - 9 - - - 3 - - - 9 - - - 3 - - - - - - - - QFrame::VLine - - - QFrame::Sunken - - - - - - - - - Export current configurations - - - - - - - Import current configurations - - - - - - - - - - - - - - - + + + MainWindow + + + Qt::NonModal + + + + 0 + 0 + 600 + 316 + + + + + 600 + 0 + + + + RP Soundboard + + + + :/icon/img/rpmb_icon_64.png:/icon/img/rpmb_icon_64.png + + + + + + + 0 + + + + + + 3 + + + 3 + + + + + + + + QFrame::HLine + + + QFrame::Raised + + + + + + + + 6 + + + 3 + + + 3 + + + + + 1 + + + + + true + + + + 0 + 0 + + + + + + + + :/icon/img/stoparrow_32.png:/icon/img/stoparrow_32.png + + + + + + + true + + + + 0 + 0 + + + + + + + + :/icon/img/playarrow_32.png:/icon/img/playarrow_32.png + + + false + + + + + + + + + + 0 + 0 + + + + + 23 + 23 + + + + + + + :/icon/img/speaker_icon_3_64.png + + + true + + + + + + + + 0 + 0 + + + + playing label + + + + + + + + 50 + 16777215 + + + + filter + + + + + + + TextLabel + + + + + + + + + + + 15 + + + 0 + + + 3 + + + 3 + + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Volume Remote: + + + + + + + 100 + + + 10 + + + Qt::Horizontal + + + QSlider::NoTicks + + + 0 + + + + + + + + + + + Volume Local: + + + + + + + 100 + + + 10 + + + Qt::Horizontal + + + QSlider::NoTicks + + + 0 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 0 + + + + + + + + 2 + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + Mute on my client + + + + + + + Mute myself during playback + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 0 + + + + + + + + 3 + + + + + + + Rows: + + + + + + + 1 + + + 50 + + + + + + + Columns: + + + + + + + 1 + + + 50 + + + + + + + + + Show hotkeys on buttons + + + + + + + Disable hotkeys + + + true + + + + + + + + + + + + + 0 + 0 + + + + + 9 + + + 3 + + + 9 + + + 3 + + + + + + + + QFrame::VLine + + + QFrame::Sunken + + + + + + + + + Export current configurations + + + + + + + Import current configurations + + + + + + + + + + + + + + + diff --git a/src/SampleBuffer.cpp b/src/SampleBuffer.cpp index d063fca..deee27f 100644 --- a/src/SampleBuffer.cpp +++ b/src/SampleBuffer.cpp @@ -1,51 +1,51 @@ -// src/SampleBuffer.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#include -#include -#include "SampleBuffer.h" - - -SampleBuffer::SampleBuffer( int channels, size_t maxSize /*= 0*/ ) : - m_channels(channels), - m_maxSize(maxSize), - m_cbProd(nullptr), - m_cbCons(nullptr) -{ - -} - - -void SampleBuffer::produce( const short *samples, int count ) -{ - assert(!m_mutex.try_lock() && "Mutex not locked"); - if(m_maxSize == 0 || avail() < (int)m_maxSize) - { - m_buf.insert(m_buf.end(), samples, samples + (count * m_channels)); - if(m_cbProd) - m_cbProd->onProduceSamples(samples, count, this); - } -} - - -int SampleBuffer::consume( short *samples, int maxCount, bool eraseConsumed ) -{ - assert(!m_mutex.try_lock() && "Mutex not locked"); - int count = std::min(avail(), maxCount); - size_t shorts = count * m_channels; - if(samples) - memcpy(samples, m_buf.data(), shorts * 2); - if(eraseConsumed) - m_buf.erase(m_buf.begin(), m_buf.begin() + count * m_channels); - if(m_cbCons) - m_cbCons->onConsumeSamples(samples, count, this); - return count; -} - - - +// src/SampleBuffer.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#include +#include +#include "SampleBuffer.h" + + +SampleBuffer::SampleBuffer( int channels, size_t maxSize /*= 0*/ ) : + m_channels(channels), + m_maxSize(maxSize), + m_cbProd(nullptr), + m_cbCons(nullptr) +{ + +} + + +void SampleBuffer::produce( const short *samples, int count ) +{ + assert(!m_mutex.try_lock() && "Mutex not locked"); + if(m_maxSize == 0 || avail() < (int)m_maxSize) + { + m_buf.insert(m_buf.end(), samples, samples + (count * m_channels)); + if(m_cbProd) + m_cbProd->onProduceSamples(samples, count, this); + } +} + + +int SampleBuffer::consume( short *samples, int maxCount, bool eraseConsumed ) +{ + assert(!m_mutex.try_lock() && "Mutex not locked"); + int count = std::min(avail(), maxCount); + size_t shorts = count * m_channels; + if(samples) + memcpy(samples, m_buf.data(), shorts * 2); + if(eraseConsumed) + m_buf.erase(m_buf.begin(), m_buf.begin() + count * m_channels); + if(m_cbCons) + m_cbCons->onConsumeSamples(samples, count, this); + return count; +} + + + diff --git a/src/SampleBuffer.h b/src/SampleBuffer.h index e9a97dc..c0e1c09 100644 --- a/src/SampleBuffer.h +++ b/src/SampleBuffer.h @@ -1,125 +1,125 @@ -// src/SampleBuffer.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#pragma once - -#include -#include -#include - - -#include "SampleProducer.h" - -class SampleBuffer : public SampleProducer -{ -public: - typedef std::mutex Mutex; - typedef std::lock_guard Lock; - - class ProduceCallback - { - public: - virtual void onProduceSamples(const short *samples, int count, SampleBuffer *caller) = 0; - }; - - class ConsumeCallback - { - public: - virtual void onConsumeSamples(const short *samples, int count, SampleBuffer *caller) = 0; - }; - -public: - SampleBuffer(int channels, size_t maxSize = 0); - - //Set the callback that is called when samples are placed into the buffer (produced) - inline void setOnProduce(ProduceCallback *cb) { - assert(!m_mutex.try_lock() && "Mutex not locked"); - m_cbProd = cb; - } - - //Get the callback that is called when samples are placed into the buffer (produced) - inline ConsumeCallback *getOnProduce() const { - assert(!m_mutex.try_lock() && "Mutex not locked"); - return m_cbCons; - } - - //Set the callback that is called when samples are read from the buffer (consumed) - inline void setOnConsume(ConsumeCallback *cb) { - assert(!m_mutex.try_lock() && "Mutex not locked"); - m_cbCons = cb; - } - - //Get the callback that is called when samples are read from the buffer (consumed) - inline ConsumeCallback *getOnConsume() const { - assert(!m_mutex.try_lock() && "Mutex not locked"); - return m_cbCons; - } - - //Get the number of available samples - //One sample is (2 * channels) bytes in size - inline int avail() const { - assert(!m_mutex.try_lock() && "Mutex not locked"); - return m_buf.size() / m_channels; - } - - //Return the number of channels this buffer was initialized with - inline int channels() const { - assert(!m_mutex.try_lock() && "Mutex not locked"); - return m_channels; - } - - //Return max size as set - inline size_t maxSize() const { - assert(!m_mutex.try_lock() && "Mutex not locked"); - return m_maxSize; - } - - //Place some samples into the buffer - //samples: The sample buffer - //count: Number of samples in buffer - //One sample is (2 * channels) bytes in size - virtual void produce(const short *samples, int count) override; - - //Consume some samples from the buffer - //samples: The sample buffer - //count: Size of buffer measured in Samples - //eraseConsumed: Erase the consumed samples from the buffer - //One sample is (2 * channels) bytes in size - int consume(short *samples, int maxCount, bool eraseConsumed = true); - - //Get size of a sample in bytes - inline int sampleSize() const { - assert(!m_mutex.try_lock() && "Mutex not locked"); - return 2 * m_channels; - } - - //Directly return bare memory adress - //Be careful! - inline short *getBufferData() { - assert(!m_mutex.try_lock() && "Mutex not locked"); - return m_buf.data(); - } - - inline const std::mutex &getMutex() const { - return m_mutex; - } - - inline std::mutex &getMutex() { - return m_mutex; - } - -private: - const int m_channels; - const size_t m_maxSize; - std::vector m_buf; - mutable std::mutex m_mutex; - ProduceCallback *m_cbProd; - ConsumeCallback *m_cbCons; -}; - +// src/SampleBuffer.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#pragma once + +#include +#include +#include + + +#include "SampleProducer.h" + +class SampleBuffer : public SampleProducer +{ +public: + typedef std::mutex Mutex; + typedef std::lock_guard Lock; + + class ProduceCallback + { + public: + virtual void onProduceSamples(const short *samples, int count, SampleBuffer *caller) = 0; + }; + + class ConsumeCallback + { + public: + virtual void onConsumeSamples(const short *samples, int count, SampleBuffer *caller) = 0; + }; + +public: + SampleBuffer(int channels, size_t maxSize = 0); + + //Set the callback that is called when samples are placed into the buffer (produced) + inline void setOnProduce(ProduceCallback *cb) { + assert(!m_mutex.try_lock() && "Mutex not locked"); + m_cbProd = cb; + } + + //Get the callback that is called when samples are placed into the buffer (produced) + inline ConsumeCallback *getOnProduce() const { + assert(!m_mutex.try_lock() && "Mutex not locked"); + return m_cbCons; + } + + //Set the callback that is called when samples are read from the buffer (consumed) + inline void setOnConsume(ConsumeCallback *cb) { + assert(!m_mutex.try_lock() && "Mutex not locked"); + m_cbCons = cb; + } + + //Get the callback that is called when samples are read from the buffer (consumed) + inline ConsumeCallback *getOnConsume() const { + assert(!m_mutex.try_lock() && "Mutex not locked"); + return m_cbCons; + } + + //Get the number of available samples + //One sample is (2 * channels) bytes in size + inline int avail() const { + assert(!m_mutex.try_lock() && "Mutex not locked"); + return m_buf.size() / m_channels; + } + + //Return the number of channels this buffer was initialized with + inline int channels() const { + assert(!m_mutex.try_lock() && "Mutex not locked"); + return m_channels; + } + + //Return max size as set + inline size_t maxSize() const { + assert(!m_mutex.try_lock() && "Mutex not locked"); + return m_maxSize; + } + + //Place some samples into the buffer + //samples: The sample buffer + //count: Number of samples in buffer + //One sample is (2 * channels) bytes in size + virtual void produce(const short *samples, int count) override; + + //Consume some samples from the buffer + //samples: The sample buffer + //count: Size of buffer measured in Samples + //eraseConsumed: Erase the consumed samples from the buffer + //One sample is (2 * channels) bytes in size + int consume(short *samples, int maxCount, bool eraseConsumed = true); + + //Get size of a sample in bytes + inline int sampleSize() const { + assert(!m_mutex.try_lock() && "Mutex not locked"); + return 2 * m_channels; + } + + //Directly return bare memory adress + //Be careful! + inline short *getBufferData() { + assert(!m_mutex.try_lock() && "Mutex not locked"); + return m_buf.data(); + } + + inline const std::mutex &getMutex() const { + return m_mutex; + } + + inline std::mutex &getMutex() { + return m_mutex; + } + +private: + const int m_channels; + const size_t m_maxSize; + std::vector m_buf; + mutable std::mutex m_mutex; + ProduceCallback *m_cbProd; + ConsumeCallback *m_cbCons; +}; + diff --git a/src/SampleProducer.h b/src/SampleProducer.h index a3640eb..b058b02 100644 --- a/src/SampleProducer.h +++ b/src/SampleProducer.h @@ -1,16 +1,16 @@ -// src/SampleProducer.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -class SampleProducer -{ -public: - virtual void produce(const short *samples, int count) = 0; -}; - +// src/SampleProducer.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +class SampleProducer +{ +public: + virtual void produce(const short *samples, int count) = 0; +}; + diff --git a/src/SampleProducerThread.cpp b/src/SampleProducerThread.cpp index 5922e2f..f00cd84 100644 --- a/src/SampleProducerThread.cpp +++ b/src/SampleProducerThread.cpp @@ -1,149 +1,149 @@ -// src/SampleProducerThread.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#include -#include -#include - -#include "SampleBuffer.h" -#include "SampleSource.h" -#include "SampleProducerThread.h" - - - -SampleProducerThread::SampleProducerThread() : - m_source(nullptr), - m_running(false), - m_stop(false) -{ - -} - - -void SampleProducerThread::start() -{ - if(m_running) - return; - - m_running = true; - m_stop = false; - std::thread t(&SampleProducerThread::threadFunc, this); - m_thread = std::move(t); -} - - -void SampleProducerThread::stop(bool wait) -{ - m_stop = true; - if(wait && m_thread.joinable()) - m_thread.join(); -} - - -bool SampleProducerThread::isRunning() -{ - return m_running; -} - - -void SampleProducerThread::setSource( SampleSource *source ) -{ - m_mutex.lock(); - m_source = source; - m_mutex.unlock(); -} - -#define MIN_BUFFER_SAMPLES (48000 / 2) -void SampleProducerThread::run() -{ - while(!m_stop) - { - m_mutex.lock(); - if (m_source) - singleBufferFill(); - m_mutex.unlock(); - - // We now have half a second of samples available and have done - // so much work that we deserve a little rest - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } -} - - -void SampleProducerThread::threadFunc() -{ - run(); - m_running = false; -} - - -void SampleProducerThread::addBuffer( SampleBuffer *buffer, bool enableBuffer /*= true*/ ) -{ - Lock lock(m_mutex); - if(std::find_if(m_buffers.begin(), m_buffers.end(), [buffer](const buffer_t &b) {return b.buffer == buffer;}) == m_buffers.end()) - { - buffer_t b = {buffer, enableBuffer}; - m_buffers.push_back(b); - } -} - - -void SampleProducerThread::remBuffer( SampleBuffer *buffer ) -{ - Lock lock(m_mutex); - auto it = std::find_if(m_buffers.begin(), m_buffers.end(), [buffer](const buffer_t &b) {return b.buffer == buffer;}); - if(it != m_buffers.end()) - m_buffers.erase(it); -} - - -void SampleProducerThread::setBufferEnabled( SampleBuffer *buffer, bool enabled ) -{ - Lock lock(m_mutex); - auto it = std::find_if(m_buffers.begin(), m_buffers.end(), [buffer](const buffer_t &b) {return b.buffer == buffer;}); - if(it != m_buffers.end()) - it->enabled = enabled; -} - - -void SampleProducerThread::produce( const short *samples, int count ) -{ - for(const buffer_t &buffer : m_buffers) - { - if(buffer.enabled) - { - SampleBuffer::Lock lock(buffer.buffer->getMutex()); - buffer.buffer->produce(samples, count); - } - } -} - - -bool SampleProducerThread::singleBufferFill() -{ - for(const buffer_t &buffer : m_buffers) - { - if (buffer.enabled) - { - std::unique_lock sbl(buffer.buffer->getMutex()); - while (buffer.buffer->avail() < MIN_BUFFER_SAMPLES) - { - assert(buffer.buffer->maxSize() > MIN_BUFFER_SAMPLES && "Buffer too small"); - sbl.unlock(); - int samples = m_source->readSamples(this); - if (samples < 0) // error - return false; - if (samples == 0) // file is done - return true; - sbl.lock(); - } - } - } - return true; -} +// src/SampleProducerThread.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#include +#include +#include + +#include "SampleBuffer.h" +#include "SampleSource.h" +#include "SampleProducerThread.h" + + + +SampleProducerThread::SampleProducerThread() : + m_source(nullptr), + m_running(false), + m_stop(false) +{ + +} + + +void SampleProducerThread::start() +{ + if(m_running) + return; + + m_running = true; + m_stop = false; + std::thread t(&SampleProducerThread::threadFunc, this); + m_thread = std::move(t); +} + + +void SampleProducerThread::stop(bool wait) +{ + m_stop = true; + if(wait && m_thread.joinable()) + m_thread.join(); +} + + +bool SampleProducerThread::isRunning() +{ + return m_running; +} + + +void SampleProducerThread::setSource( SampleSource *source ) +{ + m_mutex.lock(); + m_source = source; + m_mutex.unlock(); +} + +#define MIN_BUFFER_SAMPLES (48000 / 2) +void SampleProducerThread::run() +{ + while(!m_stop) + { + m_mutex.lock(); + if (m_source) + singleBufferFill(); + m_mutex.unlock(); + + // We now have half a second of samples available and have done + // so much work that we deserve a little rest + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } +} + + +void SampleProducerThread::threadFunc() +{ + run(); + m_running = false; +} + + +void SampleProducerThread::addBuffer( SampleBuffer *buffer, bool enableBuffer /*= true*/ ) +{ + Lock lock(m_mutex); + if(std::find_if(m_buffers.begin(), m_buffers.end(), [buffer](const buffer_t &b) {return b.buffer == buffer;}) == m_buffers.end()) + { + buffer_t b = {buffer, enableBuffer}; + m_buffers.push_back(b); + } +} + + +void SampleProducerThread::remBuffer( SampleBuffer *buffer ) +{ + Lock lock(m_mutex); + auto it = std::find_if(m_buffers.begin(), m_buffers.end(), [buffer](const buffer_t &b) {return b.buffer == buffer;}); + if(it != m_buffers.end()) + m_buffers.erase(it); +} + + +void SampleProducerThread::setBufferEnabled( SampleBuffer *buffer, bool enabled ) +{ + Lock lock(m_mutex); + auto it = std::find_if(m_buffers.begin(), m_buffers.end(), [buffer](const buffer_t &b) {return b.buffer == buffer;}); + if(it != m_buffers.end()) + it->enabled = enabled; +} + + +void SampleProducerThread::produce( const short *samples, int count ) +{ + for(const buffer_t &buffer : m_buffers) + { + if(buffer.enabled) + { + SampleBuffer::Lock lock(buffer.buffer->getMutex()); + buffer.buffer->produce(samples, count); + } + } +} + + +bool SampleProducerThread::singleBufferFill() +{ + for(const buffer_t &buffer : m_buffers) + { + if (buffer.enabled) + { + std::unique_lock sbl(buffer.buffer->getMutex()); + while (buffer.buffer->avail() < MIN_BUFFER_SAMPLES) + { + assert(buffer.buffer->maxSize() > MIN_BUFFER_SAMPLES && "Buffer too small"); + sbl.unlock(); + int samples = m_source->readSamples(this); + if (samples < 0) // error + return false; + if (samples == 0) // file is done + return true; + sbl.lock(); + } + } + } + return true; +} diff --git a/src/SampleProducerThread.h b/src/SampleProducerThread.h index e260745..06d26e7 100644 --- a/src/SampleProducerThread.h +++ b/src/SampleProducerThread.h @@ -1,55 +1,55 @@ -// src/SampleProducerThread.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - - -#pragma once - -#include -#include - -#include "SampleProducer.h" - -class SampleBuffer; -class SampleSource; - - -class SampleProducerThread : public SampleProducer -{ - struct buffer_t - { - SampleBuffer *buffer; - bool enabled; - }; - -public: - SampleProducerThread(); - void addBuffer(SampleBuffer *buffer, bool enableBuffer = true); - void remBuffer(SampleBuffer *buffer); - void setBufferEnabled(SampleBuffer *buffer, bool enabled); - void start(); - void stop(bool wait = true); - bool isRunning(); - void setSource(SampleSource *source); - -private: - void run(); - void threadFunc(); - bool singleBufferFill(); - void produce(const short *samples, int count) override; - - typedef std::lock_guard Lock; - - std::thread m_thread; - SampleSource * volatile m_source; - std::vector m_buffers; - bool m_running; - volatile bool m_stop; - std::mutex m_mutex; -}; - +// src/SampleProducerThread.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + + +#pragma once + +#include +#include + +#include "SampleProducer.h" + +class SampleBuffer; +class SampleSource; + + +class SampleProducerThread : public SampleProducer +{ + struct buffer_t + { + SampleBuffer *buffer; + bool enabled; + }; + +public: + SampleProducerThread(); + void addBuffer(SampleBuffer *buffer, bool enableBuffer = true); + void remBuffer(SampleBuffer *buffer); + void setBufferEnabled(SampleBuffer *buffer, bool enabled); + void start(); + void stop(bool wait = true); + bool isRunning(); + void setSource(SampleSource *source); + +private: + void run(); + void threadFunc(); + bool singleBufferFill(); + void produce(const short *samples, int count) override; + + typedef std::lock_guard Lock; + + std::thread m_thread; + SampleSource * volatile m_source; + std::vector m_buffers; + bool m_running; + volatile bool m_stop; + std::mutex m_mutex; +}; + diff --git a/src/SampleSource.h b/src/SampleSource.h index 1837600..09ffa4b 100644 --- a/src/SampleSource.h +++ b/src/SampleSource.h @@ -1,19 +1,19 @@ -// src/SampleSource.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include "SampleProducer.h" - -//Interface SampleSource -class SampleSource -{ -public: - virtual int readSamples(SampleProducer *sampleBuffer) = 0; -}; - +// src/SampleSource.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include "SampleProducer.h" + +//Interface SampleSource +class SampleSource +{ +public: + virtual int readSamples(SampleProducer *sampleBuffer) = 0; +}; + diff --git a/src/SampleVisualizerThread.cpp b/src/SampleVisualizerThread.cpp index d4a2b66..6d98b86 100644 --- a/src/SampleVisualizerThread.cpp +++ b/src/SampleVisualizerThread.cpp @@ -1,216 +1,216 @@ -// src/SampleVisualizerThread.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - - -#include -#include "inputfile.h" -#include "SampleVisualizerThread.h" -#include "SampleBuffer.h" - -#define MIN_SAMPLES_PER_ITERATION (1024 * 32) -#define SAMPLE_RATE 44100; - - -SampleVisualizerThread::SampleBufferSynced::SampleBufferSynced(int channels, size_t maxSize) : - SampleBuffer(channels, maxSize) -{} - - -void SampleVisualizerThread::SampleBufferSynced::produce(const short *samples, int count) -{ - SampleBuffer::Lock l(getMutex()); - SampleBuffer::produce(samples, count); -} - - -double SampleVisualizerThread::fileLength() const -{ - uint64_t samples = m_running ? m_numSamplesTotalEst : m_numSamplesProcessed; - return double(samples) / SAMPLE_RATE; -} - - -SampleVisualizerThread & SampleVisualizerThread::GetInstance() -{ - static SampleVisualizerThread t; - return t; -} - - -SampleVisualizerThread::SampleVisualizerThread() : - m_buffer(1), - m_numBins(0), - m_numBinsProcessed(0), - m_numSamplesProcessed(0), - m_numSamplesTotalEst(0), - m_numSamplesProcessedThisBin(0), - m_file(nullptr), - m_running(false), - m_newFile(false), - m_stop(false) -{ - -} - - -SampleVisualizerThread::~SampleVisualizerThread() -{ - stop(); -} - - -void SampleVisualizerThread::startAnalysis( const char *filename, size_t numBins ) -{ - Lock lock(m_mutex); - - m_filename = filename; - m_numBins = numBins; - m_numBinsProcessed = 0; - m_numSamplesProcessed = 0; - m_numSamplesTotalEst = 0; - m_numSamplesProcessedThisBin = 0; - m_bins.clear(); - m_newFile = true; - - if(!m_running) - { - m_running = true; - m_stop = false; - std::thread t(&SampleVisualizerThread::threadFunc, this); - m_thread = std::move(t); - } -} - - -void SampleVisualizerThread::stop( bool wait /*= true*/ ) -{ - m_stop = true; - if(wait && m_thread.joinable()) - m_thread.join(); -} - - -bool SampleVisualizerThread::isRunning() const -{ - return m_running; -} - - -size_t SampleVisualizerThread::getBinsProcessed() const -{ - return m_numBinsProcessed; -} - - -void SampleVisualizerThread::run() -{ - while(!m_stop) - { - m_mutex.lock(); - if(m_newFile) - openNewFile(); - - int readSamplesThisIt = 0; - while(m_file && readSamplesThisIt < MIN_SAMPLES_PER_ITERATION) - { - int samples = m_file->readSamples(&m_buffer); - if(samples <= 0 || m_file->done()) - { - m_file->close(); - delete m_file; - m_file = nullptr; - } - if(samples > 0) - { - readSamplesThisIt += samples; - processSamples(samples); - } - } - - int sleepTime = m_file ? 1 : 100; - m_mutex.unlock(); - std::this_thread::sleep_for(std::chrono::milliseconds(sleepTime)); - } -} - - -void SampleVisualizerThread::threadFunc() -{ - run(); - m_running = false; -} - - -void SampleVisualizerThread::openNewFile() -{ - m_newFile = false; - if(m_file) - delete m_file; - InputFileOptions options; - options.outputChannelLayout = InputFileOptions::MONO; - options.outputSampleRate = SAMPLE_RATE; - m_file = CreateInputFileFFmpeg(options); - if(m_file->open(m_filename.c_str()) == 0) - m_numSamplesTotalEst = m_file->outputSamplesEstimation(); - else - { - delete m_file; - m_file = nullptr; - } -} - - -void SampleVisualizerThread::processSamples(size_t newSamples) -{ - while(true) - { - size_t samplesPerBin = (size_t)(m_numSamplesTotalEst / (int64_t)m_numBins); - if(m_numSamplesProcessedThisBin == 0) - { - m_min = std::numeric_limits::max(); - m_max = std::numeric_limits::min(); - } - SampleBuffer::Lock sbl(m_buffer.getMutex()); - size_t numSamplesThisIt = std::min((size_t)m_buffer.avail(), samplesPerBin - m_numSamplesProcessedThisBin); - if(numSamplesThisIt == 0) - break; - getMinMax(m_buffer.getBufferData(), numSamplesThisIt, m_min, m_max); - m_buffer.consume(nullptr, numSamplesThisIt); - m_numSamplesProcessedThisBin += numSamplesThisIt; - if(m_numSamplesProcessedThisBin >= samplesPerBin) - { - m_bins.push_back(m_min); - m_bins.push_back(m_max); - m_numSamplesProcessedThisBin = 0; - m_numBinsProcessed++; - } - } -} - - -void SampleVisualizerThread::getMinMax( const short *data, size_t count, int &min, int &max ) -{ - - for(size_t i = 0; i < count; ++i) - { - min = std::min(min, (int)data[i]); - max = std::max(max, (int)data[i]); - } -} - - -volatile const int * SampleVisualizerThread::getBins() const -{ - return m_bins.data(); -} - - - - - +// src/SampleVisualizerThread.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + + +#include +#include "inputfile.h" +#include "SampleVisualizerThread.h" +#include "SampleBuffer.h" + +#define MIN_SAMPLES_PER_ITERATION (1024 * 32) +#define SAMPLE_RATE 44100; + + +SampleVisualizerThread::SampleBufferSynced::SampleBufferSynced(int channels, size_t maxSize) : + SampleBuffer(channels, maxSize) +{} + + +void SampleVisualizerThread::SampleBufferSynced::produce(const short *samples, int count) +{ + SampleBuffer::Lock l(getMutex()); + SampleBuffer::produce(samples, count); +} + + +double SampleVisualizerThread::fileLength() const +{ + uint64_t samples = m_running ? m_numSamplesTotalEst : m_numSamplesProcessed; + return double(samples) / SAMPLE_RATE; +} + + +SampleVisualizerThread & SampleVisualizerThread::GetInstance() +{ + static SampleVisualizerThread t; + return t; +} + + +SampleVisualizerThread::SampleVisualizerThread() : + m_buffer(1), + m_numBins(0), + m_numBinsProcessed(0), + m_numSamplesProcessed(0), + m_numSamplesTotalEst(0), + m_numSamplesProcessedThisBin(0), + m_file(nullptr), + m_running(false), + m_newFile(false), + m_stop(false) +{ + +} + + +SampleVisualizerThread::~SampleVisualizerThread() +{ + stop(); +} + + +void SampleVisualizerThread::startAnalysis( const char *filename, size_t numBins ) +{ + Lock lock(m_mutex); + + m_filename = filename; + m_numBins = numBins; + m_numBinsProcessed = 0; + m_numSamplesProcessed = 0; + m_numSamplesTotalEst = 0; + m_numSamplesProcessedThisBin = 0; + m_bins.clear(); + m_newFile = true; + + if(!m_running) + { + m_running = true; + m_stop = false; + std::thread t(&SampleVisualizerThread::threadFunc, this); + m_thread = std::move(t); + } +} + + +void SampleVisualizerThread::stop( bool wait /*= true*/ ) +{ + m_stop = true; + if(wait && m_thread.joinable()) + m_thread.join(); +} + + +bool SampleVisualizerThread::isRunning() const +{ + return m_running; +} + + +size_t SampleVisualizerThread::getBinsProcessed() const +{ + return m_numBinsProcessed; +} + + +void SampleVisualizerThread::run() +{ + while(!m_stop) + { + m_mutex.lock(); + if(m_newFile) + openNewFile(); + + int readSamplesThisIt = 0; + while(m_file && readSamplesThisIt < MIN_SAMPLES_PER_ITERATION) + { + int samples = m_file->readSamples(&m_buffer); + if(samples <= 0 || m_file->done()) + { + m_file->close(); + delete m_file; + m_file = nullptr; + } + if(samples > 0) + { + readSamplesThisIt += samples; + processSamples(samples); + } + } + + int sleepTime = m_file ? 1 : 100; + m_mutex.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(sleepTime)); + } +} + + +void SampleVisualizerThread::threadFunc() +{ + run(); + m_running = false; +} + + +void SampleVisualizerThread::openNewFile() +{ + m_newFile = false; + if(m_file) + delete m_file; + InputFileOptions options; + options.outputChannelLayout = InputFileOptions::MONO; + options.outputSampleRate = SAMPLE_RATE; + m_file = CreateInputFileFFmpeg(options); + if(m_file->open(m_filename.c_str()) == 0) + m_numSamplesTotalEst = m_file->outputSamplesEstimation(); + else + { + delete m_file; + m_file = nullptr; + } +} + + +void SampleVisualizerThread::processSamples(size_t newSamples) +{ + while(true) + { + size_t samplesPerBin = (size_t)(m_numSamplesTotalEst / (int64_t)m_numBins); + if(m_numSamplesProcessedThisBin == 0) + { + m_min = std::numeric_limits::max(); + m_max = std::numeric_limits::min(); + } + SampleBuffer::Lock sbl(m_buffer.getMutex()); + size_t numSamplesThisIt = std::min((size_t)m_buffer.avail(), samplesPerBin - m_numSamplesProcessedThisBin); + if(numSamplesThisIt == 0) + break; + getMinMax(m_buffer.getBufferData(), numSamplesThisIt, m_min, m_max); + m_buffer.consume(nullptr, numSamplesThisIt); + m_numSamplesProcessedThisBin += numSamplesThisIt; + if(m_numSamplesProcessedThisBin >= samplesPerBin) + { + m_bins.push_back(m_min); + m_bins.push_back(m_max); + m_numSamplesProcessedThisBin = 0; + m_numBinsProcessed++; + } + } +} + + +void SampleVisualizerThread::getMinMax( const short *data, size_t count, int &min, int &max ) +{ + + for(size_t i = 0; i < count; ++i) + { + min = std::min(min, (int)data[i]); + max = std::max(max, (int)data[i]); + } +} + + +volatile const int * SampleVisualizerThread::getBins() const +{ + return m_bins.data(); +} + + + + + diff --git a/src/SampleVisualizerThread.h b/src/SampleVisualizerThread.h index c9d578e..ddb771f 100644 --- a/src/SampleVisualizerThread.h +++ b/src/SampleVisualizerThread.h @@ -1,77 +1,77 @@ -// src/SampleVisualizerThread.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include -#include -#include -#include - -#include "SampleBuffer.h" - - -class InputFile; -class SampleVisualizerThread -{ -public: - class SampleBufferSynced : public SampleBuffer - { - public: - SampleBufferSynced(int channels, size_t maxSize = 0); - virtual void produce(const short *samples, int count) override; - }; - -public: - SampleVisualizerThread(); - ~SampleVisualizerThread(); - void startAnalysis(const char *filename, size_t numBins); - void stop(bool wait = true); - bool isRunning() const; - size_t getBinsProcessed() const; - - // Array with numBins * 2 values (min and max value) - volatile const int *getBins() const; - - inline int64_t getTotalSamplesEst() const { - return m_numSamplesTotalEst; - } - - // Get file length in seconds, might be an estimation when processing isn't finished yet - double fileLength() const; - - static SampleVisualizerThread &GetInstance(); - -private: - void run(); - void threadFunc(); - void openNewFile(); - void processSamples(size_t newSamples); - static void getMinMax(const short *data, size_t count, int &min, int &max); - - typedef std::lock_guard Lock; - -private: - SampleBufferSynced m_buffer; - size_t m_numBins; - size_t m_numBinsProcessed; - int64_t m_numSamplesProcessed; - int64_t m_numSamplesTotalEst; - size_t m_numSamplesProcessedThisBin; - int m_min; - int m_max; - mutable std::mutex m_mutex; - std::vector m_bins; - InputFile *m_file; - std::thread m_thread; - std::string m_filename; - volatile bool m_running; - volatile bool m_newFile; - volatile bool m_stop; -}; - +// src/SampleVisualizerThread.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include +#include +#include +#include + +#include "SampleBuffer.h" + + +class InputFile; +class SampleVisualizerThread +{ +public: + class SampleBufferSynced : public SampleBuffer + { + public: + SampleBufferSynced(int channels, size_t maxSize = 0); + virtual void produce(const short *samples, int count) override; + }; + +public: + SampleVisualizerThread(); + ~SampleVisualizerThread(); + void startAnalysis(const char *filename, size_t numBins); + void stop(bool wait = true); + bool isRunning() const; + size_t getBinsProcessed() const; + + // Array with numBins * 2 values (min and max value) + volatile const int *getBins() const; + + inline int64_t getTotalSamplesEst() const { + return m_numSamplesTotalEst; + } + + // Get file length in seconds, might be an estimation when processing isn't finished yet + double fileLength() const; + + static SampleVisualizerThread &GetInstance(); + +private: + void run(); + void threadFunc(); + void openNewFile(); + void processSamples(size_t newSamples); + static void getMinMax(const short *data, size_t count, int &min, int &max); + + typedef std::lock_guard Lock; + +private: + SampleBufferSynced m_buffer; + size_t m_numBins; + size_t m_numBinsProcessed; + int64_t m_numSamplesProcessed; + int64_t m_numSamplesTotalEst; + size_t m_numSamplesProcessedThisBin; + int m_min; + int m_max; + mutable std::mutex m_mutex; + std::vector m_bins; + InputFile *m_file; + std::thread m_thread; + std::string m_filename; + volatile bool m_running; + volatile bool m_newFile; + volatile bool m_stop; +}; + diff --git a/src/SoundInfo.cpp b/src/SoundInfo.cpp index d96661e..0886b22 100644 --- a/src/SoundInfo.cpp +++ b/src/SoundInfo.cpp @@ -1,122 +1,122 @@ -// src/SoundInfo.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#include "SoundInfo.h" - -#define NAME_PATH "path" -#define NAME_CUSTOM_TEXT "customText" -#define NAME_CUSTOM_COLOR "customColor" -#define NAME_VOLUME "volume" -#define NAME_CROP_ENABLED "cropEnabled" -#define NAME_CROP_START_VALUE "cropStartValue" -#define NAME_CROP_START_UNIT "cropStartUnit" -#define NAME_CROP_STOP_AFTER_AT "cropStopAfterAt" -#define NAME_CROP_STOP_VALUE "cropStopValue" -#define NAME_CROP_STOP_UNIT "cropStopUnit" - -#define DEFAULT_PATH "" -#define DEFAULT_CUSTOM_TEXT "" -#define DEFAULT_CUSTOM_COLOR "00FFFFFF" -#define DEFAULT_VOLUME 0 -#define DEFAULT_CROP_ENABLED false -#define DEFAULT_CROP_START_VALUE 1 -#define DEFAULT_CROP_START_UNIT 1 -#define DEFAULT_CROP_STOP_AFTER_AT 0 -#define DEFAULT_CROP_STOP_VALUE 0 -#define DEFAULT_CROP_STOP_UNIT 1 - - -QColor stringToColor(const QString &str) -{ - QRgb rgb = str.toUInt(nullptr, 16); - // don't construct directly from QRgb because alpha is ignored that way - return QColor(qRed(rgb), qGreen(rgb), qBlue(rgb), qAlpha(rgb)); -} - -QString colorToString(const QColor &col) -{ - return QString::number(col.rgba(), 16); -} - - -SoundInfo::SoundInfo() : - filename(DEFAULT_PATH), - customText(DEFAULT_CUSTOM_TEXT), - customColor(stringToColor(DEFAULT_CUSTOM_COLOR)), - volume(DEFAULT_VOLUME), - cropEnabled(DEFAULT_CROP_ENABLED), - cropStartValue(DEFAULT_CROP_START_VALUE), - cropStartUnit(DEFAULT_CROP_START_UNIT), - cropStopAfterAt(DEFAULT_CROP_STOP_AFTER_AT), - cropStopValue(DEFAULT_CROP_STOP_VALUE), - cropStopUnit(DEFAULT_CROP_STOP_UNIT) -{ - -} - - -void SoundInfo::readFromConfig( const QSettings &settings ) -{ - filename = settings.value(NAME_PATH, DEFAULT_PATH).toString(); - customText = settings.value(NAME_CUSTOM_TEXT, DEFAULT_CUSTOM_TEXT).toString(); - customColor = stringToColor(settings.value(NAME_CUSTOM_COLOR, DEFAULT_CUSTOM_COLOR).toString()); - volume = settings.value(NAME_VOLUME, DEFAULT_VOLUME).toInt(); - cropEnabled = settings.value(NAME_CROP_ENABLED, DEFAULT_CROP_ENABLED).toBool(); - cropStartValue = settings.value(NAME_CROP_START_VALUE, DEFAULT_CROP_START_VALUE).toInt(); - cropStartUnit = settings.value(NAME_CROP_START_UNIT, DEFAULT_CROP_START_UNIT).toInt(); - cropStopAfterAt = settings.value(NAME_CROP_STOP_AFTER_AT, DEFAULT_CROP_STOP_AFTER_AT).toInt(); - cropStopValue = settings.value(NAME_CROP_STOP_VALUE, DEFAULT_CROP_STOP_VALUE).toInt(); - cropStopUnit = settings.value(NAME_CROP_STOP_UNIT, DEFAULT_CROP_STOP_UNIT).toInt(); -} - - -void SoundInfo::saveToConfig( QSettings &settings ) const -{ - settings.setValue(NAME_PATH, filename); - settings.setValue(NAME_CUSTOM_TEXT, customText); - settings.setValue(NAME_CUSTOM_COLOR, colorToString(customColor)); - settings.setValue(NAME_VOLUME, volume); - settings.setValue(NAME_CROP_ENABLED, cropEnabled); - settings.setValue(NAME_CROP_START_VALUE, cropStartValue); - settings.setValue(NAME_CROP_START_UNIT, cropStartUnit); - settings.setValue(NAME_CROP_STOP_AFTER_AT, cropStopAfterAt); - settings.setValue(NAME_CROP_STOP_VALUE, cropStopValue); - settings.setValue(NAME_CROP_STOP_UNIT, cropStopUnit); -} - - -double SoundInfo::getStartTime() const -{ - if(!cropEnabled) - return 0.0; - return (double)cropStartValue * getTimeUnitFactor(cropStartUnit); -} - - -double SoundInfo::getPlayTime() const -{ - if(!cropEnabled) - return -1.0; - double t = (double)cropStopValue * getTimeUnitFactor(cropStopUnit); - if(cropStopAfterAt == 1) //stop AT x seconds instead of AFTER? - t -= getStartTime(); - return std::max(t, 0.0); -} - - -double SoundInfo::getTimeUnitFactor(int unit) -{ - switch(unit) - { - case 0: return 0.001; - case 1: return 1.0; - default: - throw std::logic_error("No such unit"); - } -} +// src/SoundInfo.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#include "SoundInfo.h" + +#define NAME_PATH "path" +#define NAME_CUSTOM_TEXT "customText" +#define NAME_CUSTOM_COLOR "customColor" +#define NAME_VOLUME "volume" +#define NAME_CROP_ENABLED "cropEnabled" +#define NAME_CROP_START_VALUE "cropStartValue" +#define NAME_CROP_START_UNIT "cropStartUnit" +#define NAME_CROP_STOP_AFTER_AT "cropStopAfterAt" +#define NAME_CROP_STOP_VALUE "cropStopValue" +#define NAME_CROP_STOP_UNIT "cropStopUnit" + +#define DEFAULT_PATH "" +#define DEFAULT_CUSTOM_TEXT "" +#define DEFAULT_CUSTOM_COLOR "00FFFFFF" +#define DEFAULT_VOLUME 0 +#define DEFAULT_CROP_ENABLED false +#define DEFAULT_CROP_START_VALUE 1 +#define DEFAULT_CROP_START_UNIT 1 +#define DEFAULT_CROP_STOP_AFTER_AT 0 +#define DEFAULT_CROP_STOP_VALUE 0 +#define DEFAULT_CROP_STOP_UNIT 1 + + +QColor stringToColor(const QString &str) +{ + QRgb rgb = str.toUInt(nullptr, 16); + // don't construct directly from QRgb because alpha is ignored that way + return QColor(qRed(rgb), qGreen(rgb), qBlue(rgb), qAlpha(rgb)); +} + +QString colorToString(const QColor &col) +{ + return QString::number(col.rgba(), 16); +} + + +SoundInfo::SoundInfo() : + filename(DEFAULT_PATH), + customText(DEFAULT_CUSTOM_TEXT), + customColor(stringToColor(DEFAULT_CUSTOM_COLOR)), + volume(DEFAULT_VOLUME), + cropEnabled(DEFAULT_CROP_ENABLED), + cropStartValue(DEFAULT_CROP_START_VALUE), + cropStartUnit(DEFAULT_CROP_START_UNIT), + cropStopAfterAt(DEFAULT_CROP_STOP_AFTER_AT), + cropStopValue(DEFAULT_CROP_STOP_VALUE), + cropStopUnit(DEFAULT_CROP_STOP_UNIT) +{ + +} + + +void SoundInfo::readFromConfig( const QSettings &settings ) +{ + filename = settings.value(NAME_PATH, DEFAULT_PATH).toString(); + customText = settings.value(NAME_CUSTOM_TEXT, DEFAULT_CUSTOM_TEXT).toString(); + customColor = stringToColor(settings.value(NAME_CUSTOM_COLOR, DEFAULT_CUSTOM_COLOR).toString()); + volume = settings.value(NAME_VOLUME, DEFAULT_VOLUME).toInt(); + cropEnabled = settings.value(NAME_CROP_ENABLED, DEFAULT_CROP_ENABLED).toBool(); + cropStartValue = settings.value(NAME_CROP_START_VALUE, DEFAULT_CROP_START_VALUE).toInt(); + cropStartUnit = settings.value(NAME_CROP_START_UNIT, DEFAULT_CROP_START_UNIT).toInt(); + cropStopAfterAt = settings.value(NAME_CROP_STOP_AFTER_AT, DEFAULT_CROP_STOP_AFTER_AT).toInt(); + cropStopValue = settings.value(NAME_CROP_STOP_VALUE, DEFAULT_CROP_STOP_VALUE).toInt(); + cropStopUnit = settings.value(NAME_CROP_STOP_UNIT, DEFAULT_CROP_STOP_UNIT).toInt(); +} + + +void SoundInfo::saveToConfig( QSettings &settings ) const +{ + settings.setValue(NAME_PATH, filename); + settings.setValue(NAME_CUSTOM_TEXT, customText); + settings.setValue(NAME_CUSTOM_COLOR, colorToString(customColor)); + settings.setValue(NAME_VOLUME, volume); + settings.setValue(NAME_CROP_ENABLED, cropEnabled); + settings.setValue(NAME_CROP_START_VALUE, cropStartValue); + settings.setValue(NAME_CROP_START_UNIT, cropStartUnit); + settings.setValue(NAME_CROP_STOP_AFTER_AT, cropStopAfterAt); + settings.setValue(NAME_CROP_STOP_VALUE, cropStopValue); + settings.setValue(NAME_CROP_STOP_UNIT, cropStopUnit); +} + + +double SoundInfo::getStartTime() const +{ + if(!cropEnabled) + return 0.0; + return (double)cropStartValue * getTimeUnitFactor(cropStartUnit); +} + + +double SoundInfo::getPlayTime() const +{ + if(!cropEnabled) + return -1.0; + double t = (double)cropStopValue * getTimeUnitFactor(cropStopUnit); + if(cropStopAfterAt == 1) //stop AT x seconds instead of AFTER? + t -= getStartTime(); + return std::max(t, 0.0); +} + + +double SoundInfo::getTimeUnitFactor(int unit) +{ + switch(unit) + { + case 0: return 0.001; + case 1: return 1.0; + default: + throw std::logic_error("No such unit"); + } +} diff --git a/src/SoundInfo.h b/src/SoundInfo.h index e482add..6f6e044 100644 --- a/src/SoundInfo.h +++ b/src/SoundInfo.h @@ -1,40 +1,40 @@ -// src/SoundInfo.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include -#include -#include - -class SoundInfo -{ -public: - SoundInfo(); - void readFromConfig(const QSettings &settings); - void saveToConfig(QSettings &settings) const; - double getStartTime() const; - double getPlayTime() const; - - static double getTimeUnitFactor(int unit); - bool customColorEnabled() const { return customColor.alpha() != 0; } - void setCustomColorEnabled(bool enabled) { customColor.setAlpha(enabled ? 255 : 0); } - -public: - QString filename; - QString customText; - QColor customColor; - int volume; - bool cropEnabled; - int cropStartValue; - int cropStartUnit; - int cropStopAfterAt; - int cropStopValue; - int cropStopUnit; -}; - +// src/SoundInfo.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include +#include +#include + +class SoundInfo +{ +public: + SoundInfo(); + void readFromConfig(const QSettings &settings); + void saveToConfig(QSettings &settings) const; + double getStartTime() const; + double getPlayTime() const; + + static double getTimeUnitFactor(int unit); + bool customColorEnabled() const { return customColor.alpha() != 0; } + void setCustomColorEnabled(bool enabled) { customColor.setAlpha(enabled ? 255 : 0); } + +public: + QString filename; + QString customText; + QColor customColor; + int volume; + bool cropEnabled; + int cropStartValue; + int cropStartUnit; + int cropStopAfterAt; + int cropStopValue; + int cropStopUnit; +}; + diff --git a/src/SoundSettings.cpp b/src/SoundSettings.cpp index cca4969..743cad4 100644 --- a/src/SoundSettings.cpp +++ b/src/SoundSettings.cpp @@ -1,214 +1,214 @@ -// src/SoundSettings.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - - -#include "ui_SoundSettings.h" -#include "SoundSettings.h" -#include "ConfigModel.h" -#include "main.h" -#include "samples.h" -#include "SoundView.h" -#include -#include -#include -#include "MainWindow.h" -#include - - -SoundSettingsQt::SoundSettingsQt(const SoundInfo &soundInfo, size_t buttonId, QWidget *parent /*= 0*/) : - QDialog(parent), - ui(new Ui::SoundSettingsQt), - m_soundInfo(soundInfo), - m_buttonId(buttonId), - m_iconPlay(":/icon/img/playarrow_32.png"), - m_iconStop(":/icon/img/stoparrow_32.png") -{ - m_soundview = new SoundView(this); - m_soundview->setObjectName(QStringLiteral("soundview")); - m_soundview->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding)); - m_soundview->setMinimumSize(QSize(0, 30)); - - ui->setupUi(this); - ui->startSoundUnitCombo->addItem("milliseconds"); - ui->startSoundUnitCombo->addItem("seconds"); - ui->stopSoundUnitCombo->addItem("milliseconds"); - ui->stopSoundUnitCombo->addItem("seconds"); - ui->stopSoundAtAfterCombo->addItem("after"); - ui->stopSoundAtAfterCombo->addItem("at"); - connect(ui->soundVolumeSlider, SIGNAL(valueChanged(int)), this, SLOT(onVolumeChanged(int))); - connect(ui->filenameBrowseButton, SIGNAL(released()), this, SLOT(onBrowsePressed())); - connect(ui->previewSoundButton, SIGNAL(released()), this, SLOT(onPreviewPressed())); - connect(ui->hotkeyChangeButton, SIGNAL(clicked()), this, SLOT(onHotkeyChangePressed())); - connect(ui->colorCheckBox, SIGNAL(clicked()), this, SLOT(onColorEnabledPressed())); - connect(ui->colorButton, SIGNAL(clicked()), this, SLOT(onChooseColorPressed())); - connect(parent, SIGNAL(hotkeyRecordedEvent(QString,QString)), this, SLOT(updateHotkeyText())); - connect(ui->startSoundUnitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSoundView())); - connect(ui->stopSoundUnitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSoundView())); - connect(ui->stopSoundAtAfterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSoundView())); - connect(ui->startSoundValueSpin, SIGNAL(valueChanged(int)), this, SLOT(updateSoundView())); - connect(ui->stopSoundValueSpin, SIGNAL(valueChanged(int)), this, SLOT(updateSoundView())); - connect(ui->groupCrop, SIGNAL(clicked(bool)), this, SLOT(updateSoundView())); - initGui(m_soundInfo); - - m_timer = new QTimer(this); - connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimer())); - - ui->groupCrop->layout()->addWidget(m_soundview); -} - - -void SoundSettingsQt::initGui(const SoundInfo &sound) -{ - ui->filenameEdit->setText(sound.filename); - ui->customTextEdit->setPlaceholderText(QFileInfo(sound.filename).baseName()); - ui->customTextEdit->setText(sound.customText); - ui->soundVolumeSlider->setValue(sound.volume); - ui->groupCrop->setChecked(sound.cropEnabled); - ui->startSoundValueSpin->setValue(sound.cropStartValue); - ui->startSoundUnitCombo->setCurrentIndex(sound.cropStartUnit); - ui->stopSoundAtAfterCombo->setCurrentIndex(sound.cropStopAfterAt); - ui->stopSoundValueSpin->setValue(sound.cropStopValue); - ui->stopSoundUnitCombo->setCurrentIndex(sound.cropStopUnit); - ui->colorCheckBox->setChecked(sound.customColorEnabled()); - ui->colorButton->setEnabled(sound.customColorEnabled()); - ui->colorButton->setStyleSheet(QString("background-color: %1").arg(sound.customColor.name())); - this->customColor = sound.customColor; - - updateHotkeyText(); - - m_soundview->setSound(sound); -} - - -void SoundSettingsQt::fillFromGui(SoundInfo &sound) -{ - sound.filename = ui->filenameEdit->text(); - sound.customText = ui->customTextEdit->text(); - sound.volume = ui->soundVolumeSlider->value(); - sound.cropEnabled = ui->groupCrop->isChecked(); - sound.cropStartValue = ui->startSoundValueSpin->value(); - sound.cropStartUnit = ui->startSoundUnitCombo->currentIndex(); - sound.cropStopAfterAt = ui->stopSoundAtAfterCombo->currentIndex(); - sound.cropStopValue = ui->stopSoundValueSpin->value(); - sound.cropStopUnit = ui->stopSoundUnitCombo->currentIndex(); - sound.customColor = this->customColor; -} - -SoundSettingsQt::~SoundSettingsQt() -{ - delete ui; -} - - -void SoundSettingsQt::done( int r ) -{ - Sampler *sampler = sb_getSampler(); - if(sampler->getState() == Sampler::ePLAYING_PREVIEW) - sampler->stopPlayback(); - fillFromGui(m_soundInfo); - QDialog::done(r); -} - - -void SoundSettingsQt::onVolumeChanged(int value) -{ - ui->soundVolumeDbLabel->setText(QString("%1%2 dB").arg(value > 0 ? "+" : "", QString::number(value))); -} - - -void SoundSettingsQt::onBrowsePressed() -{ - QString filePath = ui->filenameEdit->text(); - QString fn = QFileDialog::getOpenFileName(this, tr("Choose File"), filePath, tr("Files (*.*)")); - if(!fn.isNull()) - { - ui->filenameEdit->setText(fn); - ui->customTextEdit->setPlaceholderText(QFileInfo(fn).baseName()); - - SoundInfo info; - fillFromGui(info); - m_soundview->setSound(info); - } -} - - -void SoundSettingsQt::onPreviewPressed() -{ - Sampler *sampler = sb_getSampler(); - if(sampler->getState() != Sampler::ePLAYING_PREVIEW) - { - SoundInfo sound; - fillFromGui(sound); - if(sampler->playPreview(sound)) - { - ui->previewSoundButton->setIcon(m_iconStop); - m_timer->start(100); - } - } - else - { - sampler->stopPlayback(); - ui->previewSoundButton->setIcon(m_iconPlay); - } -} - - -void SoundSettingsQt::onTimer() -{ - Sampler *sampler = sb_getSampler(); - if(sampler->getState() != Sampler::ePLAYING_PREVIEW) - { - ui->previewSoundButton->setIcon(m_iconPlay); - m_timer->stop(); - } -} - - -void SoundSettingsQt::onHotkeyChangePressed() -{ - MainWindow::openHotkeySetDialog(m_buttonId, this); -} - - -void SoundSettingsQt::updateHotkeyText() -{ - QString hotkeyText = MainWindow::getShortcutString(m_buttonId); - ui->hotkeyCurrentLabel->setText(QString("Current hotkey: ") + - (hotkeyText.isEmpty() ? QString("None") : hotkeyText)); -} - - -void SoundSettingsQt::onColorEnabledPressed() -{ - customColor.setAlpha(ui->colorCheckBox->isChecked() ? 255 : 0); - ui->colorButton->setEnabled(ui->colorCheckBox->isChecked()); -} - - -void SoundSettingsQt::onChooseColorPressed() -{ - int alpha = customColor.alpha(); - customColor = QColorDialog::getColor(customColor, this, "Custom button color"); - customColor.setAlpha(alpha); - ui->colorButton->setStyleSheet(QString("background-color: %1").arg(customColor.name())); -} - - -void SoundSettingsQt::updateSoundView() -{ - SoundInfo info; - fillFromGui(info); - m_soundview->setSound(info); - m_soundview->update(); -} - - - - - +// src/SoundSettings.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + + +#include "ui_SoundSettings.h" +#include "SoundSettings.h" +#include "ConfigModel.h" +#include "main.h" +#include "samples.h" +#include "SoundView.h" +#include +#include +#include +#include "MainWindow.h" +#include + + +SoundSettingsQt::SoundSettingsQt(const SoundInfo &soundInfo, size_t buttonId, QWidget *parent /*= 0*/) : + QDialog(parent), + ui(new Ui::SoundSettingsQt), + m_soundInfo(soundInfo), + m_buttonId(buttonId), + m_iconPlay(":/icon/img/playarrow_32.png"), + m_iconStop(":/icon/img/stoparrow_32.png") +{ + m_soundview = new SoundView(this); + m_soundview->setObjectName(QStringLiteral("soundview")); + m_soundview->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding)); + m_soundview->setMinimumSize(QSize(0, 30)); + + ui->setupUi(this); + ui->startSoundUnitCombo->addItem("milliseconds"); + ui->startSoundUnitCombo->addItem("seconds"); + ui->stopSoundUnitCombo->addItem("milliseconds"); + ui->stopSoundUnitCombo->addItem("seconds"); + ui->stopSoundAtAfterCombo->addItem("after"); + ui->stopSoundAtAfterCombo->addItem("at"); + connect(ui->soundVolumeSlider, SIGNAL(valueChanged(int)), this, SLOT(onVolumeChanged(int))); + connect(ui->filenameBrowseButton, SIGNAL(released()), this, SLOT(onBrowsePressed())); + connect(ui->previewSoundButton, SIGNAL(released()), this, SLOT(onPreviewPressed())); + connect(ui->hotkeyChangeButton, SIGNAL(clicked()), this, SLOT(onHotkeyChangePressed())); + connect(ui->colorCheckBox, SIGNAL(clicked()), this, SLOT(onColorEnabledPressed())); + connect(ui->colorButton, SIGNAL(clicked()), this, SLOT(onChooseColorPressed())); + connect(parent, SIGNAL(hotkeyRecordedEvent(QString,QString)), this, SLOT(updateHotkeyText())); + connect(ui->startSoundUnitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSoundView())); + connect(ui->stopSoundUnitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSoundView())); + connect(ui->stopSoundAtAfterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSoundView())); + connect(ui->startSoundValueSpin, SIGNAL(valueChanged(int)), this, SLOT(updateSoundView())); + connect(ui->stopSoundValueSpin, SIGNAL(valueChanged(int)), this, SLOT(updateSoundView())); + connect(ui->groupCrop, SIGNAL(clicked(bool)), this, SLOT(updateSoundView())); + initGui(m_soundInfo); + + m_timer = new QTimer(this); + connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimer())); + + ui->groupCrop->layout()->addWidget(m_soundview); +} + + +void SoundSettingsQt::initGui(const SoundInfo &sound) +{ + ui->filenameEdit->setText(sound.filename); + ui->customTextEdit->setPlaceholderText(QFileInfo(sound.filename).baseName()); + ui->customTextEdit->setText(sound.customText); + ui->soundVolumeSlider->setValue(sound.volume); + ui->groupCrop->setChecked(sound.cropEnabled); + ui->startSoundValueSpin->setValue(sound.cropStartValue); + ui->startSoundUnitCombo->setCurrentIndex(sound.cropStartUnit); + ui->stopSoundAtAfterCombo->setCurrentIndex(sound.cropStopAfterAt); + ui->stopSoundValueSpin->setValue(sound.cropStopValue); + ui->stopSoundUnitCombo->setCurrentIndex(sound.cropStopUnit); + ui->colorCheckBox->setChecked(sound.customColorEnabled()); + ui->colorButton->setEnabled(sound.customColorEnabled()); + ui->colorButton->setStyleSheet(QString("background-color: %1").arg(sound.customColor.name())); + this->customColor = sound.customColor; + + updateHotkeyText(); + + m_soundview->setSound(sound); +} + + +void SoundSettingsQt::fillFromGui(SoundInfo &sound) +{ + sound.filename = ui->filenameEdit->text(); + sound.customText = ui->customTextEdit->text(); + sound.volume = ui->soundVolumeSlider->value(); + sound.cropEnabled = ui->groupCrop->isChecked(); + sound.cropStartValue = ui->startSoundValueSpin->value(); + sound.cropStartUnit = ui->startSoundUnitCombo->currentIndex(); + sound.cropStopAfterAt = ui->stopSoundAtAfterCombo->currentIndex(); + sound.cropStopValue = ui->stopSoundValueSpin->value(); + sound.cropStopUnit = ui->stopSoundUnitCombo->currentIndex(); + sound.customColor = this->customColor; +} + +SoundSettingsQt::~SoundSettingsQt() +{ + delete ui; +} + + +void SoundSettingsQt::done( int r ) +{ + Sampler *sampler = sb_getSampler(); + if(sampler->getState() == Sampler::ePLAYING_PREVIEW) + sampler->stopPlayback(); + fillFromGui(m_soundInfo); + QDialog::done(r); +} + + +void SoundSettingsQt::onVolumeChanged(int value) +{ + ui->soundVolumeDbLabel->setText(QString("%1%2 dB").arg(value > 0 ? "+" : "", QString::number(value))); +} + + +void SoundSettingsQt::onBrowsePressed() +{ + QString filePath = ui->filenameEdit->text(); + QString fn = QFileDialog::getOpenFileName(this, tr("Choose File"), filePath, tr("Files (*.*)")); + if(!fn.isNull()) + { + ui->filenameEdit->setText(fn); + ui->customTextEdit->setPlaceholderText(QFileInfo(fn).baseName()); + + SoundInfo info; + fillFromGui(info); + m_soundview->setSound(info); + } +} + + +void SoundSettingsQt::onPreviewPressed() +{ + Sampler *sampler = sb_getSampler(); + if(sampler->getState() != Sampler::ePLAYING_PREVIEW) + { + SoundInfo sound; + fillFromGui(sound); + if(sampler->playPreview(sound)) + { + ui->previewSoundButton->setIcon(m_iconStop); + m_timer->start(100); + } + } + else + { + sampler->stopPlayback(); + ui->previewSoundButton->setIcon(m_iconPlay); + } +} + + +void SoundSettingsQt::onTimer() +{ + Sampler *sampler = sb_getSampler(); + if(sampler->getState() != Sampler::ePLAYING_PREVIEW) + { + ui->previewSoundButton->setIcon(m_iconPlay); + m_timer->stop(); + } +} + + +void SoundSettingsQt::onHotkeyChangePressed() +{ + MainWindow::openHotkeySetDialog(m_buttonId, this); +} + + +void SoundSettingsQt::updateHotkeyText() +{ + QString hotkeyText = MainWindow::getShortcutString(m_buttonId); + ui->hotkeyCurrentLabel->setText(QString("Current hotkey: ") + + (hotkeyText.isEmpty() ? QString("None") : hotkeyText)); +} + + +void SoundSettingsQt::onColorEnabledPressed() +{ + customColor.setAlpha(ui->colorCheckBox->isChecked() ? 255 : 0); + ui->colorButton->setEnabled(ui->colorCheckBox->isChecked()); +} + + +void SoundSettingsQt::onChooseColorPressed() +{ + int alpha = customColor.alpha(); + customColor = QColorDialog::getColor(customColor, this, "Custom button color"); + customColor.setAlpha(alpha); + ui->colorButton->setStyleSheet(QString("background-color: %1").arg(customColor.name())); +} + + +void SoundSettingsQt::updateSoundView() +{ + SoundInfo info; + fillFromGui(info); + m_soundview->setSound(info); + m_soundview->update(); +} + + + + + diff --git a/src/SoundSettings.h b/src/SoundSettings.h index feac998..51aca4e 100644 --- a/src/SoundSettings.h +++ b/src/SoundSettings.h @@ -1,62 +1,62 @@ -// src/SoundSettings.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include -#include -#include -#include -#include "SoundInfo.h" - -class ConfigModel; -class SoundView; - -namespace Ui { - class SoundSettingsQt; -} - - -class SoundSettingsQt: public QDialog -{ - Q_OBJECT - -public: - explicit SoundSettingsQt(const SoundInfo &soundInfo, size_t buttonId, QWidget *parent = 0); - ~SoundSettingsQt(); - const SoundInfo &getSoundInfo() const { return m_soundInfo; } - -protected: - void done(int r); - -private slots: - void onVolumeChanged(int value); - void onBrowsePressed(); - void onPreviewPressed(); - void onTimer(); - void onHotkeyChangePressed(); - void updateHotkeyText(); - void onColorEnabledPressed(); - void onChooseColorPressed(); - void updateSoundView(); - -private: - void initGui(const SoundInfo &sound); - void fillFromGui(SoundInfo &sound); - -private: - Ui::SoundSettingsQt *ui; - SoundInfo m_soundInfo; - size_t m_buttonId; - QIcon m_iconPlay; - QIcon m_iconStop; - QTimer *m_timer; - SoundView *m_soundview; - QColor customColor; -}; - +// src/SoundSettings.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include +#include +#include +#include +#include "SoundInfo.h" + +class ConfigModel; +class SoundView; + +namespace Ui { + class SoundSettingsQt; +} + + +class SoundSettingsQt: public QDialog +{ + Q_OBJECT + +public: + explicit SoundSettingsQt(const SoundInfo &soundInfo, size_t buttonId, QWidget *parent = 0); + ~SoundSettingsQt(); + const SoundInfo &getSoundInfo() const { return m_soundInfo; } + +protected: + void done(int r); + +private slots: + void onVolumeChanged(int value); + void onBrowsePressed(); + void onPreviewPressed(); + void onTimer(); + void onHotkeyChangePressed(); + void updateHotkeyText(); + void onColorEnabledPressed(); + void onChooseColorPressed(); + void updateSoundView(); + +private: + void initGui(const SoundInfo &sound); + void fillFromGui(SoundInfo &sound); + +private: + Ui::SoundSettingsQt *ui; + SoundInfo m_soundInfo; + size_t m_buttonId; + QIcon m_iconPlay; + QIcon m_iconStop; + QTimer *m_timer; + SoundView *m_soundview; + QColor customColor; +}; + diff --git a/src/SoundSettings.ui b/src/SoundSettings.ui index f2255f2..49c48d0 100644 --- a/src/SoundSettings.ui +++ b/src/SoundSettings.ui @@ -1,346 +1,346 @@ - - - SoundSettingsQt - - - - 0 - 0 - 394 - 380 - - - - Sound Settings - - - - :/icon/img/rpmb_icon_64.png:/icon/img/rpmb_icon_64.png - - - - - - - - - Sound File - - - - - - - - - - - Browse - - - - - - - - - - - Volume modifier: - - - - - - - -10 - - - 10 - - - 5 - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 10 - - - - - - - - 40 - 0 - - - - +0 dB - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - Custom button text: - - - - - - - 120 - - - asd - - - - - - - - - - - Custom button color: - - - - - - - - 24 - 24 - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - Crop Sound - - - true - - - - - - - - Start sound at - - - - - - - - - - 10000000 - - - 1 - - - - - - - - - - -1 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Stop sound - - - - - - - - - - 10000000 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - Preview Sound - - - - :/icon/img/playarrow_32.png:/icon/img/playarrow_32.png - - - - - - - Hotkey - - - - - - Current Hotkey: - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Change - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - - buttonBox - accepted() - SoundSettingsQt - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - SoundSettingsQt - reject() - - - 316 - 260 - - - 286 - 274 - - - - - + + + SoundSettingsQt + + + + 0 + 0 + 394 + 380 + + + + Sound Settings + + + + :/icon/img/rpmb_icon_64.png:/icon/img/rpmb_icon_64.png + + + + + + + + + Sound File + + + + + + + + + + + Browse + + + + + + + + + + + Volume modifier: + + + + + + + -10 + + + 10 + + + 5 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 40 + 0 + + + + +0 dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Custom button text: + + + + + + + 120 + + + asd + + + + + + + + + + + Custom button color: + + + + + + + + 24 + 24 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Crop Sound + + + true + + + + + + + + Start sound at + + + + + + + + + + 10000000 + + + 1 + + + + + + + + + + -1 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Stop sound + + + + + + + + + + 10000000 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Preview Sound + + + + :/icon/img/playarrow_32.png:/icon/img/playarrow_32.png + + + + + + + Hotkey + + + + + + Current Hotkey: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Change + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + SoundSettingsQt + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SoundSettingsQt + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/SoundView.cpp b/src/SoundView.cpp index 7b4fc3e..20fbc4d 100644 --- a/src/SoundView.cpp +++ b/src/SoundView.cpp @@ -1,131 +1,131 @@ -// src/SoundView.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - - -#include -#include -#include "SoundView.h" -#include "SampleVisualizerThread.h" - - -SoundView::SoundView( QWidget *parent /*= nullptr*/ ) : - QWidget(parent), - m_timer(new QTimer(this)), - m_drawnBins(0) -{ - connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimer())); -} - - -void SoundView::paintEvent(QPaintEvent *evt) -{ - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing, false); - - painter.setPen(QColor(70, 70, 70)); - painter.setBrush(QColor(30, 30, 30)); - painter.drawRect(QRect(0, 0, width() - 1, height() - 1)); - - painter.setPen(QColor(255, 255, 255)); - painter.setBrush(QColor(255, 255, 255)); - drawWaves(&painter); - - double songLength = SampleVisualizerThread::GetInstance().fileLength(); - double start = m_soundInfo.getStartTime(); - double playTime = m_soundInfo.getPlayTime(); - double end = (playTime > 0.0) ? (start + playTime) : songLength; - int startPixel = int(start / songLength * (width() - 1)); - int endPixel = int(end / songLength * (width() - 1)); - - painter.setPen(QColor(255, 174, 0)); - painter.setBrush(Qt::NoBrush); - painter.drawRect(startPixel, 0, endPixel - startPixel, height() - 1); - - painter.setCompositionMode(QPainter::CompositionMode_Multiply); - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(0, 130, 230, 255)); - painter.drawRect(startPixel + 1, 1, endPixel - startPixel - 1, height() - 2); - - painter.setPen(Qt::NoPen); - painter.setBrush(QColor(100, 100, 100)); - if (start > 0.0) - painter.drawRect(0, 0, startPixel, height() - 1); - if (end < songLength) - painter.drawRect(endPixel + 1, 0, width() - 1 - (endPixel + 1), height() - 1); -} - - -void SoundView::resizeEvent(QResizeEvent *evt) -{ - // Invalidate drawings - m_drawnBins = 0; -} - - -void SoundView::setSound( const SoundInfo &sound ) -{ - bool filenameDiffers = m_soundInfo.filename != sound.filename; - m_soundInfo = sound; - m_drawnBins = 0; - if(filenameDiffers && !sound.filename.isEmpty()) - { - SampleVisualizerThread::GetInstance().startAnalysis(sound.filename.toUtf8(), 1024); - m_timer->start(250); - } - else - { - update(); - } -} - - -void SoundView::onTimer() -{ - update(); -} - - -void SoundView::drawWaves(QPainter *painter) -{ - preparePaths(); - - painter->drawPath(m_path[0]); - painter->drawPath(m_path[1]); -} - - -void SoundView::preparePaths() -{ - SampleVisualizerThread &t = SampleVisualizerThread::GetInstance(); - size_t bins = t.getBinsProcessed(); - if(m_drawnBins < bins) - { - double fhh = (double)height() * 0.5; - double fw = (double)width(); - double fbins = (double)bins; - double shortScale = 1.0 / ((double)std::numeric_limits::max() * 1.1); - m_path[0] = QPainterPath(QPointF(0.0, fhh)); - m_path[1] = QPainterPath(QPointF(0.0, fhh)); - for(size_t i = 0; i < bins; ++i) - { - double binValue0 = (double)t.getBins()[i * 2] * shortScale; - double binValue1 = (double)t.getBins()[i * 2 + 1] * shortScale; - double x = (double)i * (1.0 / 1024.0) * fw; - m_path[0].lineTo(x, (1.0 + binValue0) * fhh); - m_path[1].lineTo(x, (1.0 + binValue1) * fhh); - } - double endx = fw * fbins * (1.0 / 1024.0); - m_path[0].lineTo(endx, fhh); - m_path[1].lineTo(endx, fhh); - m_path[0].closeSubpath(); - m_path[1].closeSubpath(); - m_drawnBins = bins; - } -} - +// src/SoundView.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + + +#include +#include +#include "SoundView.h" +#include "SampleVisualizerThread.h" + + +SoundView::SoundView( QWidget *parent /*= nullptr*/ ) : + QWidget(parent), + m_timer(new QTimer(this)), + m_drawnBins(0) +{ + connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimer())); +} + + +void SoundView::paintEvent(QPaintEvent *evt) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, false); + + painter.setPen(QColor(70, 70, 70)); + painter.setBrush(QColor(30, 30, 30)); + painter.drawRect(QRect(0, 0, width() - 1, height() - 1)); + + painter.setPen(QColor(255, 255, 255)); + painter.setBrush(QColor(255, 255, 255)); + drawWaves(&painter); + + double songLength = SampleVisualizerThread::GetInstance().fileLength(); + double start = m_soundInfo.getStartTime(); + double playTime = m_soundInfo.getPlayTime(); + double end = (playTime > 0.0) ? (start + playTime) : songLength; + int startPixel = int(start / songLength * (width() - 1)); + int endPixel = int(end / songLength * (width() - 1)); + + painter.setPen(QColor(255, 174, 0)); + painter.setBrush(Qt::NoBrush); + painter.drawRect(startPixel, 0, endPixel - startPixel, height() - 1); + + painter.setCompositionMode(QPainter::CompositionMode_Multiply); + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(0, 130, 230, 255)); + painter.drawRect(startPixel + 1, 1, endPixel - startPixel - 1, height() - 2); + + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(100, 100, 100)); + if (start > 0.0) + painter.drawRect(0, 0, startPixel, height() - 1); + if (end < songLength) + painter.drawRect(endPixel + 1, 0, width() - 1 - (endPixel + 1), height() - 1); +} + + +void SoundView::resizeEvent(QResizeEvent *evt) +{ + // Invalidate drawings + m_drawnBins = 0; +} + + +void SoundView::setSound( const SoundInfo &sound ) +{ + bool filenameDiffers = m_soundInfo.filename != sound.filename; + m_soundInfo = sound; + m_drawnBins = 0; + if(filenameDiffers && !sound.filename.isEmpty()) + { + SampleVisualizerThread::GetInstance().startAnalysis(sound.filename.toUtf8(), 1024); + m_timer->start(250); + } + else + { + update(); + } +} + + +void SoundView::onTimer() +{ + update(); +} + + +void SoundView::drawWaves(QPainter *painter) +{ + preparePaths(); + + painter->drawPath(m_path[0]); + painter->drawPath(m_path[1]); +} + + +void SoundView::preparePaths() +{ + SampleVisualizerThread &t = SampleVisualizerThread::GetInstance(); + size_t bins = t.getBinsProcessed(); + if(m_drawnBins < bins) + { + double fhh = (double)height() * 0.5; + double fw = (double)width(); + double fbins = (double)bins; + double shortScale = 1.0 / ((double)std::numeric_limits::max() * 1.1); + m_path[0] = QPainterPath(QPointF(0.0, fhh)); + m_path[1] = QPainterPath(QPointF(0.0, fhh)); + for(size_t i = 0; i < bins; ++i) + { + double binValue0 = (double)t.getBins()[i * 2] * shortScale; + double binValue1 = (double)t.getBins()[i * 2 + 1] * shortScale; + double x = (double)i * (1.0 / 1024.0) * fw; + m_path[0].lineTo(x, (1.0 + binValue0) * fhh); + m_path[1].lineTo(x, (1.0 + binValue1) * fhh); + } + double endx = fw * fbins * (1.0 / 1024.0); + m_path[0].lineTo(endx, fhh); + m_path[1].lineTo(endx, fhh); + m_path[0].closeSubpath(); + m_path[1].closeSubpath(); + m_drawnBins = bins; + } +} + diff --git a/src/SoundView.h b/src/SoundView.h index be8df1a..d244b24 100644 --- a/src/SoundView.h +++ b/src/SoundView.h @@ -1,44 +1,44 @@ -// src/SoundView.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include -#include -#include - -#include "SoundInfo.h" - -class QTimer; - -class SoundView : public QWidget -{ - Q_OBJECT - -public: - SoundView(QWidget *parent = nullptr); - void setSound(const SoundInfo &sound); - -protected: - void paintEvent(QPaintEvent *evt); - void resizeEvent(QResizeEvent *evt); - -private slots: - void onTimer(); - -private: - void drawWaves(QPainter *painter); - void preparePaths(); - -private: - SoundInfo m_soundInfo; - QTimer *m_timer; - size_t m_drawnBins; - QPainterPath m_path[2]; -}; - +// src/SoundView.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include +#include +#include + +#include "SoundInfo.h" + +class QTimer; + +class SoundView : public QWidget +{ + Q_OBJECT + +public: + SoundView(QWidget *parent = nullptr); + void setSound(const SoundInfo &sound); + +protected: + void paintEvent(QPaintEvent *evt); + void resizeEvent(QResizeEvent *evt); + +private slots: + void onTimer(); + +private: + void drawWaves(QPainter *painter); + void preparePaths(); + +private: + SoundInfo m_soundInfo; + QTimer *m_timer; + size_t m_drawnBins; + QPainterPath m_path[2]; +}; + diff --git a/src/SpeechBubble.cpp b/src/SpeechBubble.cpp index b79c9d6..0023b1b 100644 --- a/src/SpeechBubble.cpp +++ b/src/SpeechBubble.cpp @@ -1,214 +1,214 @@ -// src/SpeechBubble.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#include "SpeechBubble.h" -#include -#include -#include -#include -#include -#include - -SpeechBubble::SpeechBubble(QWidget *parent /*= 0*/) : - BaseClass(parent, Qt::FramelessWindowHint), - m_attach(nullptr), - m_tipHeight(25), - m_tipWidth(15), - m_tipDistLeft(10), - m_mouseOverCloseButton(false), - m_closable(true), - m_bubbleStyle(true), - m_backgroundColor(QColor(255, 183, 59)) -{ - setAttribute(Qt::WA_TranslucentBackground); - if (parent) - parent->installEventFilter(this); - setMouseTracking(true); -} - - -void SpeechBubble::setText( const QString &text ) -{ - m_text = text; -} - - -void SpeechBubble::paintEvent(QPaintEvent *evt) -{ - QPainter painter(this); - //painter.setRenderHint(QPainter::Antialiasing, false); - - painter.setPen(QColor(0, 0, 0)); - painter.setBrush(m_backgroundColor); - - // Draw outer appearance - if (m_bubbleStyle) - { - QPoint polygonPoints[] = { - QPoint(0, m_tipHeight), // top left - QPoint(m_tipDistLeft, m_tipHeight), - QPoint(m_tipDistLeft, 0), - QPoint(m_tipDistLeft + m_tipWidth, m_tipHeight), - QPoint(width() - 1, m_tipHeight), // top right - QPoint(width() - 1, height() - 1), // bottom right - QPoint(0, height() - 1) // bottom left - }; - painter.drawPolygon(polygonPoints, 7); - } - else - { - QPoint polygonPoints[] = { - QPoint(0, 0), // top left - QPoint(width() - 1, 0), // top right - QPoint(width() - 1, height() - 1), // bottom right - QPoint(0, height() - 1) // bottom left - }; - painter.drawPolygon(polygonPoints, 4); - } - - // Draw text - QTextOption option; - option.setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); - int maxTextPos = m_bubbleStyle ? m_tipHeight : 0; - painter.drawText(QRect(0, maxTextPos, width() - 1, height() - maxTextPos - 1), m_text, option); - - // Draw closing X - if (m_closable) - { - QRect xRect = getCloseButtonRect(); - if (m_mouseOverCloseButton) - { - painter.drawRect(xRect + QMargins(2, 2, 1, 1)); - painter.setPen(QColor(100, 100, 100)); - } - painter.drawLine(xRect.topLeft(), xRect.bottomRight()); - painter.drawLine(xRect.bottomLeft(), xRect.topRight()); - } - - BaseClass::paintEvent(evt); -} - - -void SpeechBubble::attachTo( QWidget *widget ) -{ - m_attach = widget; - m_attach->installEventFilter(this); - recalcPos(); -} - - -void SpeechBubble::setBackgroundColor(const QColor & color) -{ - m_backgroundColor = color; - update(); -} - - -void SpeechBubble::setClosable(bool closable) -{ - m_closable = closable; - update(); -} - - -void SpeechBubble::setBubbleStyle(bool bubbleStyle) -{ - m_bubbleStyle = bubbleStyle; - update(); -} - - -bool SpeechBubble::eventFilter(QObject *object, QEvent *evt) -{ - if(object == parent()) - { - switch(evt->type()) - { - case QEvent::Move: - case QEvent::Resize: - recalcPos(); - break; - case QEvent::Close: - close(); - break; - case QEvent::Hide: - hide(); - break; - case QEvent::Show: - QTimer::singleShot(0, this, [this](){ - this->recalcPos(); - this->show(); - }); - break; - default: - break; - } - } - - return BaseClass::eventFilter(object, evt); -} - - -void SpeechBubble::mouseReleaseEvent( QMouseEvent *evt ) -{ - if (!m_closable) - return; - - // Mouse inside closing x rect? - if (getCloseButtonRect().contains(this->mapFromGlobal(QCursor::pos()))) - { - emit closePressed(); - delete this; - } -} - - -void SpeechBubble::mouseMoveEvent( QMouseEvent *evt ) -{ - if (!m_closable) - return; - - if ((getCloseButtonRect() + QMargins(1, 1, 1, 1)).contains(evt->pos())) - { - if(!m_mouseOverCloseButton) - { - m_mouseOverCloseButton = true; - repaint(); - } - } - else - { - if(m_mouseOverCloseButton) - { - m_mouseOverCloseButton = false; - repaint(); - } - } -} - - -void SpeechBubble::recalcPos() -{ - QPoint pos; - if (m_bubbleStyle) - pos = QPoint(m_attach->width() / 2 - 10, m_attach->height() / 2); - else - pos = QPoint(m_attach->width() / 2 - width() / 2, m_attach->height() / 2 - height() / 2); - this->move(m_attach->mapToGlobal(pos)); -} - - -QRect SpeechBubble::getCloseButtonRect() -{ - int xsize = 8; - int xborder = 4; - return QRect(width() - xsize - xborder, m_tipHeight + xborder, xsize, xsize); -} - - +// src/SpeechBubble.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#include "SpeechBubble.h" +#include +#include +#include +#include +#include +#include + +SpeechBubble::SpeechBubble(QWidget *parent /*= 0*/) : + BaseClass(parent, Qt::FramelessWindowHint), + m_attach(nullptr), + m_tipHeight(25), + m_tipWidth(15), + m_tipDistLeft(10), + m_mouseOverCloseButton(false), + m_closable(true), + m_bubbleStyle(true), + m_backgroundColor(QColor(255, 183, 59)) +{ + setAttribute(Qt::WA_TranslucentBackground); + if (parent) + parent->installEventFilter(this); + setMouseTracking(true); +} + + +void SpeechBubble::setText( const QString &text ) +{ + m_text = text; +} + + +void SpeechBubble::paintEvent(QPaintEvent *evt) +{ + QPainter painter(this); + //painter.setRenderHint(QPainter::Antialiasing, false); + + painter.setPen(QColor(0, 0, 0)); + painter.setBrush(m_backgroundColor); + + // Draw outer appearance + if (m_bubbleStyle) + { + QPoint polygonPoints[] = { + QPoint(0, m_tipHeight), // top left + QPoint(m_tipDistLeft, m_tipHeight), + QPoint(m_tipDistLeft, 0), + QPoint(m_tipDistLeft + m_tipWidth, m_tipHeight), + QPoint(width() - 1, m_tipHeight), // top right + QPoint(width() - 1, height() - 1), // bottom right + QPoint(0, height() - 1) // bottom left + }; + painter.drawPolygon(polygonPoints, 7); + } + else + { + QPoint polygonPoints[] = { + QPoint(0, 0), // top left + QPoint(width() - 1, 0), // top right + QPoint(width() - 1, height() - 1), // bottom right + QPoint(0, height() - 1) // bottom left + }; + painter.drawPolygon(polygonPoints, 4); + } + + // Draw text + QTextOption option; + option.setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); + int maxTextPos = m_bubbleStyle ? m_tipHeight : 0; + painter.drawText(QRect(0, maxTextPos, width() - 1, height() - maxTextPos - 1), m_text, option); + + // Draw closing X + if (m_closable) + { + QRect xRect = getCloseButtonRect(); + if (m_mouseOverCloseButton) + { + painter.drawRect(xRect + QMargins(2, 2, 1, 1)); + painter.setPen(QColor(100, 100, 100)); + } + painter.drawLine(xRect.topLeft(), xRect.bottomRight()); + painter.drawLine(xRect.bottomLeft(), xRect.topRight()); + } + + BaseClass::paintEvent(evt); +} + + +void SpeechBubble::attachTo( QWidget *widget ) +{ + m_attach = widget; + m_attach->installEventFilter(this); + recalcPos(); +} + + +void SpeechBubble::setBackgroundColor(const QColor & color) +{ + m_backgroundColor = color; + update(); +} + + +void SpeechBubble::setClosable(bool closable) +{ + m_closable = closable; + update(); +} + + +void SpeechBubble::setBubbleStyle(bool bubbleStyle) +{ + m_bubbleStyle = bubbleStyle; + update(); +} + + +bool SpeechBubble::eventFilter(QObject *object, QEvent *evt) +{ + if(object == parent()) + { + switch(evt->type()) + { + case QEvent::Move: + case QEvent::Resize: + recalcPos(); + break; + case QEvent::Close: + close(); + break; + case QEvent::Hide: + hide(); + break; + case QEvent::Show: + QTimer::singleShot(0, this, [this](){ + this->recalcPos(); + this->show(); + }); + break; + default: + break; + } + } + + return BaseClass::eventFilter(object, evt); +} + + +void SpeechBubble::mouseReleaseEvent( QMouseEvent *evt ) +{ + if (!m_closable) + return; + + // Mouse inside closing x rect? + if (getCloseButtonRect().contains(this->mapFromGlobal(QCursor::pos()))) + { + emit closePressed(); + delete this; + } +} + + +void SpeechBubble::mouseMoveEvent( QMouseEvent *evt ) +{ + if (!m_closable) + return; + + if ((getCloseButtonRect() + QMargins(1, 1, 1, 1)).contains(evt->pos())) + { + if(!m_mouseOverCloseButton) + { + m_mouseOverCloseButton = true; + repaint(); + } + } + else + { + if(m_mouseOverCloseButton) + { + m_mouseOverCloseButton = false; + repaint(); + } + } +} + + +void SpeechBubble::recalcPos() +{ + QPoint pos; + if (m_bubbleStyle) + pos = QPoint(m_attach->width() / 2 - 10, m_attach->height() / 2); + else + pos = QPoint(m_attach->width() / 2 - width() / 2, m_attach->height() / 2 - height() / 2); + this->move(m_attach->mapToGlobal(pos)); +} + + +QRect SpeechBubble::getCloseButtonRect() +{ + int xsize = 8; + int xborder = 4; + return QRect(width() - xsize - xborder, m_tipHeight + xborder, xsize, xsize); +} + + diff --git a/src/SpeechBubble.h b/src/SpeechBubble.h index 9d2f7e8..1edc922 100644 --- a/src/SpeechBubble.h +++ b/src/SpeechBubble.h @@ -1,54 +1,54 @@ -// src/SpeechBubble.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#pragma once - -#include -#include - -class QLayout; -class QLabel; - -class SpeechBubble : public QDialog -{ - Q_OBJECT - typedef QDialog BaseClass; - -public: - explicit SpeechBubble(QWidget *parent = 0); - void setText(const QString &text); - void attachTo(QWidget *widget); - void setBackgroundColor(const QColor &color); - void setClosable(bool closable); - void setBubbleStyle(bool bubbleStyle); - -protected: - void paintEvent(QPaintEvent *evt) override; - bool eventFilter(QObject *object, QEvent *evt) override; - void mouseReleaseEvent(QMouseEvent *evt) override; - void mouseMoveEvent(QMouseEvent *evt) override; - -signals: - void closePressed(); - -private: - void recalcPos(); - QRect getCloseButtonRect(); - - QString m_text; - QWidget *m_attach; - int m_tipHeight; - int m_tipWidth; - int m_tipDistLeft; - bool m_mouseOverCloseButton; - bool m_closable; - bool m_bubbleStyle; - QColor m_backgroundColor; -}; - +// src/SpeechBubble.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#pragma once + +#include +#include + +class QLayout; +class QLabel; + +class SpeechBubble : public QDialog +{ + Q_OBJECT + typedef QDialog BaseClass; + +public: + explicit SpeechBubble(QWidget *parent = 0); + void setText(const QString &text); + void attachTo(QWidget *widget); + void setBackgroundColor(const QColor &color); + void setClosable(bool closable); + void setBubbleStyle(bool bubbleStyle); + +protected: + void paintEvent(QPaintEvent *evt) override; + bool eventFilter(QObject *object, QEvent *evt) override; + void mouseReleaseEvent(QMouseEvent *evt) override; + void mouseMoveEvent(QMouseEvent *evt) override; + +signals: + void closePressed(); + +private: + void recalcPos(); + QRect getCloseButtonRect(); + + QString m_text; + QWidget *m_attach; + int m_tipHeight; + int m_tipWidth; + int m_tipDistLeft; + bool m_mouseOverCloseButton; + bool m_closable; + bool m_bubbleStyle; + QColor m_backgroundColor; +}; + diff --git a/src/UpdateChecker.cpp b/src/UpdateChecker.cpp index 6403318..739c818 100644 --- a/src/UpdateChecker.cpp +++ b/src/UpdateChecker.cpp @@ -1,313 +1,313 @@ -// src/UpdateChecker.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -//Parses XML Files from a server -//Example File: - -// -// -// -// -// 1101 -// -// http://mgraefe.de/rpsb/dl/rp_soundboard_1101.ts3_plugin -// -// http://mgraefe.de/rpsb/version/features_1101.txt -// -// - -#include -#include -#include -#include -#include -#include - -#include "buildinfo.h" - -#include "UpdateChecker.h" -#include "ts3log.h" -#include "UpdaterWindow.h" -#include "ConfigModel.h" - -#define CHECK_URL "https://mgraefe.de/rpsb/version/version.xml" - - -std::string toStdStringUtf8(const QString &str) -{ - QByteArray arr = str.toUtf8(); - std::string res(arr.constData(), arr.size()); - return res; -} - -bool isValid(const QXmlStreamReader &xml) -{ - return !(xml.hasError() || xml.atEnd()); -} - - -UpdateChecker::UpdateChecker( QObject *parent /*= nullptr*/ ) : - QObject(parent), - m_updater(nullptr), - m_config(nullptr), - m_explicitCheck(false) -{ - -} - - -void UpdateChecker::startCheck(bool explicitCheck, ConfigModel *config) -{ - m_explicitCheck = explicitCheck; - m_config = config; - - uint currentTime = QDateTime::currentDateTime().toTime_t(); - if (!m_explicitCheck && m_config && currentTime < m_config->getNextUpdateCheck()) - return; - - m_mgr = new QNetworkAccessManager(this); - connect(m_mgr, SIGNAL(finished(QNetworkReply*)), this, SLOT(onFinishDownload(QNetworkReply*))); - - QUrl url(CHECK_URL); - QNetworkRequest request; - request.setUrl(url); - setUserAgent(request); - loading = Loading::mainXml; - m_mgr->get(request); -} - - -void UpdateChecker::onFinishDownload(QNetworkReply *reply) -{ - switch (loading) - { - case Loading::mainXml: - onFinishDownloadXml(reply); - break; - case Loading::features: - onFinishDownloadFeatures(reply); - break; - } -} - - -void UpdateChecker::onFinishDownloadXml(QNetworkReply *reply) -{ - if(reply->error() != QNetworkReply::NoError) - { - std::string err = toStdStringUtf8(reply->errorString()); - logError("UpdateChecker: Error requesting version document %s.\nError-String: %s", CHECK_URL, err.c_str()); - } - else - { - parseXml(reply); - if(m_verInfo.valid() && m_verInfo.build > buildinfo_getVersionNumber(3)) - { - if (!m_verInfo.featuresUrl.isEmpty()) - { - QNetworkRequest request; - request.setUrl(QUrl(m_verInfo.featuresUrl)); - setUserAgent(request); - loading = Loading::features; - m_mgr->get(request); - } - else - askUserForUpdate(); - } - else // no new version - { - if (m_config) - { - // No update found -> don't bother checking again for a day - uint currentTime = QDateTime::currentDateTime().toTime_t(); - m_config->setNextUpdateCheck(currentTime + 60 * 60 * 24); - } - - if (m_explicitCheck) - { - QMessageBox::information(nullptr, "Update Check", "Your version of RP Soundboard is up to date."); - } - } - } -} - - -void UpdateChecker::onFinishDownloadFeatures(QNetworkReply *reply) -{ - if (reply->error() != QNetworkReply::NoError) - { - std::string err = toStdStringUtf8(reply->errorString()); - std::string url = toStdStringUtf8(reply->url().toString()); - logError("UpdateChecker: Error requesting features document %s.\nError-String: %s", url.c_str(), err.c_str()); - } - else - { - m_verInfo.features = reply->readAll(); - } - - askUserForUpdate(); -} - - -void UpdateChecker::parseXml(QIODevice *device) -{ - m_verInfo.reset(); - - QXmlStreamReader xml; - xml.setDevice(device); - - while(isValid(xml)) - { - QXmlStreamReader::TokenType token = xml.readNext(); - if(token == QXmlStreamReader::StartElement && xml.name() == "product") - parseProduct(xml); - } - - xml.clear(); -} - - -void UpdateChecker::parseProduct( QXmlStreamReader &xml ) -{ - if (xml.attributes().value("descVersion") == "1" && - xml.attributes().value("name") == "rp_soundboard") - { - m_verInfo.productName = xml.attributes().value("name").toString(); - xml.readNext(); - while(isValid(xml) && !(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "product")) - { - if(xml.tokenType() == QXmlStreamReader::StartElement) - parseProductInner(xml); - xml.readNext(); - } - } -} - - -void UpdateChecker::parseProductInner( QXmlStreamReader &xml ) -{ - if(xml.name() == "latestVersion") - { - xml.readNext(); - m_verInfo.build = xml.text().toInt(); - } - else if(xml.name() == "latestVersionString") - { - xml.readNext(); - m_verInfo.version = xml.text().toString(); - } - else if(xml.name() == "latestDownload") - { - xml.readNext(); - while(isValid(xml) && !(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "latestDownload")) - { - if(xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == "url") - { - xml.readNext(); - m_verInfo.latestDownload = xml.text().toString(); - } - xml.readNext(); - } - } - else if (xml.name() == "featuresUrl") - { - xml.readNext(); - m_verInfo.featuresUrl = xml.text().toString(); - } -} - - -void UpdateChecker::askUserForUpdate() -{ - QMessageBox msgBox0; - msgBox0.setTextFormat(Qt::RichText); - msgBox0.setText(QString("A new version of RP Soundboard is available (%1).

"\ - "Would you like to download and install it?").arg(m_verInfo.version)); - msgBox0.setIcon(QMessageBox::Information); - msgBox0.setWindowTitle("New version of RP Soundboard!"); - msgBox0.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - msgBox0.setDefaultButton(QMessageBox::Yes); - if (m_verInfo.features.length() > 0) - msgBox0.setDetailedText(m_verInfo.features); - if(msgBox0.exec() == QMessageBox::Yes) - { - QUrl url(m_verInfo.latestDownload); - QFileInfo info(QDir::temp(), url.fileName()); - m_updater = new UpdaterWindow(); - connect(m_updater, SIGNAL(finished()), this, SLOT(onFinishedUpdate())); - m_updater->show(); - m_updater->startDownload(url, info, true); - } - else - { - if (m_config) - { - // Don't bother user for 3 days - uint currentTime = QDateTime::currentDateTime().toTime_t(); - m_config->setNextUpdateCheck(currentTime + 60 * 60 * 24 * 3); - } - } -} - - -void UpdateChecker::onFinishedUpdate() -{ - if(m_updater->getSuccess()) - { - QApplication::closeAllWindows(); - } - else - { - QMessageBox msgBox; - msgBox.setTextFormat(Qt::RichText); - msgBox.setText(QString("The Update to %1 failed.

"\ - "Please download it manually here:
%2") - .arg(m_verInfo.version.isEmpty() ? QString("build %1").arg(m_verInfo.build) : QString("version %1").arg(m_verInfo.version)) - .arg(m_verInfo.latestDownload)); - msgBox.setIcon(QMessageBox::Information); - msgBox.setWindowTitle("Update failed"); - msgBox.setStandardButtons(QMessageBox::Close); - msgBox.setDefaultButton(QMessageBox::Close); - msgBox.exec(); - } - - m_updater->deleteLater(); - m_updater = nullptr; -} - - -void UpdateChecker::version_info_t::reset() -{ - productName = QString(); - build = 0; - latestDownload = QString(); - version = QString(); - featuresUrl = QString(); - features = QString(); -} - - -bool UpdateChecker::version_info_t::valid() -{ - return !productName.isNull() && !productName.isEmpty() && - !latestDownload.isNull() && !latestDownload.isEmpty() && - build > 0; -} - - -QByteArray UpdateChecker::getUserAgent() // static -{ - return QByteArray("RP Soundboard Update Checker, ") + buildinfo_getPluginVersion(); -} - - -void UpdateChecker::setUserAgent(QNetworkRequest& request) // static -{ - request.setRawHeader("User-Agent", getUserAgent()); -} +// src/UpdateChecker.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +//Parses XML Files from a server +//Example File: + +// +// +// +// +// 1101 +// +// http://mgraefe.de/rpsb/dl/rp_soundboard_1101.ts3_plugin +// +// http://mgraefe.de/rpsb/version/features_1101.txt +// +// + +#include +#include +#include +#include +#include +#include + +#include "buildinfo.h" + +#include "UpdateChecker.h" +#include "ts3log.h" +#include "UpdaterWindow.h" +#include "ConfigModel.h" + +#define CHECK_URL "https://mgraefe.de/rpsb/version/version.xml" + + +std::string toStdStringUtf8(const QString &str) +{ + QByteArray arr = str.toUtf8(); + std::string res(arr.constData(), arr.size()); + return res; +} + +bool isValid(const QXmlStreamReader &xml) +{ + return !(xml.hasError() || xml.atEnd()); +} + + +UpdateChecker::UpdateChecker( QObject *parent /*= nullptr*/ ) : + QObject(parent), + m_updater(nullptr), + m_config(nullptr), + m_explicitCheck(false) +{ + +} + + +void UpdateChecker::startCheck(bool explicitCheck, ConfigModel *config) +{ + m_explicitCheck = explicitCheck; + m_config = config; + + uint currentTime = QDateTime::currentDateTime().toTime_t(); + if (!m_explicitCheck && m_config && currentTime < m_config->getNextUpdateCheck()) + return; + + m_mgr = new QNetworkAccessManager(this); + connect(m_mgr, SIGNAL(finished(QNetworkReply*)), this, SLOT(onFinishDownload(QNetworkReply*))); + + QUrl url(CHECK_URL); + QNetworkRequest request; + request.setUrl(url); + setUserAgent(request); + loading = Loading::mainXml; + m_mgr->get(request); +} + + +void UpdateChecker::onFinishDownload(QNetworkReply *reply) +{ + switch (loading) + { + case Loading::mainXml: + onFinishDownloadXml(reply); + break; + case Loading::features: + onFinishDownloadFeatures(reply); + break; + } +} + + +void UpdateChecker::onFinishDownloadXml(QNetworkReply *reply) +{ + if(reply->error() != QNetworkReply::NoError) + { + std::string err = toStdStringUtf8(reply->errorString()); + logError("UpdateChecker: Error requesting version document %s.\nError-String: %s", CHECK_URL, err.c_str()); + } + else + { + parseXml(reply); + if(m_verInfo.valid() && m_verInfo.build > buildinfo_getVersionNumber(3)) + { + if (!m_verInfo.featuresUrl.isEmpty()) + { + QNetworkRequest request; + request.setUrl(QUrl(m_verInfo.featuresUrl)); + setUserAgent(request); + loading = Loading::features; + m_mgr->get(request); + } + else + askUserForUpdate(); + } + else // no new version + { + if (m_config) + { + // No update found -> don't bother checking again for a day + uint currentTime = QDateTime::currentDateTime().toTime_t(); + m_config->setNextUpdateCheck(currentTime + 60 * 60 * 24); + } + + if (m_explicitCheck) + { + QMessageBox::information(nullptr, "Update Check", "Your version of RP Soundboard is up to date."); + } + } + } +} + + +void UpdateChecker::onFinishDownloadFeatures(QNetworkReply *reply) +{ + if (reply->error() != QNetworkReply::NoError) + { + std::string err = toStdStringUtf8(reply->errorString()); + std::string url = toStdStringUtf8(reply->url().toString()); + logError("UpdateChecker: Error requesting features document %s.\nError-String: %s", url.c_str(), err.c_str()); + } + else + { + m_verInfo.features = reply->readAll(); + } + + askUserForUpdate(); +} + + +void UpdateChecker::parseXml(QIODevice *device) +{ + m_verInfo.reset(); + + QXmlStreamReader xml; + xml.setDevice(device); + + while(isValid(xml)) + { + QXmlStreamReader::TokenType token = xml.readNext(); + if(token == QXmlStreamReader::StartElement && xml.name() == "product") + parseProduct(xml); + } + + xml.clear(); +} + + +void UpdateChecker::parseProduct( QXmlStreamReader &xml ) +{ + if (xml.attributes().value("descVersion") == "1" && + xml.attributes().value("name") == "rp_soundboard") + { + m_verInfo.productName = xml.attributes().value("name").toString(); + xml.readNext(); + while(isValid(xml) && !(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "product")) + { + if(xml.tokenType() == QXmlStreamReader::StartElement) + parseProductInner(xml); + xml.readNext(); + } + } +} + + +void UpdateChecker::parseProductInner( QXmlStreamReader &xml ) +{ + if(xml.name() == "latestVersion") + { + xml.readNext(); + m_verInfo.build = xml.text().toInt(); + } + else if(xml.name() == "latestVersionString") + { + xml.readNext(); + m_verInfo.version = xml.text().toString(); + } + else if(xml.name() == "latestDownload") + { + xml.readNext(); + while(isValid(xml) && !(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "latestDownload")) + { + if(xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == "url") + { + xml.readNext(); + m_verInfo.latestDownload = xml.text().toString(); + } + xml.readNext(); + } + } + else if (xml.name() == "featuresUrl") + { + xml.readNext(); + m_verInfo.featuresUrl = xml.text().toString(); + } +} + + +void UpdateChecker::askUserForUpdate() +{ + QMessageBox msgBox0; + msgBox0.setTextFormat(Qt::RichText); + msgBox0.setText(QString("A new version of RP Soundboard is available (%1).

"\ + "Would you like to download and install it?").arg(m_verInfo.version)); + msgBox0.setIcon(QMessageBox::Information); + msgBox0.setWindowTitle("New version of RP Soundboard!"); + msgBox0.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox0.setDefaultButton(QMessageBox::Yes); + if (m_verInfo.features.length() > 0) + msgBox0.setDetailedText(m_verInfo.features); + if(msgBox0.exec() == QMessageBox::Yes) + { + QUrl url(m_verInfo.latestDownload); + QFileInfo info(QDir::temp(), url.fileName()); + m_updater = new UpdaterWindow(); + connect(m_updater, SIGNAL(finished()), this, SLOT(onFinishedUpdate())); + m_updater->show(); + m_updater->startDownload(url, info, true); + } + else + { + if (m_config) + { + // Don't bother user for 3 days + uint currentTime = QDateTime::currentDateTime().toTime_t(); + m_config->setNextUpdateCheck(currentTime + 60 * 60 * 24 * 3); + } + } +} + + +void UpdateChecker::onFinishedUpdate() +{ + if(m_updater->getSuccess()) + { + QApplication::closeAllWindows(); + } + else + { + QMessageBox msgBox; + msgBox.setTextFormat(Qt::RichText); + msgBox.setText(QString("The Update to %1 failed.

"\ + "Please download it manually here:
%2") + .arg(m_verInfo.version.isEmpty() ? QString("build %1").arg(m_verInfo.build) : QString("version %1").arg(m_verInfo.version)) + .arg(m_verInfo.latestDownload)); + msgBox.setIcon(QMessageBox::Information); + msgBox.setWindowTitle("Update failed"); + msgBox.setStandardButtons(QMessageBox::Close); + msgBox.setDefaultButton(QMessageBox::Close); + msgBox.exec(); + } + + m_updater->deleteLater(); + m_updater = nullptr; +} + + +void UpdateChecker::version_info_t::reset() +{ + productName = QString(); + build = 0; + latestDownload = QString(); + version = QString(); + featuresUrl = QString(); + features = QString(); +} + + +bool UpdateChecker::version_info_t::valid() +{ + return !productName.isNull() && !productName.isEmpty() && + !latestDownload.isNull() && !latestDownload.isEmpty() && + build > 0; +} + + +QByteArray UpdateChecker::getUserAgent() // static +{ + return QByteArray("RP Soundboard Update Checker, ") + buildinfo_getPluginVersion(); +} + + +void UpdateChecker::setUserAgent(QNetworkRequest& request) // static +{ + request.setRawHeader("User-Agent", getUserAgent()); +} diff --git a/src/UpdateChecker.h b/src/UpdateChecker.h index aa6caf4..40503a6 100644 --- a/src/UpdateChecker.h +++ b/src/UpdateChecker.h @@ -1,69 +1,69 @@ -// src/UpdateChecker.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include -#include -#include - -class QNetworkReply; -class QNetworkAccessManager; -class UpdaterWindow; -class ConfigModel; - -class UpdateChecker : public QObject -{ - Q_OBJECT - -public: - struct version_info_t - { - QString productName; - int build; - QString latestDownload; - QString version; - QString featuresUrl; - QString features; - - void reset(); - bool valid(); - }; - -public: - explicit UpdateChecker(QObject *parent = nullptr); - void startCheck(bool explicitCheck = true, ConfigModel *config = nullptr); - static QByteArray getUserAgent(); - static void setUserAgent(QNetworkRequest &request); - -public slots: - void onFinishedUpdate(); - void onFinishDownload(QNetworkReply *reply); - -private: - void parseXml(QIODevice *device); - void parseProduct(QXmlStreamReader &xml); - void parseProductInner(QXmlStreamReader &xml); - void onFinishDownloadXml(QNetworkReply *reply); - void onFinishDownloadFeatures(QNetworkReply * reply); - void askUserForUpdate(); - -private: - enum class Loading - { - mainXml, - features, - } loading; - - QNetworkAccessManager *m_mgr; - version_info_t m_verInfo; - UpdaterWindow *m_updater; - ConfigModel *m_config; - bool m_explicitCheck; -}; - +// src/UpdateChecker.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include +#include +#include + +class QNetworkReply; +class QNetworkAccessManager; +class UpdaterWindow; +class ConfigModel; + +class UpdateChecker : public QObject +{ + Q_OBJECT + +public: + struct version_info_t + { + QString productName; + int build; + QString latestDownload; + QString version; + QString featuresUrl; + QString features; + + void reset(); + bool valid(); + }; + +public: + explicit UpdateChecker(QObject *parent = nullptr); + void startCheck(bool explicitCheck = true, ConfigModel *config = nullptr); + static QByteArray getUserAgent(); + static void setUserAgent(QNetworkRequest &request); + +public slots: + void onFinishedUpdate(); + void onFinishDownload(QNetworkReply *reply); + +private: + void parseXml(QIODevice *device); + void parseProduct(QXmlStreamReader &xml); + void parseProductInner(QXmlStreamReader &xml); + void onFinishDownloadXml(QNetworkReply *reply); + void onFinishDownloadFeatures(QNetworkReply * reply); + void askUserForUpdate(); + +private: + enum class Loading + { + mainXml, + features, + } loading; + + QNetworkAccessManager *m_mgr; + version_info_t m_verInfo; + UpdaterWindow *m_updater; + ConfigModel *m_config; + bool m_explicitCheck; +}; + diff --git a/src/UpdaterWindow.cpp b/src/UpdaterWindow.cpp index 9aa413e..076b7d0 100644 --- a/src/UpdaterWindow.cpp +++ b/src/UpdaterWindow.cpp @@ -1,184 +1,184 @@ -// src/UpdaterWindow.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#include "buildinfo.h" -#include "UpdaterWindow.h" -#include "ts3log.h" -#include -#include - - - -UpdaterWindow::UpdaterWindow( QWidget *parent /*= 0*/ ) : - QDialog(parent), - ui(new Ui::updaterWindow), - m_file(nullptr), - m_manager(nullptr), - m_reply(nullptr), - m_redirects(0), - m_execute(false), - m_canceled(false), - m_success(false) -{ - ui->setupUi(this); - connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(onClickedCancel(QAbstractButton*))); -} - - -void UpdaterWindow::startDownload(const QUrl &url, const QFileInfo &fileInfo, bool execute /*= false*/) -{ - m_url = url; - m_fileinfo = fileInfo; - m_execute = execute; - - if(m_fileinfo.fileName().isEmpty()) - { - QFileInfo info(url.path()); - m_fileinfo.setFile(info.fileName()); - } - - logInfo("Downloading update from '%s' to '%s'", m_url.toString().toUtf8().data(), m_fileinfo.absoluteFilePath().toUtf8().data()); - - m_file = new QFile(m_fileinfo.filePath()); - if(!m_file->open(QIODevice::WriteOnly)) - { - logError("Unable to write to file %s", m_fileinfo.absoluteFilePath().toUtf8().data()); - QMessageBox::information(this, "Error", "Unable to save the file."); - delete m_file; - m_file = nullptr; - return; - } - - ui->statusLabel->setText("Downloading Update..."); - m_manager = new QNetworkAccessManager(this); - startRequest(url); -} - - -void UpdaterWindow::startRequest(const QUrl &url) -{ - QNetworkRequest request(url); - request.setRawHeader("User-Agent", QByteArray("RP Soundboard Updater, ") + buildinfo_getPluginVersion()); - m_reply = m_manager->get(QNetworkRequest(url)); - connect(m_reply, SIGNAL(readyRead()), this, SLOT(onReadyRead())); - connect(m_reply, SIGNAL(downloadProgress(qint64,qint64)), this, - SLOT(onDownloadProgress(qint64, qint64))); - connect(m_reply, SIGNAL(finished()), this, SLOT(onFinished())); -} - - -void UpdaterWindow::onReadyRead() -{ - if(m_file) - m_file->write(m_reply->readAll()); -} - - -void UpdaterWindow::onDownloadProgress(qint64 bytes, qint64 total) -{ - if(m_canceled) - return; - - ui->progressBar->setMaximum(total); - ui->progressBar->setValue(bytes); -} - - -void UpdaterWindow::onFinished() -{ - if(m_canceled) - { - logDebug("Canceled download of update"); - m_canceled = false; - if(m_file) - { - m_file->close(); - m_file->remove(); - delete m_file; - m_file = nullptr; - } - m_reply->deleteLater(); - m_reply = nullptr; - this->hide(); - } - else - { - m_file->flush(); - m_file->close(); - - QVariant redirect = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute); - if(m_reply->error()) - { - m_file->remove(); - QMessageBox::information(this, "Error", QString("Could not download file: ") + m_reply->errorString()); - } - else if(redirect.isValid()) - { - if(m_redirects < 10) - { - m_redirects++; - QUrl url = m_url.resolved(redirect.toUrl()); - logInfo("Download of update redirected to %s", url.toString().toUtf8().data()); - m_url = url; - m_reply->deleteLater(); - m_reply = nullptr; - m_file->open(QIODevice::WriteOnly); - m_file->resize(0); - startRequest(url); - return; - } - else - { - logError("Download of update redirected more than 10 times, maybe a redirection loop?"); - m_success = false; - } - } - else - { - m_success = true; - if(m_execute) - m_success = executeFile(); - this->hide(); - } - - m_reply->deleteLater(); - m_reply = nullptr; - delete m_file; - m_file = nullptr; - m_manager = nullptr; - m_redirects = 0; - } - - emit finished(); -} - - -bool UpdaterWindow::executeFile() -{ - bool status = QProcess::startDetached("package_inst.exe", - QStringList(m_fileinfo.absoluteFilePath())); - if(!status) - logError("Error starting the update process package_inst.exe with cmd line \"%s\"", - m_fileinfo.absoluteFilePath().toUtf8().data()); - return status; -} - - -void UpdaterWindow::onClickedCancel(QAbstractButton*) -{ - if(!m_canceled) - { - logDebug("Cancelling download of update..."); - m_canceled = true; - ui->statusLabel->setText("Canceling Download..."); - m_reply->abort(); - } -} - - +// src/UpdaterWindow.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#include "buildinfo.h" +#include "UpdaterWindow.h" +#include "ts3log.h" +#include +#include + + + +UpdaterWindow::UpdaterWindow( QWidget *parent /*= 0*/ ) : + QDialog(parent), + ui(new Ui::updaterWindow), + m_file(nullptr), + m_manager(nullptr), + m_reply(nullptr), + m_redirects(0), + m_execute(false), + m_canceled(false), + m_success(false) +{ + ui->setupUi(this); + connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(onClickedCancel(QAbstractButton*))); +} + + +void UpdaterWindow::startDownload(const QUrl &url, const QFileInfo &fileInfo, bool execute /*= false*/) +{ + m_url = url; + m_fileinfo = fileInfo; + m_execute = execute; + + if(m_fileinfo.fileName().isEmpty()) + { + QFileInfo info(url.path()); + m_fileinfo.setFile(info.fileName()); + } + + logInfo("Downloading update from '%s' to '%s'", m_url.toString().toUtf8().data(), m_fileinfo.absoluteFilePath().toUtf8().data()); + + m_file = new QFile(m_fileinfo.filePath()); + if(!m_file->open(QIODevice::WriteOnly)) + { + logError("Unable to write to file %s", m_fileinfo.absoluteFilePath().toUtf8().data()); + QMessageBox::information(this, "Error", "Unable to save the file."); + delete m_file; + m_file = nullptr; + return; + } + + ui->statusLabel->setText("Downloading Update..."); + m_manager = new QNetworkAccessManager(this); + startRequest(url); +} + + +void UpdaterWindow::startRequest(const QUrl &url) +{ + QNetworkRequest request(url); + request.setRawHeader("User-Agent", QByteArray("RP Soundboard Updater, ") + buildinfo_getPluginVersion()); + m_reply = m_manager->get(QNetworkRequest(url)); + connect(m_reply, SIGNAL(readyRead()), this, SLOT(onReadyRead())); + connect(m_reply, SIGNAL(downloadProgress(qint64,qint64)), this, + SLOT(onDownloadProgress(qint64, qint64))); + connect(m_reply, SIGNAL(finished()), this, SLOT(onFinished())); +} + + +void UpdaterWindow::onReadyRead() +{ + if(m_file) + m_file->write(m_reply->readAll()); +} + + +void UpdaterWindow::onDownloadProgress(qint64 bytes, qint64 total) +{ + if(m_canceled) + return; + + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(bytes); +} + + +void UpdaterWindow::onFinished() +{ + if(m_canceled) + { + logDebug("Canceled download of update"); + m_canceled = false; + if(m_file) + { + m_file->close(); + m_file->remove(); + delete m_file; + m_file = nullptr; + } + m_reply->deleteLater(); + m_reply = nullptr; + this->hide(); + } + else + { + m_file->flush(); + m_file->close(); + + QVariant redirect = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute); + if(m_reply->error()) + { + m_file->remove(); + QMessageBox::information(this, "Error", QString("Could not download file: ") + m_reply->errorString()); + } + else if(redirect.isValid()) + { + if(m_redirects < 10) + { + m_redirects++; + QUrl url = m_url.resolved(redirect.toUrl()); + logInfo("Download of update redirected to %s", url.toString().toUtf8().data()); + m_url = url; + m_reply->deleteLater(); + m_reply = nullptr; + m_file->open(QIODevice::WriteOnly); + m_file->resize(0); + startRequest(url); + return; + } + else + { + logError("Download of update redirected more than 10 times, maybe a redirection loop?"); + m_success = false; + } + } + else + { + m_success = true; + if(m_execute) + m_success = executeFile(); + this->hide(); + } + + m_reply->deleteLater(); + m_reply = nullptr; + delete m_file; + m_file = nullptr; + m_manager = nullptr; + m_redirects = 0; + } + + emit finished(); +} + + +bool UpdaterWindow::executeFile() +{ + bool status = QProcess::startDetached("package_inst.exe", + QStringList(m_fileinfo.absoluteFilePath())); + if(!status) + logError("Error starting the update process package_inst.exe with cmd line \"%s\"", + m_fileinfo.absoluteFilePath().toUtf8().data()); + return status; +} + + +void UpdaterWindow::onClickedCancel(QAbstractButton*) +{ + if(!m_canceled) + { + logDebug("Cancelling download of update..."); + m_canceled = true; + ui->statusLabel->setText("Canceling Download..."); + m_reply->abort(); + } +} + + diff --git a/src/UpdaterWindow.h b/src/UpdaterWindow.h index 2190ea0..c90cb39 100644 --- a/src/UpdaterWindow.h +++ b/src/UpdaterWindow.h @@ -1,59 +1,59 @@ -// src/UpdaterWindow.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "ui_UpdaterWindow.h" - -namespace Ui { - class updaterWindow; -} - - -class UpdaterWindow : public QDialog -{ - Q_OBJECT - -public: - explicit UpdaterWindow(QWidget *parent = 0); - void startDownload(const QUrl &url, const QFileInfo &fileInfo, bool execute = false); - void startRequest(const QUrl & url); - inline bool getSuccess() const { - return m_success; - } - -public slots: - void onReadyRead(); - void onDownloadProgress(qint64 bytes, qint64 total); - void onClickedCancel(QAbstractButton*); - void onFinished(); - -signals: - void finished(); - -private: - bool executeFile(); - Ui::updaterWindow *ui; - QUrl m_url; - QFileInfo m_fileinfo; - QFile *m_file; - QNetworkAccessManager *m_manager; - QNetworkReply *m_reply; - int m_redirects; - bool m_execute; - bool m_canceled; - bool m_success; -}; - +// src/UpdaterWindow.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "ui_UpdaterWindow.h" + +namespace Ui { + class updaterWindow; +} + + +class UpdaterWindow : public QDialog +{ + Q_OBJECT + +public: + explicit UpdaterWindow(QWidget *parent = 0); + void startDownload(const QUrl &url, const QFileInfo &fileInfo, bool execute = false); + void startRequest(const QUrl & url); + inline bool getSuccess() const { + return m_success; + } + +public slots: + void onReadyRead(); + void onDownloadProgress(qint64 bytes, qint64 total); + void onClickedCancel(QAbstractButton*); + void onFinished(); + +signals: + void finished(); + +private: + bool executeFile(); + Ui::updaterWindow *ui; + QUrl m_url; + QFileInfo m_fileinfo; + QFile *m_file; + QNetworkAccessManager *m_manager; + QNetworkReply *m_reply; + int m_redirects; + bool m_execute; + bool m_canceled; + bool m_success; +}; + diff --git a/src/buildinfo.c b/src/buildinfo.c index 56cceb7..ba681b7 100644 --- a/src/buildinfo.c +++ b/src/buildinfo.c @@ -1,107 +1,107 @@ - -#include "buildinfo.h" -#include "version/version.h" - - -#ifdef NDEBUG -#define BUILD_TARGET "Release" -#else -#define BUILD_TARGET "Debug" -#endif - -#if defined(_WIN64) -#define BUILD_NAME "Win 64-bit" -#elif defined(_WIN32) -#define BUILD_NAME "Win 32-bit" -#elif defined(LINUX) && defined(__x86_64__) -#define BUILD_NAME "Linux 64-bit" -#elif defined(LINUX) && defined(__i386__) -#define BUILD_NAME "Linux 32-bit" -#elif defined(MACOS) -#define BUILD_NAME "MacOS 64-bit" -#else -#define BUILD_NAME "Unknown Build" -#endif - -#define PLUGIN_NAME "RP Soundboard" - -#define PLUGIN_VERSION (TS3SB_VERSION_S " " BUILD_TARGET " " BUILD_NAME) -#define PLUGIN_AUTHOR ("Marius Gr\xC3\xA4" "fe") -#define PLUGIN_DESCRIPTION ("Easy to use Soundboard.\n" \ - "This software uses libraries from the FFmpeg project under the LGPLv2.1. " \ - "A copy of the used source code along with the relevant configuration files " \ - "can be downloaded here: https://github.com/MGraefe/RP-Soundboard") - - -const char * buildinfo_getPluginVersion() -{ - return PLUGIN_VERSION; -} - - -const char * buildinfo_getPluginName() -{ - return PLUGIN_NAME; -} - - -const char * buildinfo_getPluginAuthor() -{ - return PLUGIN_AUTHOR; -} - - -const char * buildinfo_getPluginDescription() -{ - return PLUGIN_DESCRIPTION; -} - - -const char * buildinfo_getBuildName() -{ - return BUILD_NAME; -} - - -const char * buildinfo_getPluginVersionShort() -{ - return TS3SB_VERSION_S; -} - - -//index: 0 = major, 1 = minor, 2 = revision, 3 = build -int buildinfo_getVersionNumber( int index ) -{ - switch(index) - { - case 0: return TS3SB_VERSION_MAJOR; - case 1: return TS3SB_VERSION_MINOR; - case 2: return TS3SB_VERSION_REVISION; - case 3: return TS3SB_VERSION_BUILD; - default: return -1; - } -} - - -const char * buildinfo_getBuildTarget() -{ - return BUILD_TARGET; -} - - -int buildinfo_getBuildNumber() -{ - return TS3SB_VERSION_BUILD; -} - - -const char *buildinfo_getBuildDate() -{ - return __DATE__; -} - - -const char *buildinfo_getBuildTime() -{ - return __TIME__; + +#include "buildinfo.h" +#include "version/version.h" + + +#ifdef NDEBUG +#define BUILD_TARGET "Release" +#else +#define BUILD_TARGET "Debug" +#endif + +#if defined(_WIN64) +#define BUILD_NAME "Win 64-bit" +#elif defined(_WIN32) +#define BUILD_NAME "Win 32-bit" +#elif defined(LINUX) && defined(__x86_64__) +#define BUILD_NAME "Linux 64-bit" +#elif defined(LINUX) && defined(__i386__) +#define BUILD_NAME "Linux 32-bit" +#elif defined(MACOS) +#define BUILD_NAME "MacOS 64-bit" +#else +#define BUILD_NAME "Unknown Build" +#endif + +#define PLUGIN_NAME "RP Soundboard" + +#define PLUGIN_VERSION (TS3SB_VERSION_S " " BUILD_TARGET " " BUILD_NAME) +#define PLUGIN_AUTHOR ("Marius Gr\xC3\xA4" "fe") +#define PLUGIN_DESCRIPTION ("Easy to use Soundboard.\n" \ + "This software uses libraries from the FFmpeg project under the LGPLv2.1. " \ + "A copy of the used source code along with the relevant configuration files " \ + "can be downloaded here: https://github.com/MGraefe/RP-Soundboard") + + +const char * buildinfo_getPluginVersion() +{ + return PLUGIN_VERSION; +} + + +const char * buildinfo_getPluginName() +{ + return PLUGIN_NAME; +} + + +const char * buildinfo_getPluginAuthor() +{ + return PLUGIN_AUTHOR; +} + + +const char * buildinfo_getPluginDescription() +{ + return PLUGIN_DESCRIPTION; +} + + +const char * buildinfo_getBuildName() +{ + return BUILD_NAME; +} + + +const char * buildinfo_getPluginVersionShort() +{ + return TS3SB_VERSION_S; +} + + +//index: 0 = major, 1 = minor, 2 = revision, 3 = build +int buildinfo_getVersionNumber( int index ) +{ + switch(index) + { + case 0: return TS3SB_VERSION_MAJOR; + case 1: return TS3SB_VERSION_MINOR; + case 2: return TS3SB_VERSION_REVISION; + case 3: return TS3SB_VERSION_BUILD; + default: return -1; + } +} + + +const char * buildinfo_getBuildTarget() +{ + return BUILD_TARGET; +} + + +int buildinfo_getBuildNumber() +{ + return TS3SB_VERSION_BUILD; +} + + +const char *buildinfo_getBuildDate() +{ + return __DATE__; +} + + +const char *buildinfo_getBuildTime() +{ + return __TIME__; } \ No newline at end of file diff --git a/src/buildinfo.h b/src/buildinfo.h index c3c2504..4b733a4 100644 --- a/src/buildinfo.h +++ b/src/buildinfo.h @@ -1,35 +1,35 @@ -// src/buildinfo.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -const char *buildinfo_getPluginVersion(); -const char *buildinfo_getPluginName(); -const char *buildinfo_getPluginAuthor(); -const char *buildinfo_getPluginDescription(); -const char *buildinfo_getBuildName(); -const char *buildinfo_getPluginVersionShort(); -const char *buildinfo_getBuildTarget(); -const char *buildinfo_getBuildDate(); -const char *buildinfo_getBuildTime(); - - -//index: 0 = major, 1 = minor, 2 = revision, 3 = build -int buildinfo_getVersionNumber(int index); -int buildinfo_getBuildNumber(); - - -#ifdef __cplusplus -} // extern "C" -#endif - +// src/buildinfo.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +const char *buildinfo_getPluginVersion(); +const char *buildinfo_getPluginName(); +const char *buildinfo_getPluginAuthor(); +const char *buildinfo_getPluginDescription(); +const char *buildinfo_getBuildName(); +const char *buildinfo_getPluginVersionShort(); +const char *buildinfo_getBuildTarget(); +const char *buildinfo_getBuildDate(); +const char *buildinfo_getBuildTime(); + + +//index: 0 = major, 1 = minor, 2 = revision, 3 = build +int buildinfo_getVersionNumber(int index); +int buildinfo_getBuildNumber(); + + +#ifdef __cplusplus +} // extern "C" +#endif + diff --git a/src/common.h b/src/common.h index b19fcac..2527afd 100644 --- a/src/common.h +++ b/src/common.h @@ -1,59 +1,59 @@ -// src/common.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include -#include - -#ifdef __cplusplus -extern "C" -{ -#endif - #include "teamspeak/public_errors.h" - #include "teamspeak/public_errors_rare.h" - #include "teamspeak/public_definitions.h" - #include "teamspeak/public_rare_definitions.h" -#ifdef __cplusplus -} -#endif - -#include "ts3_functions.h" - - -#ifndef CAPI -#ifdef __cplusplus -#define CAPI extern "C" -#else -#define CAPI extern -#endif -#endif - -#ifdef __cplusplus - #include - #ifdef UNICODE - typedef std::wstring tstring; - #else - typedef std::string tstring; - #endif -#endif // __cplusplus - -typedef uint32_t UINT; -typedef uint16_t USHORT; - -CAPI struct TS3Functions ts3Functions; - -#define PATH_BUFSIZE 512 -#define COMMAND_BUFSIZE 128 -#define INFODATA_BUFSIZE 128 -#define SERVERINFO_BUFSIZE 256 -#define CHANNELINFO_BUFSIZE 512 -#define RETURNCODE_BUFSIZE 128 - -#define NUM_CONFIGS 4 - +// src/common.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + #include "teamspeak/public_errors.h" + #include "teamspeak/public_errors_rare.h" + #include "teamspeak/public_definitions.h" + #include "teamspeak/public_rare_definitions.h" +#ifdef __cplusplus +} +#endif + +#include "ts3_functions.h" + + +#ifndef CAPI +#ifdef __cplusplus +#define CAPI extern "C" +#else +#define CAPI extern +#endif +#endif + +#ifdef __cplusplus + #include + #ifdef UNICODE + typedef std::wstring tstring; + #else + typedef std::string tstring; + #endif +#endif // __cplusplus + +typedef uint32_t UINT; +typedef uint16_t USHORT; + +CAPI struct TS3Functions ts3Functions; + +#define PATH_BUFSIZE 512 +#define COMMAND_BUFSIZE 128 +#define INFODATA_BUFSIZE 128 +#define SERVERINFO_BUFSIZE 256 +#define CHANNELINFO_BUFSIZE 512 +#define RETURNCODE_BUFSIZE 128 + +#define NUM_CONFIGS 4 + diff --git a/src/inputfile.h b/src/inputfile.h index 59504e0..0bf09bd 100644 --- a/src/inputfile.h +++ b/src/inputfile.h @@ -1,56 +1,56 @@ -// src/inputfile.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include -#include "SampleSource.h" - -class SampleBuffer; - -struct InputFileOptions -{ - enum channel_layout_e - { - MONO = 0, - STEREO, - }; - - channel_layout_e outputChannelLayout; - int outputSampleRate; - - InputFileOptions() : - outputChannelLayout(STEREO), - outputSampleRate(48000) - {} - - inline int getNumChannels() const - { - switch(outputChannelLayout) - { - case MONO: return 1; - case STEREO: return 2; - default: return 0; - } - } -}; - - -class InputFile : public SampleSource -{ -public: - virtual ~InputFile() {}; - virtual int open(const char *filename, double startPosSeconds = 0.0, double playTimeSeconds = -1.0) = 0; - virtual int close() = 0; - virtual bool done() const = 0; - virtual int seek(double seconds) = 0; - virtual int64_t outputSamplesEstimation() const = 0; -}; - -extern InputFile *CreateInputFileFFmpeg(InputFileOptions options = InputFileOptions()); - +// src/inputfile.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include +#include "SampleSource.h" + +class SampleBuffer; + +struct InputFileOptions +{ + enum channel_layout_e + { + MONO = 0, + STEREO, + }; + + channel_layout_e outputChannelLayout; + int outputSampleRate; + + InputFileOptions() : + outputChannelLayout(STEREO), + outputSampleRate(48000) + {} + + inline int getNumChannels() const + { + switch(outputChannelLayout) + { + case MONO: return 1; + case STEREO: return 2; + default: return 0; + } + } +}; + + +class InputFile : public SampleSource +{ +public: + virtual ~InputFile() {}; + virtual int open(const char *filename, double startPosSeconds = 0.0, double playTimeSeconds = -1.0) = 0; + virtual int close() = 0; + virtual bool done() const = 0; + virtual int seek(double seconds) = 0; + virtual int64_t outputSamplesEstimation() const = 0; +}; + +extern InputFile *CreateInputFileFFmpeg(InputFileOptions options = InputFileOptions()); + diff --git a/src/inputfileffmpeg.cpp b/src/inputfileffmpeg.cpp index c3fdf23..e4dba2e 100644 --- a/src/inputfileffmpeg.cpp +++ b/src/inputfileffmpeg.cpp @@ -1,496 +1,496 @@ -// src/inputfileffmpeg.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#include "common.h" - -#ifdef _WIN32 -#define NOMINMAX -#define WIN32_LEAN_AND_MEAN -#include -#endif - -#include -#include -#include - -#include "ts3log.h" -#include "inputfile.h" -#include "SampleBuffer.h" -#include "SampleSource.h" -#include "main.h" -#include - -extern "C" -{ -#include -#include -#include -#include -} - - -#define OUTPUT_BUFFER_COUNT 32768 -#define OUTPUT_FORMAT AV_SAMPLE_FMT_S16 - - -int checkFFmpegErr(int code, const char *msg = nullptr) -{ - if(code < 0) - { - char buf[256]; - if(av_strerror(code, buf, sizeof buf) < 0) - strcpy(buf, "Unknown Error"); - if(msg) - logError("%s. FFmpeg Error: %s", msg, buf); - else - logError("FFmpeg Error: %s", buf); - } - - return code; -} - - -class InputFileFFmpeg : public InputFile -{ -public: - InputFileFFmpeg(const InputFileOptions &options); - ~InputFileFFmpeg(); - int open(const char *filename, double startPosSeconds = 0.0, double playTimeSeconds = -1.0) override; - int close() override; - - int readSamples(SampleProducer *sampleBuffer) override; - bool done() const override; - int seek(double seconds) override; - int64_t outputSamplesEstimation() const override; - -private: - bool openInternal(const char *filename, double startPosSeconds, double playTimeSeconds); - int closeNoLock(); - void reset(); - int getAudioStreamNum() const; - int handleDecoded(AVFrame *frame, SampleProducer *sb); - int receiveSamples(SampleProducer *sampleBuffer, int& producedSamples); - int seekNoLock(double seconds); - - typedef std::lock_guard Lock; -private: - const InputFileOptions m_inputFileOptions; - const int m_outputChannels; - const int m_outputSamplerate; - AVChannelLayout m_outputChannelLayout; - - AVFormatContext *m_fmtCtx; - AVCodecContext *m_codecCtx; - AVFrame *m_frame = nullptr; - AVPacket *m_packet = nullptr; - SwrContext *m_swrCtx; - int m_streamIndex; - uint8_t *m_outBuf; - bool m_opened; - std::atomic m_done; - std::mutex m_mutex; - int64_t m_decodedSamples; - int64_t m_convertedSamples; - int64_t m_maxConvertedSamples; - int64_t m_nextSeekTimestamp; - int64_t m_skipSamples; -}; - - - -InputFileFFmpeg::InputFileFFmpeg(const InputFileOptions &options) : - m_inputFileOptions(options), - m_outputChannels(options.getNumChannels()), - m_outputSamplerate(options.outputSampleRate) -{ - av_channel_layout_from_mask(&m_outputChannelLayout, - options.outputChannelLayout == InputFileOptions::MONO - ? AV_CH_LAYOUT_MONO - : AV_CH_LAYOUT_STEREO - ); - - reset(); - av_samples_alloc(&m_outBuf, nullptr, m_outputChannels, OUTPUT_BUFFER_COUNT, OUTPUT_FORMAT, 0); -} - - - -void InputFileFFmpeg::reset() -{ - m_fmtCtx = nullptr; - m_codecCtx = nullptr; - m_swrCtx = nullptr; - m_streamIndex = 0; - m_opened = false; - m_done = false; - m_decodedSamples = 0; - m_convertedSamples = 0; - m_maxConvertedSamples = 0; - m_nextSeekTimestamp = 0; - m_skipSamples = 0; -} - - - -InputFileFFmpeg::~InputFileFFmpeg() -{ - closeNoLock(); - av_freep(&m_outBuf); - - av_channel_layout_uninit(&m_outputChannelLayout); -} - - -bool InputFileFFmpeg::openInternal(const char *filename, double startPosSeconds, double playTimeSeconds) -{ - if(checkFFmpegErr(avformat_open_input(&m_fmtCtx, filename, nullptr, nullptr), "Cannot open file") != 0) - return false; - - if(checkFFmpegErr(avformat_find_stream_info(m_fmtCtx, nullptr), "Cannot find stream info") < 0) - return false; - - m_streamIndex = getAudioStreamNum(); - if(m_streamIndex < 0) - { - logError("Cannot find a suitable stream"); - return false; - } - - AVCodecParameters *codecParams = m_fmtCtx->streams[m_streamIndex]->codecpar; - - // 2. Find the appropriate decoder - const AVCodec *decoder = avcodec_find_decoder(codecParams->codec_id); - if (!decoder) - { - logError("Cannot find suitable decoder"); - return false; - } - - // 3. Allocate a new codec context - m_codecCtx = avcodec_alloc_context3(decoder); - if (!m_codecCtx) - { - logError("Unsupported codec"); - return false; - } - - if (checkFFmpegErr(avcodec_parameters_to_context(m_codecCtx, codecParams), "Failed to copy codec parameters") < 0) - return false; - - // Decoder needs to know the timebase to calculate correct timestamps during decoding, - // but it's not always set by the demuxer, so set it manually from the stream info - m_codecCtx->pkt_timebase = m_fmtCtx->streams[m_streamIndex]->time_base; - - if(checkFFmpegErr(avcodec_open2(m_codecCtx, decoder, nullptr), "Cannot open codec") < 0) - return false; //Cannot open codec - - //Open Resample context - int result = swr_alloc_set_opts2(&m_swrCtx, - &m_outputChannelLayout, //Output layout (stereo) - OUTPUT_FORMAT, //Output format (signed 16bit int) - m_outputSamplerate, //Output Sample Rate - &m_codecCtx->ch_layout, //Input layout - m_codecCtx->sample_fmt, //Input format - m_codecCtx->sample_rate, //Input Sample Rate - 0, nullptr); - - if (result < 0) - { - logError("Failed to set resample options"); - return false; - } - - if(!m_swrCtx) - { - logError("Failed to allocate resample context"); - return false; - } - - if(checkFFmpegErr(swr_init(m_swrCtx), "Cannot initialize resample context") < 0) - return false; - - logInfo("Opened file: %s; Codec: %s, Channels: %i, Rate: %i, Format: %s, Timebase: %i/%i, Sample-Estimation: %lld", - filename, m_codecCtx->codec->long_name, m_codecCtx->ch_layout.nb_channels, m_codecCtx->sample_rate, - av_get_sample_fmt_name(m_codecCtx->sample_fmt), m_codecCtx->time_base.num, m_codecCtx->time_base.den, - outputSamplesEstimation()); - - m_frame = av_frame_alloc(); - m_packet = av_packet_alloc(); - if (!m_frame || !m_packet) - { - logError("Failed to allocate frame or packet"); - return false; - } - - m_opened = true; - - if(startPosSeconds > 0.0) - seekNoLock(startPosSeconds); - - if(playTimeSeconds > 0.0) - m_maxConvertedSamples = uint64_t(playTimeSeconds * (double)m_outputSamplerate + 0.5); - - return true; -} - - - -int InputFileFFmpeg::open(const char *filename, double startPosSeconds /*= 0.0*/, double playTimeSeconds /*= -1.0*/) -{ - Lock lock(m_mutex); - - if(m_opened) - { - closeNoLock(); - reset(); - } - - if (!openInternal(filename, startPosSeconds, playTimeSeconds)) - { - closeNoLock(); - return -1; - } - - return 0; -} - - - -int InputFileFFmpeg::seekNoLock( double seconds ) -{ - AVRational time_base = m_fmtCtx->streams[m_streamIndex]->time_base; - int64_t ts = (int64_t)(seconds / av_q2d(time_base)); - if(checkFFmpegErr(avformat_seek_file(m_fmtCtx, m_streamIndex, INT64_MIN, ts, ts, 0), "Seeking failed") < 0) - return -1; - avcodec_flush_buffers(m_codecCtx); - m_nextSeekTimestamp = ts; - return 0; -} - - -int InputFileFFmpeg::seek( double seconds ) -{ - Lock lock(m_mutex); - if(!m_opened) - return -1; - - return seekNoLock(seconds); -} - - -int InputFileFFmpeg::close() -{ - Lock lock(m_mutex); - return closeNoLock(); -} - - -int InputFileFFmpeg::handleDecoded(AVFrame *frame, SampleProducer *sb) -{ - // Need to skip some samples? We currently can't do this while flushing, so do it here before the first conversion - if (frame && m_nextSeekTimestamp > 0) - { - int64_t curTs = frame->pts; - if (curTs < 0) - curTs = frame->pkt_dts; // Fall back to pkt_dts if pts is not set, should be close enough for skipping - if (curTs < 0) // Still broken? Just give up on skipping, play the frame in full - curTs = m_nextSeekTimestamp; - - int64_t tsToSkip = m_nextSeekTimestamp - curTs; - if (tsToSkip > 0) - { - double timeBase = av_q2d(m_fmtCtx->streams[m_streamIndex]->time_base); - double secondsToSkip = tsToSkip * timeBase; - m_skipSamples = (int)(secondsToSkip * m_outputSamplerate); - logInfo("Current timestep is %f but desired is %f, skipping %lli samples", - curTs * timeBase, m_nextSeekTimestamp * timeBase, m_skipSamples); - } - m_nextSeekTimestamp = 0; - } - - int res; - int generatedSamples = 0; - do { - res = swr_convert(m_swrCtx, &m_outBuf, OUTPUT_BUFFER_COUNT, - frame ? (const uint8_t **)frame->extended_data : nullptr, - frame ? frame->nb_samples : 0); - if(res < 0) - return res; - int64_t outSamples = std::max(int64_t(0), res - m_skipSamples); - int64_t skippedSamples = res - outSamples; - if(m_maxConvertedSamples > 0 && outSamples > (m_maxConvertedSamples - m_convertedSamples)) - { - outSamples = m_maxConvertedSamples - m_convertedSamples; - m_done = true; - } - if(outSamples > 0) - sb->produce(((int16_t*)m_outBuf) + (skippedSamples * m_outputChannels), outSamples); - - m_skipSamples -= skippedSamples; - m_convertedSamples += outSamples; - generatedSamples += outSamples; - frame = nullptr; // Only use the frame for the first conversion, then pass NULL to flush the resampler - } while (!m_done && res == OUTPUT_BUFFER_COUNT); // If we filled the whole output buffer, there might be more data to convert, so try again immediately - - return generatedSamples; -} - - -// Returns the number of generated samples, or a negative error code -int InputFileFFmpeg::receiveSamples(SampleProducer *sampleBuffer, int& producedSamples) -{ - int result = avcodec_receive_frame(m_codecCtx, m_frame); - if (result < 0) - { - // Report error if it's not EAGAIN (need more packets) or EOF (flushed all frames) - if (result != AVERROR(EAGAIN) && result != AVERROR_EOF) - checkFFmpegErr(result, "Error while receiving frame"); - } - else - { - m_decodedSamples += m_frame->nb_samples; - - // Resample - auto decodeRes = handleDecoded(m_frame, sampleBuffer); - if (checkFFmpegErr(decodeRes, "Unable to resample") < 0) - { - av_frame_unref(m_frame); - return decodeRes; - } - producedSamples = decodeRes; - } - - av_frame_unref(m_frame); - return result; -} - - -// Read a bunch of samples and push them into sampleBuffer. -// Returns the number of read samples, or a negative error code -int InputFileFFmpeg::readSamples(SampleProducer *sampleBuffer) -{ - Lock lock(m_mutex); - - if(!m_opened) - return -1; - - int written = 0; //samples read - while(!m_done && written == 0) - { - int readRet = av_read_frame(m_fmtCtx, m_packet); - bool eofFlush = false; - if (readRet < 0) - { - if (readRet != AVERROR_EOF) // not just EOF? Return real error. - { - checkFFmpegErr(readRet, "Error while reading frame"); - av_packet_unref(m_packet); - return readRet; - } - // File is EOF, but we still have data in the decoder. - eofFlush = true; - m_done = true; - } - else if (m_packet->stream_index != m_streamIndex) - { - av_packet_unref(m_packet); - continue; - } - - // Patch missing PTS (can happen with some formats/codecs, e.g. MP3) by using DTS, which is usually set if PTS is not. - if (m_packet->pts == AV_NOPTS_VALUE && m_packet->dts != AV_NOPTS_VALUE) - m_packet->pts = m_packet->dts; - - int sendRet = avcodec_send_packet(m_codecCtx, eofFlush ? nullptr : m_packet); - av_packet_unref(m_packet); // Unref immediately after sending - if (checkFFmpegErr(sendRet, "Error while sending packet to decoder") < 0) - { - return sendRet; - } - - while (true) - { - int producedSamples = 0; - int receiveRet = receiveSamples(sampleBuffer, producedSamples); - if (receiveRet == AVERROR(EAGAIN) || receiveRet == AVERROR_EOF) - break; // Break inner loop, try to read next packet - else if (receiveRet < 0) // decoding error :( - return receiveRet; // Fatal error, return it - else - written += producedSamples; - } - - if (eofFlush) - { - int flushedSamples = handleDecoded(nullptr, sampleBuffer); - if (flushedSamples > 0) - written += flushedSamples; - - break; // break outer loop, return what we got so far - } - } - - return written; -} - - - -bool InputFileFFmpeg::done() const -{ - return m_done; -} - - - -int InputFileFFmpeg::getAudioStreamNum() const -{ - return av_find_best_stream(m_fmtCtx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0); -} - - - -int InputFileFFmpeg::closeNoLock() -{ - if (m_frame) - av_frame_free(&m_frame); - - if (m_packet) - av_packet_free(&m_packet); - - if(m_swrCtx) - swr_free(&m_swrCtx); - - if(m_codecCtx) - avcodec_free_context(&m_codecCtx); - - if(m_fmtCtx) - avformat_close_input(&m_fmtCtx); - - m_opened = false; - - return 0; -} - - - -int64_t InputFileFFmpeg::outputSamplesEstimation() const -{ - AVStream *stream = m_fmtCtx->streams[m_streamIndex]; - if (stream->duration > 0) - return stream->duration * (int64_t)stream->time_base.num * - (int64_t)m_outputSamplerate / (int64_t)stream->time_base.den; - else - return m_fmtCtx->duration * m_outputSamplerate / AV_TIME_BASE; -} - - - -InputFile *CreateInputFileFFmpeg(InputFileOptions options /*= InputFileOptions()*/) -{ - return new InputFileFFmpeg(options); -} +// src/inputfileffmpeg.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#include "common.h" + +#ifdef _WIN32 +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#include +#include +#include + +#include "ts3log.h" +#include "inputfile.h" +#include "SampleBuffer.h" +#include "SampleSource.h" +#include "main.h" +#include + +extern "C" +{ +#include +#include +#include +#include +} + + +#define OUTPUT_BUFFER_COUNT 32768 +#define OUTPUT_FORMAT AV_SAMPLE_FMT_S16 + + +int checkFFmpegErr(int code, const char *msg = nullptr) +{ + if(code < 0) + { + char buf[256]; + if(av_strerror(code, buf, sizeof buf) < 0) + strcpy(buf, "Unknown Error"); + if(msg) + logError("%s. FFmpeg Error: %s", msg, buf); + else + logError("FFmpeg Error: %s", buf); + } + + return code; +} + + +class InputFileFFmpeg : public InputFile +{ +public: + InputFileFFmpeg(const InputFileOptions &options); + ~InputFileFFmpeg(); + int open(const char *filename, double startPosSeconds = 0.0, double playTimeSeconds = -1.0) override; + int close() override; + + int readSamples(SampleProducer *sampleBuffer) override; + bool done() const override; + int seek(double seconds) override; + int64_t outputSamplesEstimation() const override; + +private: + bool openInternal(const char *filename, double startPosSeconds, double playTimeSeconds); + int closeNoLock(); + void reset(); + int getAudioStreamNum() const; + int handleDecoded(AVFrame *frame, SampleProducer *sb); + int receiveSamples(SampleProducer *sampleBuffer, int& producedSamples); + int seekNoLock(double seconds); + + typedef std::lock_guard Lock; +private: + const InputFileOptions m_inputFileOptions; + const int m_outputChannels; + const int m_outputSamplerate; + AVChannelLayout m_outputChannelLayout; + + AVFormatContext *m_fmtCtx; + AVCodecContext *m_codecCtx; + AVFrame *m_frame = nullptr; + AVPacket *m_packet = nullptr; + SwrContext *m_swrCtx; + int m_streamIndex; + uint8_t *m_outBuf; + bool m_opened; + std::atomic m_done; + std::mutex m_mutex; + int64_t m_decodedSamples; + int64_t m_convertedSamples; + int64_t m_maxConvertedSamples; + int64_t m_nextSeekTimestamp; + int64_t m_skipSamples; +}; + + + +InputFileFFmpeg::InputFileFFmpeg(const InputFileOptions &options) : + m_inputFileOptions(options), + m_outputChannels(options.getNumChannels()), + m_outputSamplerate(options.outputSampleRate) +{ + av_channel_layout_from_mask(&m_outputChannelLayout, + options.outputChannelLayout == InputFileOptions::MONO + ? AV_CH_LAYOUT_MONO + : AV_CH_LAYOUT_STEREO + ); + + reset(); + av_samples_alloc(&m_outBuf, nullptr, m_outputChannels, OUTPUT_BUFFER_COUNT, OUTPUT_FORMAT, 0); +} + + + +void InputFileFFmpeg::reset() +{ + m_fmtCtx = nullptr; + m_codecCtx = nullptr; + m_swrCtx = nullptr; + m_streamIndex = 0; + m_opened = false; + m_done = false; + m_decodedSamples = 0; + m_convertedSamples = 0; + m_maxConvertedSamples = 0; + m_nextSeekTimestamp = 0; + m_skipSamples = 0; +} + + + +InputFileFFmpeg::~InputFileFFmpeg() +{ + closeNoLock(); + av_freep(&m_outBuf); + + av_channel_layout_uninit(&m_outputChannelLayout); +} + + +bool InputFileFFmpeg::openInternal(const char *filename, double startPosSeconds, double playTimeSeconds) +{ + if(checkFFmpegErr(avformat_open_input(&m_fmtCtx, filename, nullptr, nullptr), "Cannot open file") != 0) + return false; + + if(checkFFmpegErr(avformat_find_stream_info(m_fmtCtx, nullptr), "Cannot find stream info") < 0) + return false; + + m_streamIndex = getAudioStreamNum(); + if(m_streamIndex < 0) + { + logError("Cannot find a suitable stream"); + return false; + } + + AVCodecParameters *codecParams = m_fmtCtx->streams[m_streamIndex]->codecpar; + + // 2. Find the appropriate decoder + const AVCodec *decoder = avcodec_find_decoder(codecParams->codec_id); + if (!decoder) + { + logError("Cannot find suitable decoder"); + return false; + } + + // 3. Allocate a new codec context + m_codecCtx = avcodec_alloc_context3(decoder); + if (!m_codecCtx) + { + logError("Unsupported codec"); + return false; + } + + if (checkFFmpegErr(avcodec_parameters_to_context(m_codecCtx, codecParams), "Failed to copy codec parameters") < 0) + return false; + + // Decoder needs to know the timebase to calculate correct timestamps during decoding, + // but it's not always set by the demuxer, so set it manually from the stream info + m_codecCtx->pkt_timebase = m_fmtCtx->streams[m_streamIndex]->time_base; + + if(checkFFmpegErr(avcodec_open2(m_codecCtx, decoder, nullptr), "Cannot open codec") < 0) + return false; //Cannot open codec + + //Open Resample context + int result = swr_alloc_set_opts2(&m_swrCtx, + &m_outputChannelLayout, //Output layout (stereo) + OUTPUT_FORMAT, //Output format (signed 16bit int) + m_outputSamplerate, //Output Sample Rate + &m_codecCtx->ch_layout, //Input layout + m_codecCtx->sample_fmt, //Input format + m_codecCtx->sample_rate, //Input Sample Rate + 0, nullptr); + + if (result < 0) + { + logError("Failed to set resample options"); + return false; + } + + if(!m_swrCtx) + { + logError("Failed to allocate resample context"); + return false; + } + + if(checkFFmpegErr(swr_init(m_swrCtx), "Cannot initialize resample context") < 0) + return false; + + logInfo("Opened file: %s; Codec: %s, Channels: %i, Rate: %i, Format: %s, Timebase: %i/%i, Sample-Estimation: %lld", + filename, m_codecCtx->codec->long_name, m_codecCtx->ch_layout.nb_channels, m_codecCtx->sample_rate, + av_get_sample_fmt_name(m_codecCtx->sample_fmt), m_codecCtx->time_base.num, m_codecCtx->time_base.den, + outputSamplesEstimation()); + + m_frame = av_frame_alloc(); + m_packet = av_packet_alloc(); + if (!m_frame || !m_packet) + { + logError("Failed to allocate frame or packet"); + return false; + } + + m_opened = true; + + if(startPosSeconds > 0.0) + seekNoLock(startPosSeconds); + + if(playTimeSeconds > 0.0) + m_maxConvertedSamples = uint64_t(playTimeSeconds * (double)m_outputSamplerate + 0.5); + + return true; +} + + + +int InputFileFFmpeg::open(const char *filename, double startPosSeconds /*= 0.0*/, double playTimeSeconds /*= -1.0*/) +{ + Lock lock(m_mutex); + + if(m_opened) + { + closeNoLock(); + reset(); + } + + if (!openInternal(filename, startPosSeconds, playTimeSeconds)) + { + closeNoLock(); + return -1; + } + + return 0; +} + + + +int InputFileFFmpeg::seekNoLock( double seconds ) +{ + AVRational time_base = m_fmtCtx->streams[m_streamIndex]->time_base; + int64_t ts = (int64_t)(seconds / av_q2d(time_base)); + if(checkFFmpegErr(avformat_seek_file(m_fmtCtx, m_streamIndex, INT64_MIN, ts, ts, 0), "Seeking failed") < 0) + return -1; + avcodec_flush_buffers(m_codecCtx); + m_nextSeekTimestamp = ts; + return 0; +} + + +int InputFileFFmpeg::seek( double seconds ) +{ + Lock lock(m_mutex); + if(!m_opened) + return -1; + + return seekNoLock(seconds); +} + + +int InputFileFFmpeg::close() +{ + Lock lock(m_mutex); + return closeNoLock(); +} + + +int InputFileFFmpeg::handleDecoded(AVFrame *frame, SampleProducer *sb) +{ + // Need to skip some samples? We currently can't do this while flushing, so do it here before the first conversion + if (frame && m_nextSeekTimestamp > 0) + { + int64_t curTs = frame->pts; + if (curTs < 0) + curTs = frame->pkt_dts; // Fall back to pkt_dts if pts is not set, should be close enough for skipping + if (curTs < 0) // Still broken? Just give up on skipping, play the frame in full + curTs = m_nextSeekTimestamp; + + int64_t tsToSkip = m_nextSeekTimestamp - curTs; + if (tsToSkip > 0) + { + double timeBase = av_q2d(m_fmtCtx->streams[m_streamIndex]->time_base); + double secondsToSkip = tsToSkip * timeBase; + m_skipSamples = (int)(secondsToSkip * m_outputSamplerate); + logInfo("Current timestep is %f but desired is %f, skipping %lli samples", + curTs * timeBase, m_nextSeekTimestamp * timeBase, m_skipSamples); + } + m_nextSeekTimestamp = 0; + } + + int res; + int generatedSamples = 0; + do { + res = swr_convert(m_swrCtx, &m_outBuf, OUTPUT_BUFFER_COUNT, + frame ? (const uint8_t **)frame->extended_data : nullptr, + frame ? frame->nb_samples : 0); + if(res < 0) + return res; + int64_t outSamples = std::max(int64_t(0), res - m_skipSamples); + int64_t skippedSamples = res - outSamples; + if(m_maxConvertedSamples > 0 && outSamples > (m_maxConvertedSamples - m_convertedSamples)) + { + outSamples = m_maxConvertedSamples - m_convertedSamples; + m_done = true; + } + if(outSamples > 0) + sb->produce(((int16_t*)m_outBuf) + (skippedSamples * m_outputChannels), outSamples); + + m_skipSamples -= skippedSamples; + m_convertedSamples += outSamples; + generatedSamples += outSamples; + frame = nullptr; // Only use the frame for the first conversion, then pass NULL to flush the resampler + } while (!m_done && res == OUTPUT_BUFFER_COUNT); // If we filled the whole output buffer, there might be more data to convert, so try again immediately + + return generatedSamples; +} + + +// Returns the number of generated samples, or a negative error code +int InputFileFFmpeg::receiveSamples(SampleProducer *sampleBuffer, int& producedSamples) +{ + int result = avcodec_receive_frame(m_codecCtx, m_frame); + if (result < 0) + { + // Report error if it's not EAGAIN (need more packets) or EOF (flushed all frames) + if (result != AVERROR(EAGAIN) && result != AVERROR_EOF) + checkFFmpegErr(result, "Error while receiving frame"); + } + else + { + m_decodedSamples += m_frame->nb_samples; + + // Resample + auto decodeRes = handleDecoded(m_frame, sampleBuffer); + if (checkFFmpegErr(decodeRes, "Unable to resample") < 0) + { + av_frame_unref(m_frame); + return decodeRes; + } + producedSamples = decodeRes; + } + + av_frame_unref(m_frame); + return result; +} + + +// Read a bunch of samples and push them into sampleBuffer. +// Returns the number of read samples, or a negative error code +int InputFileFFmpeg::readSamples(SampleProducer *sampleBuffer) +{ + Lock lock(m_mutex); + + if(!m_opened) + return -1; + + int written = 0; //samples read + while(!m_done && written == 0) + { + int readRet = av_read_frame(m_fmtCtx, m_packet); + bool eofFlush = false; + if (readRet < 0) + { + if (readRet != AVERROR_EOF) // not just EOF? Return real error. + { + checkFFmpegErr(readRet, "Error while reading frame"); + av_packet_unref(m_packet); + return readRet; + } + // File is EOF, but we still have data in the decoder. + eofFlush = true; + m_done = true; + } + else if (m_packet->stream_index != m_streamIndex) + { + av_packet_unref(m_packet); + continue; + } + + // Patch missing PTS (can happen with some formats/codecs, e.g. MP3) by using DTS, which is usually set if PTS is not. + if (m_packet->pts == AV_NOPTS_VALUE && m_packet->dts != AV_NOPTS_VALUE) + m_packet->pts = m_packet->dts; + + int sendRet = avcodec_send_packet(m_codecCtx, eofFlush ? nullptr : m_packet); + av_packet_unref(m_packet); // Unref immediately after sending + if (checkFFmpegErr(sendRet, "Error while sending packet to decoder") < 0) + { + return sendRet; + } + + while (true) + { + int producedSamples = 0; + int receiveRet = receiveSamples(sampleBuffer, producedSamples); + if (receiveRet == AVERROR(EAGAIN) || receiveRet == AVERROR_EOF) + break; // Break inner loop, try to read next packet + else if (receiveRet < 0) // decoding error :( + return receiveRet; // Fatal error, return it + else + written += producedSamples; + } + + if (eofFlush) + { + int flushedSamples = handleDecoded(nullptr, sampleBuffer); + if (flushedSamples > 0) + written += flushedSamples; + + break; // break outer loop, return what we got so far + } + } + + return written; +} + + + +bool InputFileFFmpeg::done() const +{ + return m_done; +} + + + +int InputFileFFmpeg::getAudioStreamNum() const +{ + return av_find_best_stream(m_fmtCtx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0); +} + + + +int InputFileFFmpeg::closeNoLock() +{ + if (m_frame) + av_frame_free(&m_frame); + + if (m_packet) + av_packet_free(&m_packet); + + if(m_swrCtx) + swr_free(&m_swrCtx); + + if(m_codecCtx) + avcodec_free_context(&m_codecCtx); + + if(m_fmtCtx) + avformat_close_input(&m_fmtCtx); + + m_opened = false; + + return 0; +} + + + +int64_t InputFileFFmpeg::outputSamplesEstimation() const +{ + AVStream *stream = m_fmtCtx->streams[m_streamIndex]; + if (stream->duration > 0) + return stream->duration * (int64_t)stream->time_base.num * + (int64_t)m_outputSamplerate / (int64_t)stream->time_base.den; + else + return m_fmtCtx->duration * m_outputSamplerate / AV_TIME_BASE; +} + + + +InputFile *CreateInputFileFFmpeg(InputFileOptions options /*= InputFileOptions()*/) +{ + return new InputFileFFmpeg(options); +} diff --git a/src/samples.cpp b/src/samples.cpp index d54d47a..88cfe1e 100644 --- a/src/samples.cpp +++ b/src/samples.cpp @@ -1,379 +1,379 @@ -// src/samples.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#include "common.h" - -#include "inputfile.h" -#include "samples.h" -#include "SoundInfo.h" -#include "ts3log.h" -#include "HighResClock.h" - -#include -#include -#include -#include - -//#define MEASURE_PERFORMANCE - -using std::vector; -using std::queue; - -static_assert(sizeof(short) == 2, "Short is weird size"); - -#if defined(_MSC_VER) -#define ALIGNED_(x) __declspec(align(x)) -#elif defined(__GNUC__) -#define ALIGNED_(x) __attribute__ ((aligned(x))) -#else -#error Unknown compiler -#endif - -#define ALIGNED_STACK_ARRAY(name, size, alignment) name[size] ALIGNED_(alignment) - -#define MAX_SAMPLEBUFFER_SIZE (48000 * 5) -#define AMP_THRESH (SHRT_MAX / 2) - - - -Sampler::Sampler() : - m_sbCapture(2, MAX_SAMPLEBUFFER_SIZE), - m_sbPlayback(2, MAX_SAMPLEBUFFER_SIZE), - m_sampleProducerThread(), - m_inputFile(nullptr), - m_peakMeterCapture(0.01f, 0.00005f, 24000), - m_peakMeterPlayback(0.01f, 0.00005f, 24000), - m_volumeDivider(1), - m_globalDbSettingLocal(-1.0), - m_globalDbSettingRemote(-1.0), - m_soundDbSetting(0.0), - m_state(eSILENT), - m_localPlayback(true) -{ - /* Ensure resources are loaded */ - Q_INIT_RESOURCE(qtres); - - assert(m_state.is_lock_free()); -} - - -Sampler::~Sampler() -{ - -} - - -void Sampler::init() -{ - m_sampleProducerThread.addBuffer(&m_sbCapture); - m_sampleProducerThread.addBuffer(&m_sbPlayback, m_localPlayback); - m_sampleProducerThread.start(); -} - - -void Sampler::shutdown() -{ - std::lock_guard Lock(m_mutex); - - if(m_inputFile) - { - m_inputFile->close(); - delete m_inputFile; - m_inputFile = nullptr; - } - - m_sampleProducerThread.stop(); -} - - -#ifdef MEASURE_PERFORMANCE -size_t g_perfMeasureCount = 0; -double g_perfMeasurement = 0.0; -#endif - -int Sampler::fetchSamples(SampleBuffer &sb, PeakMeter &pm, short *samples, int count, int channels, bool eraseConsumed, int ciLeft, int ciRight, bool overLeft, bool overRight ) -{ - if (m_state == ePAUSED) - return 0; - - SampleBuffer::Lock sbl(sb.getMutex()); - - if(sb.avail() == 0) - return 0; - - if(overLeft) - { - if(channels == 1) - memset(samples, 0, count * sizeof(short)); - else - for(int i = 0; i < count; i++) - samples[i*channels+ciLeft] = 0; - } - - if(overRight && channels > 1) - for(int i = 0; i < count; i++) - samples[i*channels+ciRight] = 0; - - const int write = std::min(count, sb.avail()); - - const short* const in = sb.getBufferData(); - short* const out = samples; - -#ifdef MEASURE_PERFORMANCE - std::chrono::time_point start, end; - start = HighResClock::now(); -#endif - - if(channels == 1) - { - for (int i = 0; i < write; i++) - { - float sample = out[i] + m_volumeFactor * (float(in[i * 2]) + float(in[i * 2 + 1])) * 0.5f; - pm.process(sample); - out[i] = pm.limit(sample, AMP_THRESH); - } - } - else - { - for(int i = 0; i < write; i++) - { - float sample0 = out[i * channels + ciLeft] + m_volumeFactor * float(in[i * 2]); - float sample1 = out[i * channels + ciRight] + m_volumeFactor * float(in[i * 2 + 1]); - pm.process(fabs(sample0) > fabs(sample1) ? sample0 : sample1); - out[i * channels + ciLeft] = pm.limit(sample0, AMP_THRESH); - out[i * channels + ciRight] = pm.limit(sample1, AMP_THRESH); - } - } - - sb.consume(nullptr, write, true); - -#ifdef MEASURE_PERFORMANCE - end = HighResClock::now(); - std::chrono::duration elapsed = end - start; - g_perfMeasurement += elapsed.count(); - if(++g_perfMeasureCount >= 1000) - { - logInfo("Avg. time in fetchSamples: %f us, volume: %f, limiter: %f", g_perfMeasurement / (double)g_perfMeasureCount * 1000000.0, - m_volumeFactor, std::min(AMP_THRESH / m_peakMeterPlayback.getOutput(), 1.0f)); - g_perfMeasureCount = 0; - g_perfMeasurement = 0.0; - } -#endif - return write; -} - - -int Sampler::findChannelId(unsigned int channel, const unsigned int *channelSpeakerArray, int count) -{ - for(int i = 0; i < count; i++) - if(channelSpeakerArray[i] & channel) - return i; - return 0; -} - - -int Sampler::fetchInputSamples(short *samples, int count, int channels, bool *finished) -{ - std::lock_guard Lock(m_mutex); - - setVolumeDb(m_globalDbSettingRemote + m_soundDbSetting); - int written = fetchSamples(m_sbCapture, m_peakMeterCapture, samples, count, channels, true, 0, 1, m_muteMyself, m_muteMyself); - - if(m_state == ePLAYING && m_inputFile && m_inputFile->done()) - { - SampleBuffer::Lock sbl(m_sbCapture.getMutex()); - if (m_sbCapture.avail() == 0) - { - m_state = eSILENT; - if(finished) - *finished = true; - emit onStopPlaying(); - } - } - - return written; -} - - -int Sampler::fetchOutputSamples(short *samples, int count, int channels, const unsigned int *channelSpeakerArray, unsigned int *channelFillMask) -{ - std::lock_guard Lock(m_mutex); - - const unsigned int bitMaskLeft = SPEAKER_FRONT_LEFT | SPEAKER_HEADPHONES_LEFT; - const unsigned int bitMaskRight = SPEAKER_FRONT_RIGHT | SPEAKER_HEADPHONES_RIGHT; - int ciLeft = findChannelId(bitMaskLeft, channelSpeakerArray, channels); - int ciRight = findChannelId(bitMaskRight, channelSpeakerArray, channels); - setVolumeDb(m_globalDbSettingLocal + m_soundDbSetting); - int written = fetchSamples(m_sbPlayback, m_peakMeterPlayback, samples, count, channels, true, ciLeft, ciRight, - (*channelFillMask & bitMaskLeft) == 0, - (*channelFillMask & bitMaskRight) == 0); - - if(written > 0) - *channelFillMask |= (bitMaskLeft | bitMaskRight); - - if(m_state == ePLAYING_PREVIEW && m_inputFile && m_inputFile->done()) - { - SampleBuffer::Lock sbl(m_sbPlayback.getMutex()); - if (m_sbPlayback.avail() == 0) - { - m_state = eSILENT; - emit onStopPlaying(); - } - } - - return written; -} - - -bool Sampler::playFile(const SoundInfo &sound) -{ - return playSoundInternal(sound, false); -} - - -bool Sampler::playPreview(const SoundInfo &sound) -{ - return playSoundInternal(sound, true); -} - - -void Sampler::stopPlayback() -{ - std::lock_guard Lock(m_mutex); - stopSoundInternal(); -} - -#define VOLUMESCALER_EXPONENT 1.0 -#define VOLUMESCALER_DB_MIN -28.0 -void Sampler::setVolumeRemote( int vol ) -{ - double v = (double)vol / 100.0; - double db = pow(1.0 - v, VOLUMESCALER_EXPONENT) * VOLUMESCALER_DB_MIN; - m_globalDbSettingRemote = db; - setVolumeDb(m_globalDbSettingRemote + m_soundDbSetting); -} - - -void Sampler::setVolumeLocal( int vol ) -{ - double v = (double)vol / 100.0; - double db = pow(1.0 - v, VOLUMESCALER_EXPONENT) * VOLUMESCALER_DB_MIN; - m_globalDbSettingLocal = db; - setVolumeDb(m_globalDbSettingLocal + m_soundDbSetting); -} - - -void Sampler::setLocalPlayback( bool enabled ) -{ - m_localPlayback = enabled; - m_sampleProducerThread.setBufferEnabled(&m_sbPlayback, enabled); -} - - -void Sampler::setMuteMyself(bool enabled) -{ - m_muteMyself = enabled; -} - - -void Sampler::setVolumeDb( double decibel ) -{ - double factor = pow(10.0, decibel/10.0); - m_volumeFactor = (float)factor; - m_volumeDivider = (int)(factor * (1 << volumeScaleExp) + 0.5); -} - - -void Sampler::stopSoundInternal() -{ - if (m_inputFile) - { - m_state = eSILENT; - m_sampleProducerThread.setSource(nullptr); - m_inputFile->close(); - delete m_inputFile; - m_inputFile = nullptr; - - //Clear buffers - SampleBuffer::Lock sblc(m_sbCapture.getMutex()); - SampleBuffer::Lock sblp(m_sbPlayback.getMutex()); - m_sbCapture.consume(nullptr, m_sbCapture.avail()); - m_sbPlayback.consume(nullptr, m_sbPlayback.avail()); - - emit onStopPlaying(); - } -} - - -bool Sampler::playSoundInternal( const SoundInfo &sound, bool preview ) -{ - std::lock_guard Lock(m_mutex); - - stopSoundInternal(); - - m_inputFile = CreateInputFileFFmpeg(); - - if(m_inputFile->open(sound.filename.toUtf8(), sound.getStartTime(), sound.getPlayTime()) != 0) - { - delete m_inputFile; - m_inputFile = nullptr; - return false; - } - - m_soundDbSetting = (double)sound.volume; - setVolumeDb(m_globalDbSettingLocal + m_soundDbSetting); - - SampleBuffer::Lock sblc(m_sbCapture.getMutex()); - SampleBuffer::Lock sblp(m_sbPlayback.getMutex()); - - //Clear buffers - m_sbCapture.consume(nullptr, m_sbCapture.avail()); - m_sbPlayback.consume(nullptr, m_sbPlayback.avail()); - - if(preview) - { - m_state = ePLAYING_PREVIEW; - m_sampleProducerThread.setBufferEnabled(&m_sbCapture, false); - m_sampleProducerThread.setBufferEnabled(&m_sbPlayback, true); - } - else - { - m_state = ePLAYING; - m_sampleProducerThread.setBufferEnabled(&m_sbCapture, true); - } - - m_sampleProducerThread.setSource(m_inputFile); - - emit onStartPlaying(preview, sound.filename); - - return true; -} - - -void Sampler::pausePlayback() -{ - std::lock_guard Lock(m_mutex); - if (m_state == ePLAYING) - { - m_state = ePAUSED; - emit onPausePlaying(); - } -} - - -void Sampler::unpausePlayback() -{ - std::lock_guard Lock(m_mutex); - if (m_state == ePAUSED) - { - m_state = ePLAYING; - emit onUnpausePlaying(); - } -} - +// src/samples.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#include "common.h" + +#include "inputfile.h" +#include "samples.h" +#include "SoundInfo.h" +#include "ts3log.h" +#include "HighResClock.h" + +#include +#include +#include +#include + +//#define MEASURE_PERFORMANCE + +using std::vector; +using std::queue; + +static_assert(sizeof(short) == 2, "Short is weird size"); + +#if defined(_MSC_VER) +#define ALIGNED_(x) __declspec(align(x)) +#elif defined(__GNUC__) +#define ALIGNED_(x) __attribute__ ((aligned(x))) +#else +#error Unknown compiler +#endif + +#define ALIGNED_STACK_ARRAY(name, size, alignment) name[size] ALIGNED_(alignment) + +#define MAX_SAMPLEBUFFER_SIZE (48000 * 5) +#define AMP_THRESH (SHRT_MAX / 2) + + + +Sampler::Sampler() : + m_sbCapture(2, MAX_SAMPLEBUFFER_SIZE), + m_sbPlayback(2, MAX_SAMPLEBUFFER_SIZE), + m_sampleProducerThread(), + m_inputFile(nullptr), + m_peakMeterCapture(0.01f, 0.00005f, 24000), + m_peakMeterPlayback(0.01f, 0.00005f, 24000), + m_volumeDivider(1), + m_globalDbSettingLocal(-1.0), + m_globalDbSettingRemote(-1.0), + m_soundDbSetting(0.0), + m_state(eSILENT), + m_localPlayback(true) +{ + /* Ensure resources are loaded */ + Q_INIT_RESOURCE(qtres); + + assert(m_state.is_lock_free()); +} + + +Sampler::~Sampler() +{ + +} + + +void Sampler::init() +{ + m_sampleProducerThread.addBuffer(&m_sbCapture); + m_sampleProducerThread.addBuffer(&m_sbPlayback, m_localPlayback); + m_sampleProducerThread.start(); +} + + +void Sampler::shutdown() +{ + std::lock_guard Lock(m_mutex); + + if(m_inputFile) + { + m_inputFile->close(); + delete m_inputFile; + m_inputFile = nullptr; + } + + m_sampleProducerThread.stop(); +} + + +#ifdef MEASURE_PERFORMANCE +size_t g_perfMeasureCount = 0; +double g_perfMeasurement = 0.0; +#endif + +int Sampler::fetchSamples(SampleBuffer &sb, PeakMeter &pm, short *samples, int count, int channels, bool eraseConsumed, int ciLeft, int ciRight, bool overLeft, bool overRight ) +{ + if (m_state == ePAUSED) + return 0; + + SampleBuffer::Lock sbl(sb.getMutex()); + + if(sb.avail() == 0) + return 0; + + if(overLeft) + { + if(channels == 1) + memset(samples, 0, count * sizeof(short)); + else + for(int i = 0; i < count; i++) + samples[i*channels+ciLeft] = 0; + } + + if(overRight && channels > 1) + for(int i = 0; i < count; i++) + samples[i*channels+ciRight] = 0; + + const int write = std::min(count, sb.avail()); + + const short* const in = sb.getBufferData(); + short* const out = samples; + +#ifdef MEASURE_PERFORMANCE + std::chrono::time_point start, end; + start = HighResClock::now(); +#endif + + if(channels == 1) + { + for (int i = 0; i < write; i++) + { + float sample = out[i] + m_volumeFactor * (float(in[i * 2]) + float(in[i * 2 + 1])) * 0.5f; + pm.process(sample); + out[i] = pm.limit(sample, AMP_THRESH); + } + } + else + { + for(int i = 0; i < write; i++) + { + float sample0 = out[i * channels + ciLeft] + m_volumeFactor * float(in[i * 2]); + float sample1 = out[i * channels + ciRight] + m_volumeFactor * float(in[i * 2 + 1]); + pm.process(fabs(sample0) > fabs(sample1) ? sample0 : sample1); + out[i * channels + ciLeft] = pm.limit(sample0, AMP_THRESH); + out[i * channels + ciRight] = pm.limit(sample1, AMP_THRESH); + } + } + + sb.consume(nullptr, write, true); + +#ifdef MEASURE_PERFORMANCE + end = HighResClock::now(); + std::chrono::duration elapsed = end - start; + g_perfMeasurement += elapsed.count(); + if(++g_perfMeasureCount >= 1000) + { + logInfo("Avg. time in fetchSamples: %f us, volume: %f, limiter: %f", g_perfMeasurement / (double)g_perfMeasureCount * 1000000.0, + m_volumeFactor, std::min(AMP_THRESH / m_peakMeterPlayback.getOutput(), 1.0f)); + g_perfMeasureCount = 0; + g_perfMeasurement = 0.0; + } +#endif + return write; +} + + +int Sampler::findChannelId(unsigned int channel, const unsigned int *channelSpeakerArray, int count) +{ + for(int i = 0; i < count; i++) + if(channelSpeakerArray[i] & channel) + return i; + return 0; +} + + +int Sampler::fetchInputSamples(short *samples, int count, int channels, bool *finished) +{ + std::lock_guard Lock(m_mutex); + + setVolumeDb(m_globalDbSettingRemote + m_soundDbSetting); + int written = fetchSamples(m_sbCapture, m_peakMeterCapture, samples, count, channels, true, 0, 1, m_muteMyself, m_muteMyself); + + if(m_state == ePLAYING && m_inputFile && m_inputFile->done()) + { + SampleBuffer::Lock sbl(m_sbCapture.getMutex()); + if (m_sbCapture.avail() == 0) + { + m_state = eSILENT; + if(finished) + *finished = true; + emit onStopPlaying(); + } + } + + return written; +} + + +int Sampler::fetchOutputSamples(short *samples, int count, int channels, const unsigned int *channelSpeakerArray, unsigned int *channelFillMask) +{ + std::lock_guard Lock(m_mutex); + + const unsigned int bitMaskLeft = SPEAKER_FRONT_LEFT | SPEAKER_HEADPHONES_LEFT; + const unsigned int bitMaskRight = SPEAKER_FRONT_RIGHT | SPEAKER_HEADPHONES_RIGHT; + int ciLeft = findChannelId(bitMaskLeft, channelSpeakerArray, channels); + int ciRight = findChannelId(bitMaskRight, channelSpeakerArray, channels); + setVolumeDb(m_globalDbSettingLocal + m_soundDbSetting); + int written = fetchSamples(m_sbPlayback, m_peakMeterPlayback, samples, count, channels, true, ciLeft, ciRight, + (*channelFillMask & bitMaskLeft) == 0, + (*channelFillMask & bitMaskRight) == 0); + + if(written > 0) + *channelFillMask |= (bitMaskLeft | bitMaskRight); + + if(m_state == ePLAYING_PREVIEW && m_inputFile && m_inputFile->done()) + { + SampleBuffer::Lock sbl(m_sbPlayback.getMutex()); + if (m_sbPlayback.avail() == 0) + { + m_state = eSILENT; + emit onStopPlaying(); + } + } + + return written; +} + + +bool Sampler::playFile(const SoundInfo &sound) +{ + return playSoundInternal(sound, false); +} + + +bool Sampler::playPreview(const SoundInfo &sound) +{ + return playSoundInternal(sound, true); +} + + +void Sampler::stopPlayback() +{ + std::lock_guard Lock(m_mutex); + stopSoundInternal(); +} + +#define VOLUMESCALER_EXPONENT 1.0 +#define VOLUMESCALER_DB_MIN -28.0 +void Sampler::setVolumeRemote( int vol ) +{ + double v = (double)vol / 100.0; + double db = pow(1.0 - v, VOLUMESCALER_EXPONENT) * VOLUMESCALER_DB_MIN; + m_globalDbSettingRemote = db; + setVolumeDb(m_globalDbSettingRemote + m_soundDbSetting); +} + + +void Sampler::setVolumeLocal( int vol ) +{ + double v = (double)vol / 100.0; + double db = pow(1.0 - v, VOLUMESCALER_EXPONENT) * VOLUMESCALER_DB_MIN; + m_globalDbSettingLocal = db; + setVolumeDb(m_globalDbSettingLocal + m_soundDbSetting); +} + + +void Sampler::setLocalPlayback( bool enabled ) +{ + m_localPlayback = enabled; + m_sampleProducerThread.setBufferEnabled(&m_sbPlayback, enabled); +} + + +void Sampler::setMuteMyself(bool enabled) +{ + m_muteMyself = enabled; +} + + +void Sampler::setVolumeDb( double decibel ) +{ + double factor = pow(10.0, decibel/10.0); + m_volumeFactor = (float)factor; + m_volumeDivider = (int)(factor * (1 << volumeScaleExp) + 0.5); +} + + +void Sampler::stopSoundInternal() +{ + if (m_inputFile) + { + m_state = eSILENT; + m_sampleProducerThread.setSource(nullptr); + m_inputFile->close(); + delete m_inputFile; + m_inputFile = nullptr; + + //Clear buffers + SampleBuffer::Lock sblc(m_sbCapture.getMutex()); + SampleBuffer::Lock sblp(m_sbPlayback.getMutex()); + m_sbCapture.consume(nullptr, m_sbCapture.avail()); + m_sbPlayback.consume(nullptr, m_sbPlayback.avail()); + + emit onStopPlaying(); + } +} + + +bool Sampler::playSoundInternal( const SoundInfo &sound, bool preview ) +{ + std::lock_guard Lock(m_mutex); + + stopSoundInternal(); + + m_inputFile = CreateInputFileFFmpeg(); + + if(m_inputFile->open(sound.filename.toUtf8(), sound.getStartTime(), sound.getPlayTime()) != 0) + { + delete m_inputFile; + m_inputFile = nullptr; + return false; + } + + m_soundDbSetting = (double)sound.volume; + setVolumeDb(m_globalDbSettingLocal + m_soundDbSetting); + + SampleBuffer::Lock sblc(m_sbCapture.getMutex()); + SampleBuffer::Lock sblp(m_sbPlayback.getMutex()); + + //Clear buffers + m_sbCapture.consume(nullptr, m_sbCapture.avail()); + m_sbPlayback.consume(nullptr, m_sbPlayback.avail()); + + if(preview) + { + m_state = ePLAYING_PREVIEW; + m_sampleProducerThread.setBufferEnabled(&m_sbCapture, false); + m_sampleProducerThread.setBufferEnabled(&m_sbPlayback, true); + } + else + { + m_state = ePLAYING; + m_sampleProducerThread.setBufferEnabled(&m_sbCapture, true); + } + + m_sampleProducerThread.setSource(m_inputFile); + + emit onStartPlaying(preview, sound.filename); + + return true; +} + + +void Sampler::pausePlayback() +{ + std::lock_guard Lock(m_mutex); + if (m_state == ePLAYING) + { + m_state = ePAUSED; + emit onPausePlaying(); + } +} + + +void Sampler::unpausePlayback() +{ + std::lock_guard Lock(m_mutex); + if (m_state == ePAUSED) + { + m_state = ePLAYING; + emit onUnpausePlaying(); + } +} + diff --git a/src/samples.h b/src/samples.h index 44f7228..b02c978 100644 --- a/src/samples.h +++ b/src/samples.h @@ -1,89 +1,89 @@ -// src/samples.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include - -#include "SampleBuffer.h" -#include "SampleProducerThread.h" -#include "peakmeter.h" - -#include -#include - -class InputFile; -class SoundInfo; - - -class Sampler : public QObject -{ - Q_OBJECT - -public: - enum state_e - { - eSILENT = 0, - ePLAYING, - ePAUSED, - ePLAYING_PREVIEW, - }; - -public: - Sampler(); - ~Sampler(); - void init(); - void shutdown(); - int fetchInputSamples(short *samples, int count, int channels, bool *finished); - int fetchOutputSamples(short *samples, int count, int channels, const unsigned int *channelSpeakerArray, unsigned int *channelFillMask); - bool playFile(const SoundInfo &sound); - bool playPreview(const SoundInfo &sound); - void stopPlayback(); - void setVolumeLocal(int vol); - void setVolumeRemote(int vol); - void setLocalPlayback(bool enabled); - void setMuteMyself(bool enabled); - void pausePlayback(); - void unpausePlayback(); - inline state_e getState() const { return m_state; } - -signals: - void onStartPlaying(bool preview, QString filename); - void onStopPlaying(); - void onPausePlaying(); - void onUnpausePlaying(); - -private: - void stopSoundInternal(); - bool playSoundInternal(const SoundInfo &sound, bool preview); - void setVolumeDb(double decibel); - int fetchSamples(SampleBuffer &sb, PeakMeter &pm, short *samples, int count, int channels, bool eraseConsumed, int ciLeft, int ciRight, bool overLeft, bool overRight); - int findChannelId(unsigned int channel, const unsigned int *channelSpeakerArray, int count); - inline short scale(int val) const { - return (short)((val * m_volumeDivider) >> volumeScaleExp); - } - -private: - SampleBuffer m_sbCapture; - SampleBuffer m_sbPlayback; - SampleProducerThread m_sampleProducerThread; - InputFile *m_inputFile; - PeakMeter m_peakMeterCapture; - PeakMeter m_peakMeterPlayback; - int m_volumeDivider; - float m_volumeFactor; - static const int volumeScaleExp = 12; - double m_globalDbSettingLocal; - double m_globalDbSettingRemote; - double m_soundDbSetting; - std::mutex m_mutex; - std::atomic m_state; - bool m_localPlayback; - bool m_muteMyself; -}; - +// src/samples.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include + +#include "SampleBuffer.h" +#include "SampleProducerThread.h" +#include "peakmeter.h" + +#include +#include + +class InputFile; +class SoundInfo; + + +class Sampler : public QObject +{ + Q_OBJECT + +public: + enum state_e + { + eSILENT = 0, + ePLAYING, + ePAUSED, + ePLAYING_PREVIEW, + }; + +public: + Sampler(); + ~Sampler(); + void init(); + void shutdown(); + int fetchInputSamples(short *samples, int count, int channels, bool *finished); + int fetchOutputSamples(short *samples, int count, int channels, const unsigned int *channelSpeakerArray, unsigned int *channelFillMask); + bool playFile(const SoundInfo &sound); + bool playPreview(const SoundInfo &sound); + void stopPlayback(); + void setVolumeLocal(int vol); + void setVolumeRemote(int vol); + void setLocalPlayback(bool enabled); + void setMuteMyself(bool enabled); + void pausePlayback(); + void unpausePlayback(); + inline state_e getState() const { return m_state; } + +signals: + void onStartPlaying(bool preview, QString filename); + void onStopPlaying(); + void onPausePlaying(); + void onUnpausePlaying(); + +private: + void stopSoundInternal(); + bool playSoundInternal(const SoundInfo &sound, bool preview); + void setVolumeDb(double decibel); + int fetchSamples(SampleBuffer &sb, PeakMeter &pm, short *samples, int count, int channels, bool eraseConsumed, int ciLeft, int ciRight, bool overLeft, bool overRight); + int findChannelId(unsigned int channel, const unsigned int *channelSpeakerArray, int count); + inline short scale(int val) const { + return (short)((val * m_volumeDivider) >> volumeScaleExp); + } + +private: + SampleBuffer m_sbCapture; + SampleBuffer m_sbPlayback; + SampleProducerThread m_sampleProducerThread; + InputFile *m_inputFile; + PeakMeter m_peakMeterCapture; + PeakMeter m_peakMeterPlayback; + int m_volumeDivider; + float m_volumeFactor; + static const int volumeScaleExp = 12; + double m_globalDbSettingLocal; + double m_globalDbSettingRemote; + double m_soundDbSetting; + std::mutex m_mutex; + std::atomic m_state; + bool m_localPlayback; + bool m_muteMyself; +}; + diff --git a/src/ts3log.cpp b/src/ts3log.cpp index 3380a4c..edc4bbc 100644 --- a/src/ts3log.cpp +++ b/src/ts3log.cpp @@ -1,48 +1,48 @@ -// src/ts3log.cpp -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - - -#include "common.h" - -#include -#include - - -void logMessage(const char *msg, LogLevel level, ...) -{ - char buf[512]; - va_list argptr; - - va_start(argptr, level); - vsnprintf(buf, 512, msg, argptr); - va_end(argptr); - - ts3Functions.logMessage(buf, level, "SB", 0); -} - -#define logError(msg, ...) logMessage(msg, LogLevel_ERROR, __VA_ARGS__) -#define logInfo(msg, ...) logMessage(msg, LogLevel_INFO, __VA_ARGS__) -#define logWarning(msg, ...) logMessage(msg, LogLevel_WARNING, __VA_ARGS__) - - -UINT checkError(UINT code, const char *msg, ...) -{ - if(code != ERROR_ok) - { - char buf[512]; - va_list argptr; - - va_start(argptr, msg); - vsnprintf(buf, 512, msg, argptr); - va_end(argptr); - - ts3Functions.logMessage(buf, LogLevel_ERROR, "SB", 0); - } - - return code; -} +// src/ts3log.cpp +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + + +#include "common.h" + +#include +#include + + +void logMessage(const char *msg, LogLevel level, ...) +{ + char buf[512]; + va_list argptr; + + va_start(argptr, level); + vsnprintf(buf, 512, msg, argptr); + va_end(argptr); + + ts3Functions.logMessage(buf, level, "SB", 0); +} + +#define logError(msg, ...) logMessage(msg, LogLevel_ERROR, __VA_ARGS__) +#define logInfo(msg, ...) logMessage(msg, LogLevel_INFO, __VA_ARGS__) +#define logWarning(msg, ...) logMessage(msg, LogLevel_WARNING, __VA_ARGS__) + + +UINT checkError(UINT code, const char *msg, ...) +{ + if(code != ERROR_ok) + { + char buf[512]; + va_list argptr; + + va_start(argptr, msg); + vsnprintf(buf, 512, msg, argptr); + va_end(argptr); + + ts3Functions.logMessage(buf, LogLevel_ERROR, "SB", 0); + } + + return code; +} diff --git a/src/ts3log.h b/src/ts3log.h index 230e976..272e479 100644 --- a/src/ts3log.h +++ b/src/ts3log.h @@ -1,23 +1,23 @@ -// src/ts3log.h -//---------------------------------- -// RP Soundboard Source Code -// Copyright (c) 2015 Marius Graefe -// All rights reserved -// Contact: rp_soundboard@mgraefe.de -//---------------------------------- - -#pragma once - -#include "common.h" - -void logMessage(const char *msg, LogLevel level, ...); - -#define logError(msg, ...) logMessage(msg, LogLevel_ERROR, ##__VA_ARGS__) -#define logInfo(msg, ...) logMessage(msg, LogLevel_INFO, ##__VA_ARGS__) -#define logWarning(msg, ...) logMessage(msg, LogLevel_WARNING, ##__VA_ARGS__) -#define logDebug(msg, ...) logMessage(msg, LogLevel_DEBUG, ##__VA_ARGS__) -#define logCritical(msg, ...) logMessage(msg, LogLevel_CRITICAL, ##__VA_ARGS__) - - -UINT checkError(UINT code, const char *msg, ...); - +// src/ts3log.h +//---------------------------------- +// RP Soundboard Source Code +// Copyright (c) 2015 Marius Graefe +// All rights reserved +// Contact: rp_soundboard@mgraefe.de +//---------------------------------- + +#pragma once + +#include "common.h" + +void logMessage(const char *msg, LogLevel level, ...); + +#define logError(msg, ...) logMessage(msg, LogLevel_ERROR, ##__VA_ARGS__) +#define logInfo(msg, ...) logMessage(msg, LogLevel_INFO, ##__VA_ARGS__) +#define logWarning(msg, ...) logMessage(msg, LogLevel_WARNING, ##__VA_ARGS__) +#define logDebug(msg, ...) logMessage(msg, LogLevel_DEBUG, ##__VA_ARGS__) +#define logCritical(msg, ...) logMessage(msg, LogLevel_CRITICAL, ##__VA_ARGS__) + + +UINT checkError(UINT code, const char *msg, ...); +