diff --git a/.gitignore b/.gitignore index 16242e2a62..27ded1a9d1 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ app/gui/qt/moc_mainwindow.cpp app/gui/qt/moc_sonicpiudpserver.cpp app/gui/qt/qrc_SonicPi.cpp app/gui/qt/utils/ruby_help.h +app/gui/qt/utils/lang_list.h app/gui/qt/help/*.html app/gui/qt/help_files.qrc app/gui/qt/lang/*.qm diff --git a/app/gui/qt/CMakeLists.txt b/app/gui/qt/CMakeLists.txt index b4394a0076..32c818f98f 100644 --- a/app/gui/qt/CMakeLists.txt +++ b/app/gui/qt/CMakeLists.txt @@ -74,12 +74,15 @@ set(QT_SOURCES set(SOURCES ${QTAPP_ROOT}/main.cpp ${QTAPP_ROOT}/utils/scintilla_api.cpp + ${QTAPP_ROOT}/utils/sonicpi_i18n.cpp ${QTAPP_ROOT}/widgets/sonicpilog.cpp ${QTAPP_ROOT}/widgets/sonicpilog.h ${QTAPP_ROOT}/widgets/sonicpicontext.cpp ${QTAPP_ROOT}/widgets/sonicpicontext.h ${QTAPP_ROOT}/utils/scintilla_api.h + ${QTAPP_ROOT}/utils/sonicpi_i18n.h ${QTAPP_ROOT}/utils/ruby_help.h + ${QTAPP_ROOT}/utils/lang_list.h ${QTAPP_ROOT}/model/settings.h ) diff --git a/app/gui/qt/main.cpp b/app/gui/qt/main.cpp index 35d6522fd4..660de9a750 100644 --- a/app/gui/qt/main.cpp +++ b/app/gui/qt/main.cpp @@ -11,12 +11,13 @@ // notice is included. //++ +#include + #include #include #include #include #include -#include #include #include "mainwindow.h" @@ -35,7 +36,7 @@ int main(int argc, char *argv[]) { - + std::cout << "Starting Sonic Pi..." << std::endl; #ifndef Q_OS_MAC Q_INIT_RESOURCE(SonicPi); #endif @@ -49,16 +50,6 @@ int main(int argc, char *argv[]) qRegisterMetaType("SonicPiLog::MultiMessage"); - QString systemLocale = QLocale::system().uiLanguages()[0].replace("-", "_"); - - QTranslator qtTranslator; - qtTranslator.load("qt_" + systemLocale, QLibraryInfo::location(QLibraryInfo::TranslationsPath)); - app.installTranslator(&qtTranslator); - - QTranslator translator; - bool i18n = translator.load(QLatin1String("sonic-pi_") + systemLocale, QLatin1String(":/lang")) || systemLocale.startsWith("en") || systemLocale == "C"; - app.installTranslator(&translator); - app.setApplicationName(QObject::tr("Sonic Pi")); app.setStyle("gtk"); @@ -75,7 +66,7 @@ int main(int argc, char *argv[]) splash->show(); splash->repaint(); app.processEvents(); - MainWindow mainWin(app, i18n, splash); + MainWindow mainWin(app, splash); return app.exec(); #elif _WIN32 @@ -109,7 +100,7 @@ int main(int argc, char *argv[]) splash->show(); splash->repaint(); app.processEvents(); - MainWindow mainWin(app, i18n, splash); + MainWindow mainWin(app, splash); // Fix for full screen mode. See: https://doc.qt.io/qt-5/windows-issues.html#fullscreen-opengl-based-windows QWindowsWindowFunctions::setHasBorderInFullScreen(mainWin.windowHandle(), true); @@ -139,9 +130,8 @@ int main(int argc, char *argv[]) splashWindow->show(); app.processEvents(); - MainWindow mainWin(app, i18n, splashWindow); + MainWindow mainWin(app, splashWindow); return app.exec(); #endif - } diff --git a/app/gui/qt/mainwindow.cpp b/app/gui/qt/mainwindow.cpp index 855cb6e14b..f71cf1b94e 100644 --- a/app/gui/qt/mainwindow.cpp +++ b/app/gui/qt/mainwindow.cpp @@ -52,6 +52,8 @@ #include "widgets/sonicpilexer.h" #include "widgets/sonicpiscintilla.h" +#include "utils/sonicpi_i18n.h" + #include "utils/borderlesslinksproxystyle.h" #include "visualizer/scope_window.h" @@ -89,9 +91,9 @@ using namespace std::chrono; using namespace SonicPi; #ifdef Q_OS_MAC -MainWindow::MainWindow(QApplication& app, bool i18n, QMainWindow* splash) +MainWindow::MainWindow(QApplication& app, QMainWindow* splash) #else -MainWindow::MainWindow(QApplication& app, bool i18n, QSplashScreen* splash) +MainWindow::MainWindow(QApplication& app, QSplashScreen* splash) #endif { app.installEventFilter(this); @@ -103,7 +105,6 @@ MainWindow::MainWindow(QApplication& app, bool i18n, QSplashScreen* splash) this->piSettings = new SonicPiSettings(); this->splash = splash; - this->i18n = i18n; // API and Client m_spClient = std::make_shared(this); @@ -126,7 +127,6 @@ MainWindow::MainWindow(QApplication& app, bool i18n, QSplashScreen* splash) version_num = 0; latest_version_num = 0; this->splash = splash; - this->i18n = i18n; QString settings_path = sonicPiConfigPath() + QDir::separator() + "gui-settings.ini"; gui_settings = new QSettings(settings_path, QSettings::IniFormat); @@ -139,6 +139,19 @@ MainWindow::MainWindow(QApplication& app, bool i18n, QSplashScreen* splash) m_spAPI->Init(rootPath().toStdString()); guiID = QString::fromStdString(m_spAPI->GetGuid()); + this->sonicPii18n = new SonicPii18n(rootPath()); + std::cout << "[GUI] - Language setting: " << piSettings->language.toUtf8().constData() << std::endl; + std::cout << "[GUI] - System language: " << QLocale::system().name().toStdString() << std::endl; + this->ui_language = sonicPii18n->determineUILanguage(piSettings->language); + std::cout << "[GUI] - Using language: " << ui_language.toUtf8().constData() << std::endl; + this->i18n = sonicPii18n->loadTranslations(ui_language); + + if(i18n) { + std::cout << "[GUI] - translations available " << std::endl; + } else { + std::cout << "[GUI] - translations unavailable (using EN)" << std::endl; + } + std::cout << "[GUI] - hiding main window" << std::endl; hide(); @@ -212,6 +225,11 @@ MainWindow::MainWindow(QApplication& app, bool i18n, QSplashScreen* splash) toggleOSCServer(1); app.setActiveWindow(tabs->currentWidget()); + + if (!i18n) { + showLanguageLoadingError(); + } + showWelcomeScreen(); } @@ -348,7 +366,8 @@ void MainWindow::setupWindowStructure() prefsWidget->setAllowedAreas(Qt::RightDockWidgetArea); prefsWidget->setFeatures(QDockWidget::DockWidgetClosable); - settingsWidget = new SettingsWidget(m_spAPI->GetPort(SonicPiPortId::server_osc_cues), piSettings, this); + settingsWidget = new SettingsWidget(m_spAPI->GetPort(SonicPiPortId::server_osc_cues), i18n, piSettings, sonicPii18n, this); + connect(settingsWidget, SIGNAL(restartApp()), this, SLOT(restartApp())); connect(settingsWidget, SIGNAL(volumeChanged(int)), this, SLOT(changeSystemPreAmp(int))); connect(settingsWidget, SIGNAL(mixerSettingsChanged()), this, SLOT(mixerSettingsChanged())); connect(settingsWidget, SIGNAL(midiSettingsChanged()), this, SLOT(toggleMidi())); @@ -1216,6 +1235,18 @@ void MainWindow::startupError(QString msg) // TODO: Add format error to API } +void MainWindow::showLanguageLoadingError() { + QMessageBox msgBox(this); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setText(QString(tr("Failed to load translations for language: %1")).arg(sonicPii18n->getNativeLanguageName(this->ui_language))); + msgBox.setInformativeText(tr("Falling back to English. Sorry about this.") + "\n" + tr("Please consider reporting a bug at") + "\nhttp://github.com/sonic-pi-net/sonic-pi/issues"); + + QPushButton *okButton = msgBox.addButton(tr("OK"), QMessageBox::AcceptRole); + msgBox.setDefaultButton(okButton); + + msgBox.exec(); +} + void MainWindow::replaceBuffer(QString id, QString content, int line, int index, int first_line) { SonicPiScintilla* ws = filenameToWorkspace(id.toStdString()); @@ -2880,6 +2911,39 @@ void MainWindow::createToolBar() viewMenu->addAction(focusHelpListingAct); viewMenu->addAction(focusHelpDetailsAct); viewMenu->addAction(focusErrorsAct); + + languageMenu = menuBar()->addMenu(tr("Language")); + QStringList available_languages = sonicPii18n->getAvailableLanguages(); + + langActionGroup = new QActionGroup(this); + langActionGroup->setExclusionPolicy(QActionGroup::ExclusionPolicy::Exclusive); + + QSignalMapper *signalMapper = new QSignalMapper(this); + + for (size_t i = 0; i < available_languages.length(); i += 1) { + bool is_current_lang = (available_languages[i] == piSettings->language); + + QAction *langAct = new QAction(sonicPii18n->getNativeLanguageName(available_languages[i]), this); + langAct->setCheckable(true); + langAct->setChecked(is_current_lang); + + connect(langAct, SIGNAL(triggered()), signalMapper, SLOT(map())); + signalMapper->setMapping(langAct, i); + + langActionGroup->addAction(langAct); + languageMenu->addAction(langAct); + + if (i == 0) { // add separator after System language + languageMenu->addSeparator(); + } + } + + connect(signalMapper, SIGNAL(mappedInt(int)), settingsWidget, SLOT(updateUILanguage(int))); + connect(settingsWidget, SIGNAL(uiLanguageChanged(QString)), this, SLOT(updateSelectedUILanguageAction(QString))); +} + +void MainWindow::updateSelectedUILanguageAction(QString lang) { + langActionGroup->actions()[sonicPii18n->getAvailableLanguages().indexOf(lang)]->setChecked(true); } QString MainWindow::readFile(QString name) @@ -3074,6 +3138,7 @@ void MainWindow::readSettings() std::cout << "[GUI] - reading settings" << std::endl; // Read in preferences from previous session + piSettings->language = gui_settings->value("prefs/language", "system_language").toString(); piSettings->show_buttons = gui_settings->value("prefs/show-buttons", true).toBool(); piSettings->show_tabs = gui_settings->value("prefs/show-tabs", true).toBool(); piSettings->show_log = gui_settings->value("prefs/show-log", true).toBool(); @@ -3100,6 +3165,7 @@ void MainWindow::readSettings() piSettings->show_scope_labels = gui_settings->value("prefs/scope/show-labels", false).toBool(); piSettings->show_cues = gui_settings->value("prefs/show_cues", true).toBool(); QString styleName = gui_settings->value("prefs/theme", "").toString(); + piSettings->themeStyle = theme->themeNameToStyle(styleName); piSettings->show_autocompletion = gui_settings->value("prefs/show-autocompletion", true).toBool(); piSettings->show_context = gui_settings->value("prefs/show-context", true).toBool(); @@ -3121,10 +3187,13 @@ void MainWindow::restoreScopeState(std::vector names) void MainWindow::writeSettings() { std::cout << "[GUI] - writing settings" << std::endl; + gui_settings->setValue("pos", pos()); gui_settings->setValue("size", size()); gui_settings->setValue("first_time", 0); + gui_settings->setValue("prefs/language", piSettings->language); + gui_settings->setValue("prefs/midi-default-channel", piSettings->midi_default_channel); gui_settings->setValue("prefs/midi-enable", piSettings->midi_enabled); gui_settings->setValue("prefs/osc-public", piSettings->osc_public); @@ -3172,6 +3241,9 @@ void MainWindow::writeSettings() gui_settings->setValue("docsplitState", docsplit->saveState()); gui_settings->setValue("windowState", saveState()); gui_settings->setValue("windowGeom", saveGeometry()); + + // Force Qt to write the settings to the ini file + gui_settings->sync(); } void MainWindow::loadFile(const QString& fileName, SonicPiScintilla*& text) @@ -3261,6 +3333,31 @@ void MainWindow::onExitCleanup() } } +void MainWindow::restartApp() { + QApplication* app = dynamic_cast(parent()); + statusBar()->showMessage(tr("Restarting Sonic Pi..."), 10000); + + // Save settings and perform some cleanup + writeSettings(); + onExitCleanup(); + std::cout << "Performing application restart..." << std::endl; + + // Create new process + QStringList args = qApp->arguments(); + args.removeFirst(); + QProcess process; + bool restart_success = process.startDetached(qApp->arguments()[0], args); + if (restart_success) { + std::cout << "Successfully restarted sonic-pi" << std::endl; + } else { + std::cout << "Failed to restart sonic-pi" << std::endl; + } + + // Quit + app->exit(0); + exit(0); +} + void MainWindow::heartbeatOSC() { // Message msg("/gui-heartbeat"); diff --git a/app/gui/qt/mainwindow.h b/app/gui/qt/mainwindow.h index e1d330cb74..316b170879 100644 --- a/app/gui/qt/mainwindow.h +++ b/app/gui/qt/mainwindow.h @@ -17,6 +17,7 @@ #include #include + #include #include #include @@ -34,6 +35,7 @@ #include "config.h" class QAction; +class QActionGroup; class QMenu; class QToolBar; class QLineEdit; @@ -67,6 +69,7 @@ class InfoWidget; class SettingsWidget; class Scope; class ScintillaAPI; +class SonicPii18n; class SonicPiLog; class SonicPiScintilla; class SonicPiTheme; @@ -91,9 +94,9 @@ class MainWindow : public QMainWindow public: #if defined(Q_OS_MAC) - MainWindow(QApplication &ref, bool i18n, QMainWindow* splash); + MainWindow(QApplication &ref, QMainWindow* splash); #else - MainWindow(QApplication &ref, bool i18n, QSplashScreen* splash); + MainWindow(QApplication &ref, QSplashScreen* splash); #endif SonicPiLog* GetOutputPane() const; @@ -118,6 +121,8 @@ class MainWindow : public QMainWindow bool loaded_workspaces; QString hash_salt; + QString ui_language; + protected: void closeEvent(QCloseEvent *event); @@ -132,6 +137,7 @@ class MainWindow : public QMainWindow private slots: + void updateSelectedUILanguageAction(QString lang); void updateContext(int line, int index); void updateContextWithCurrentWs(); void docLinkClicked(const QUrl &url); @@ -178,6 +184,7 @@ class MainWindow : public QMainWindow void help(); void toggleHelpIcon(); void onExitCleanup(); + void restartApp(); void toggleRecording(); void toggleRecordingOnIcon(); void changeSystemPreAmp(int val, int silent=0); @@ -229,6 +236,7 @@ class MainWindow : public QMainWindow void splashClose(); void setMessageBoxStyle(); void startupError(QString msg); + void showLanguageLoadingError(); void tabNext(); void tabPrev(); void tabGoto(int index); @@ -327,11 +335,13 @@ class MainWindow : public QMainWindow QString rootPath(); void addUniversalCopyShortcuts(QTextEdit *te); + void updateTranslatedUIText(); - QMenu *liveMenu, *codeMenu, *audioMenu, *displayMenu, *viewMenu, *ioMenu, *ioMidiInMenu, *ioMidiOutMenu, *ioMidiOutChannelMenu, *localIpAddressesMenu, *themeMenu, *scopeKindVisibilityMenu; + QMenu *liveMenu, *codeMenu, *audioMenu, *displayMenu, *viewMenu, *ioMenu, *ioMidiInMenu, *ioMidiOutMenu, *ioMidiOutChannelMenu, *localIpAddressesMenu, *themeMenu, *scopeKindVisibilityMenu, *languageMenu; QSettings *gui_settings; SonicPiSettings *piSettings; + SonicPii18n *sonicPii18n; bool focusMode; QCheckBox *startup_error_reported; @@ -380,6 +390,7 @@ class MainWindow : public QMainWindow QAction *exitAct, *runAct, *stopAct, *saveAsAct, *loadFileAct, *recAct, *textAlignAct, *textIncAct, *textDecAct, *scopeAct, *infoAct, *helpAct, *prefsAct, *focusEditorAct, *focusLogsAct, *focusContextAct, *focusCuesAct, *focusPreferencesAct, *focusHelpListingAct, *focusHelpDetailsAct, *focusErrorsAct, *showLineNumbersAct, *showAutoCompletionAct, *showContextAct, *audioSafeAct, *audioTimingGuaranteesAct, *enableExternalSynthsAct, *mixerInvertStereoAct, *mixerForceMonoAct, *midiEnabledAct, *enableOSCServerAct, *allowRemoteOSCAct, *showLogAct, *showCuesAct, *logAutoScrollAct, *logCuesAct, *logSynthsAct, *clearOutputOnRunAct, *autoIndentOnRunAct, *showButtonsAct, *showTabsAct, *fullScreenAct, *lightThemeAct, *darkThemeAct, *proLightThemeAct, *proDarkThemeAct, *highContrastThemeAct, *showScopeLabelsAct; QShortcut *runSc, *stopSc, *saveAsSc, *loadFileSc, *recSc, *textAlignSc, *textIncSc, *textDecSc, *scopeSc, *infoSc, *helpSc, *prefsSc, *focusEditorSc, *focusLogsSc, *focusContextSc, *focusCuesSc, *focusPreferencesSc, *focusHelpListingSc, *focusHelpDetailsSc, *focusErrorsSc; + QActionGroup *langActionGroup; SettingsWidget *settingsWidget; diff --git a/app/gui/qt/model/settings.h b/app/gui/qt/model/settings.h index 757da5134f..57f1e28df9 100644 --- a/app/gui/qt/model/settings.h +++ b/app/gui/qt/model/settings.h @@ -24,6 +24,7 @@ class SonicPiSettings { QString midi_default_channel_str; // EditorSettings + QString language; bool auto_indent_on_run; bool show_line_numbers; bool show_log; diff --git a/app/gui/qt/theme/app.qss b/app/gui/qt/theme/app.qss index 9edc265cdc..aa70a384d6 100644 --- a/app/gui/qt/theme/app.qss +++ b/app/gui/qt/theme/app.qss @@ -535,3 +535,8 @@ QPushButton::pressed background-color: pressedButtonColor; color: pressedButtonTextColor; } + +QMessageBox +{ + background: windowColor; +} diff --git a/app/gui/qt/utils/lang_list.tmpl b/app/gui/qt/utils/lang_list.tmpl new file mode 100644 index 0000000000..c8f0703033 --- /dev/null +++ b/app/gui/qt/utils/lang_list.tmpl @@ -0,0 +1,16 @@ +//-- +// This file is part of Sonic Pi: http://sonic-pi.net +// Full project source: https://github.com/samaaron/sonic-pi +// License: https://github.com/samaaron/sonic-pi/blob/main/LICENSE.md +// +// Copyright 2020 by Sam Aaron (http://sam.aaron.name). +// All rights reserved. +// +// Permission is granted for use, copying, modification, distribution, +// and distribution of modified versions of this work as long as this +// notice is included. +//++ + +// AUTO-GENERATED-DOCS +// Do not manually add any code below this comment +// otherwise it may be removed diff --git a/app/gui/qt/utils/sonicpi_i18n.cpp b/app/gui/qt/utils/sonicpi_i18n.cpp new file mode 100644 index 0000000000..8f10fcb321 --- /dev/null +++ b/app/gui/qt/utils/sonicpi_i18n.cpp @@ -0,0 +1,207 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "sonicpi_i18n.h" +#include "lang_list.h" + +SonicPii18n::SonicPii18n(QString rootpath) { + this->root_path = rootpath; + this->system_languages = findSystemLanguages(); + this->system_language_available = true; // Set to true unless we can't load the system language + this->available_languages = findAvailableLanguages(); + this->currently_loaded_language = "en"; + + //checkAllTranslations(); // For testing and debugging purposes +} + +SonicPii18n::~SonicPii18n() { +} + +QString SonicPii18n::determineUILanguage(QString lang_pref) { + QStringList available_languages = getAvailableLanguages(); + //std::cout << available_languages.join("\n").toUtf8().constData() << std::endl; + QLocale locale; + + if (lang_pref != "system_language") { + if (available_languages.contains(lang_pref)) { + return lang_pref; + } + + // Add the general language as a fallback (e.g. pt_BR -> pt) + QString general_name = lang_pref; + general_name.truncate(lang_pref.lastIndexOf('_')); + general_name.truncate(general_name.lastIndexOf('-')); + + if (available_languages.contains(general_name)) { + return general_name; + } + } else { + QStringList preferred_languages = locale.uiLanguages(); + // If the setting is set to system_language... + // ...run through the list of preferred languages + std::cout << "[GUI] [i18n] - Looping through preferred ui languages" << std::endl; + + QString l; + for (int i = 0; i < preferred_languages.length(); i += 1) { + l = preferred_languages[i]; + l.replace("-", "_"); + + //std::cout << preferred_languages[i].toUtf8().constData() << std::endl; + if (available_languages.contains(l)) { + return l; + } + } + } + + // Fallback to English + this->system_language_available = false; + return "en"; +} + +QStringList SonicPii18n::findSystemLanguages() { + QLocale locale; + QStringList preferred_languages = locale.uiLanguages(); + std::cout << "[GUI] [i18n] - Looping through preferred ui languages" << std::endl; + for (int i = 0; i < preferred_languages.length(); i += 1) { + preferred_languages[i] = preferred_languages[i].replace("-", "_"); + } + return preferred_languages; +} + +QStringList SonicPii18n::findAvailableLanguages() { + QStringList languages; + + QString m_langPath = root_path + "/app/gui/qt/lang"; + //std::cout << m_langPath.toUtf8().constData() << std::endl; + QDir dir(m_langPath); + QStringList fileNames = dir.entryList(QStringList("sonic-pi_*.qm")); + + for (int i = 0; i < fileNames.size(); ++i) { + // extract the language from the filename + QString lang; + lang = fileNames[i]; // "sonic-pi_pt_BR.qm" + lang.truncate(lang.lastIndexOf('.')); // "sonic-pi_pt_BR" + lang.remove(0, lang.lastIndexOf("sonic-pi_") + 9); // "pt_BR" + //lang.replace("_", "-"); // Replace underscores with dashes so it matches the language codes e.g: "pt-BR" + //std::cout << lang.toUtf8().constData() << '\n'; + languages << lang; + } + // Add the source language + languages << "en_GB"; + languages.sort(); + return languages; +} + +bool SonicPii18n::loadTranslations(QString lang) { + QString language = lang; + bool i18n = false; + QCoreApplication* app = QCoreApplication::instance(); + + // Remove any previous translations + app->removeTranslator(&translator); + app->removeTranslator(&qtTranslator); + + std::cout << "[GUI] [i18n] - Loading translations for " << language.toUtf8().constData() << std::endl; + + i18n = translator.load("sonic-pi_" + language, ":/lang/") || language == "en_GB" || language == "en" || language == "C"; + if (!i18n) { + std::cout << "[GUI] [i18n] - Error: Failed to load language translation for " << language.toUtf8().constData() << std::endl; + language = "en_GB"; + } + app->installTranslator(&translator); + + qtTranslator.load("qt_" + language, QLibraryInfo::location(QLibraryInfo::TranslationsPath)); + app->installTranslator(&qtTranslator); + + this->currently_loaded_language = language; + + return i18n; +} + +QStringList SonicPii18n::getAvailableLanguages() { + QStringList list = this->available_languages; + list.prepend("system_language"); + return list; +} + +QStringList SonicPii18n::getSystemLanguages() { + return system_languages; +}; + +bool SonicPii18n::isSystemLanguageAvailable() { + return system_language_available; +}; + +QString SonicPii18n::currentlyLoadedLanguage() { + return currently_loaded_language; +}; + +QString SonicPii18n::getNativeLanguageName(QString lang) { + if (lang == "system_language") { + return tr("System language"); + } + + std::map::iterator it = native_language_names.find(lang); + if(it != native_language_names.end()) { + // language found + return native_language_names[lang]; + } else { + std::cout << "[GUI] [i18n] - Warning: Predefined language name not found: '" << lang.toUtf8().constData() << "'" << std::endl; + // Try using QLocale to find the native language name + QLocale locale(lang); + QString name = locale.nativeLanguageName(); + if (name != "C" && name != "") { + return locale.nativeLanguageName(); + } else { + std::cout << "[GUI] [i18n] - Warning: Invalid language code: '" << lang.toUtf8().constData() << "'" << std::endl; + return lang; + } + } +} + +QStringList SonicPii18n::getNativeLanguageNames(QStringList languages) { + QStringList language_names; + for (size_t i = 0; i < languages.length(); i += 1) { + language_names << getNativeLanguageName(languages[i]); + } + return language_names; +} + +// For testing and debugging purposes +bool SonicPii18n::checkAllTranslations() { + QStringList failed_to_load = QStringList(); + bool all_succeeded = true; + + std::cout << "==========================================" << std::endl; + std::cout << "Testing all found language translations..." << std::endl; + for (int i = 0; i < this->available_languages.size(); ++i) { + QString lang = this->available_languages[i]; + bool success = loadTranslations(lang); + if (success) { + std::cout << lang.toUtf8().constData() << ": ✓" << std::endl; + } else { + std::cout << lang.toUtf8().constData() << ": ✗" << std::endl; + failed_to_load << lang; + all_succeeded = false; + } + } + std::cout << "Done" << std::endl; + + std::cout << "------------------------------------------" << std::endl; + if (failed_to_load.length() > 0) { + std::cout << "Some translations failed to load:" << std::endl; + std::cout << failed_to_load.join("\n").toUtf8().constData() << std::endl; + } else { + std::cout << "All found translations loaded successfully :)" << std::endl; + } + std::cout << "==========================================" << std::endl; + + return all_succeeded; +} diff --git a/app/gui/qt/utils/sonicpi_i18n.h b/app/gui/qt/utils/sonicpi_i18n.h new file mode 100644 index 0000000000..a12949a998 --- /dev/null +++ b/app/gui/qt/utils/sonicpi_i18n.h @@ -0,0 +1,56 @@ +//-- +// This file is part of Sonic Pi: http://sonic-pi.net +// Full project source: https://github.com/samaaron/sonic-pi +// License: https://github.com/samaaron/sonic-pi/blob/main/LICENSE.md +// +// Copyright 2013, 2014, 2015, 2016 by Sam Aaron (http://sam.aaron.name). +// All rights reserved. +// +// Permission is granted for use, copying, modification, and +// distribution of modified versions of this work as long as this +// notice is included. +//++ + +#include +#include + +#ifndef SONICPI_I18N_H +#define SONICPI_I18N_H + +class SonicPii18n : public QObject { +public: + SonicPii18n(QString rootpath); + ~SonicPii18n(); + + +public slots: + QString determineUILanguage(QString lang_pref); + bool loadTranslations(QString lang); + + QStringList getAvailableLanguages(); + QStringList getSystemLanguages(); + bool isSystemLanguageAvailable(); + QString currentlyLoadedLanguage(); + + QString getNativeLanguageName(QString lang); + QStringList getNativeLanguageNames(QStringList languages); + + +private: + QString root_path; + + QTranslator qtTranslator; + QTranslator translator; + + QStringList available_languages; + QStringList system_languages; + bool system_language_available; + QString currently_loaded_language; + + static std::map native_language_names; + + QStringList findAvailableLanguages(); + QStringList findSystemLanguages(); + bool checkAllTranslations(); +}; +#endif diff --git a/app/gui/qt/widgets/settingswidget.cpp b/app/gui/qt/widgets/settingswidget.cpp index e124349131..66449cd059 100644 --- a/app/gui/qt/widgets/settingswidget.cpp +++ b/app/gui/qt/widgets/settingswidget.cpp @@ -1,4 +1,5 @@ #include "settingswidget.h" +#include "utils/sonicpi_i18n.h" #include #include @@ -15,14 +16,19 @@ #include #include #include +#include #include /** * Default Constructor */ -SettingsWidget::SettingsWidget( int port, SonicPiSettings *piSettings, QWidget *parent) { +SettingsWidget::SettingsWidget(int port, bool i18n, SonicPiSettings *piSettings, SonicPii18n *sonicPii18n, QWidget *parent) { this->piSettings = piSettings; + this->i18n = i18n; + this->sonicPii18n = sonicPii18n; + this->available_languages = sonicPii18n->getAvailableLanguages(); server_osc_cues_port = port; + prefTabs = new QTabWidget(); QGridLayout *grid = new QGridLayout; @@ -43,25 +49,31 @@ SettingsWidget::SettingsWidget( int port, SonicPiSettings *piSettings, QWidget QGroupBox *update_prefs_box = createUpdatePrefsTab(); prefTabs->addTab(update_prefs_box, tr("Updates")); - if (!i18n) { - QGroupBox *translation_box = new QGroupBox("Translation"); - QVBoxLayout *translation_box_layout = new QVBoxLayout; - QLabel *go_translate = new QLabel; - go_translate->setOpenExternalLinks(true); - go_translate->setText( - "Sonic Pi hasn't been translated to " + - QLocale::languageToString(QLocale::system().language()) + - " yet.
" + - "We rely on crowdsourcing to help create and maintain translations.
" + - "" + - "Please consider helping to translate Sonic Pi to your language. " - ); - go_translate->setTextFormat(Qt::RichText); - translation_box_layout->addWidget(go_translate); - translation_box->setLayout(translation_box_layout); - - grid->addWidget(translation_box, 3, 0, 1, 2); + QGroupBox *language_prefs_box = createLanguagePrefsTab(); + prefTabs->addTab(language_prefs_box, tr("Language")); + + if (piSettings->language == "system_language") { + if (!sonicPii18n->isSystemLanguageAvailable()) { + QGroupBox *translation_box = new QGroupBox("Translation"); + QVBoxLayout *translation_box_layout = new QVBoxLayout; + QLabel *go_translate = new QLabel; + go_translate->setOpenExternalLinks(true); + go_translate->setText( + "Sonic Pi hasn't been translated to " + + QLocale::languageToString(QLocale::system().language()) + + " yet.
" + + "We rely on crowdsourcing to help create and maintain translations.
" + + "" + + "Please consider helping to translate Sonic Pi to your language. " + ); + go_translate->setTextFormat(Qt::RichText); + translation_box_layout->addWidget(go_translate); + translation_box->setLayout(translation_box_layout); + + grid->addWidget(translation_box, 3, 0, 1, 2); + } } + settingsChanged(); connectAll(); setLayout(grid); @@ -365,11 +377,14 @@ QGroupBox* SettingsWidget::createEditorPrefsTab() { debug_box_layout->addWidget(clear_output_on_run); debug_box->setLayout(debug_box_layout); + + gridEditorPrefs->addWidget(editor_display_box, 0, 0); gridEditorPrefs->addWidget(editor_look_feel_box, 0, 1); gridEditorPrefs->addWidget(automation_box, 1, 1); gridEditorPrefs->addWidget(debug_box, 1, 0); + editor_box->setLayout(gridEditorPrefs); return editor_box; } @@ -463,6 +478,45 @@ QGroupBox* SettingsWidget::createUpdatePrefsTab() { return update_prefs_box; } +/** + * create Language Preferences Tab of Settings Widget + */ +QGroupBox* SettingsWidget::createLanguagePrefsTab() { + QGroupBox *language_box = new QGroupBox(tr("Language")); + language_box->setToolTip(tr("Configure language settings")); + QSizePolicy languagePrefSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + language_box->setSizePolicy(languagePrefSizePolicy); + + language_option_label = new QLabel; + language_option_label->setText(tr("UI & Tutorial Language (Requires a restart to take effect)")); + language_option_label->setToolTip(tr("Change the language of the UI & Tutorial (Requires a restart to take effect)")); + + language_combo = new QComboBox(); + add_language_combo_box_entries(language_combo); + language_combo->setToolTip(tr("Change the language of the UI & Tutorial")); + language_combo->setMinimumContentsLength(2); + language_combo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength); + + language_details_label = new QLabel; + + language_info_label = new QLabel; + language_info_label->setText(tr("Translations have been generously provided by volunteers \non https://hosted.weblate.org/projects/sonic-pi/. Thank you! :)")); + + QVBoxLayout *language_box_layout = new QVBoxLayout; + + language_box_layout->addWidget(language_option_label); + language_box_layout->addWidget(language_combo); + language_box_layout->addWidget(language_details_label); + language_box_layout->addWidget(language_info_label); + + language_box->setLayout(language_box_layout); + + QGroupBox *language_prefs_box = new QGroupBox(); + QGridLayout *language_prefs_box_layout = new QGridLayout; + language_prefs_box_layout->addWidget(language_box, 0, 0, 0, 0); + language_prefs_box->setLayout(language_prefs_box_layout); + return language_prefs_box; +} // TODO utils? QString SettingsWidget::tooltipStrShiftMeta(char key, QString str) { @@ -490,7 +544,11 @@ void SettingsWidget::updateScopeKindVisibility() { QCheckBox *cb = qobject_cast(scope_box_kinds_layout->itemAt(i)->widget()); cb->setChecked(piSettings->isScopeActive(cb->text())); } +} +void SettingsWidget::updateSelectedUILanguage(QString lang) { + int index = available_languages.indexOf(lang); + language_combo->setCurrentIndex(index); } void SettingsWidget::toggleScope( QWidget* qw ) { @@ -502,6 +560,71 @@ void SettingsWidget::toggleScope( QWidget* qw ) { emit scopeChanged(name); } +// TODO: Implement real-time language switching +void SettingsWidget::updateUILanguage(int index) { + QString lang = available_languages[index]; + std::cout << "Changed language to " << lang.toUtf8().constData() << std::endl; + if (lang != piSettings->language) { + std::cout << "Current language: " << piSettings->language.toUtf8().constData() << std::endl; + std::cout << "New language selected: " << lang.toUtf8().constData() << std::endl; + QString old_lang = sonicPii18n->getNativeLanguageName(piSettings->language); + QString new_lang = sonicPii18n->getNativeLanguageName(lang); + + // Load new language + //QString language = sonicPii18n->determineUILanguage(lang); + //sonicPii18n->loadTranslations(language); + //QString title_new = tr("Updated the UI language from %s to %s").arg(); + + QMessageBox msgBox(this); + msgBox.setText(QString(tr("You've selected a new language: %1")).arg(new_lang)); + QString info_text = ( + tr("Do you want to apply this language?") + + "\n" + + tr("The new language will be applied when you next start Sonic Pi.") + ); + + if (lang == "system_language") { + info_text = tr("System languages found: %1").arg(sonicPii18n->getNativeLanguageNames(sonicPii18n->getSystemLanguages()).join(", ")) + "\n" + info_text; + } + + msgBox.setInformativeText(info_text); + QPushButton *applyButton = msgBox.addButton(tr("Apply"), QMessageBox::ActionRole); + QPushButton *dismissButton = msgBox.addButton(tr("Cancel"), QMessageBox::RejectRole); + msgBox.setDefaultButton(applyButton); + msgBox.setIcon(QMessageBox::Question); + msgBox.exec(); + + if (msgBox.clickedButton() == (QAbstractButton*)applyButton) { + piSettings->language = lang; + updateSelectedUILanguage(piSettings->language); + emit uiLanguageChanged(piSettings->language); + + language_details_label->setText( + tr("The new language will be applied when you next start Sonic Pi.
") + + tr("Current UI language: %1\n").arg(sonicPii18n->getNativeLanguageName(sonicPii18n->currentlyLoadedLanguage())) + ); + + QMessageBox restartMsgBox(this); + restartMsgBox.setText(QString(tr("Restart Sonic Pi?"))); + QString info_text = (tr("Do you want to restart Sonic Pi now? This will stop any current runs & recordings.")); + QPushButton *restartButton = restartMsgBox.addButton(tr("Restart"), QMessageBox::ActionRole); + QPushButton *dismissButton = restartMsgBox.addButton(tr("Dismiss"), QMessageBox::RejectRole); + restartMsgBox.setInformativeText(info_text); + restartMsgBox.setDefaultButton(dismissButton); + restartMsgBox.setIcon(QMessageBox::Question); + restartMsgBox.exec(); + if (restartMsgBox.clickedButton() == (QAbstractButton*)restartButton) { + emit restartApp(); + } + //emit uiLanguageChanged(lang); + } else if (msgBox.clickedButton() == (QAbstractButton*)dismissButton) { + // Don't apply the new language settings + updateSelectedUILanguage(piSettings->language); + emit uiLanguageChanged(piSettings->language); + } + + } +} void SettingsWidget::update_mixer_invert_stereo() { emit mixerSettingsChanged(); @@ -629,7 +752,7 @@ void SettingsWidget::autoIndentOnRun() { } void SettingsWidget::openSonicPiNet() { - QDesktopServices::openUrl(QUrl("http://sonic-pi.net", QUrl::TolerantMode)); + QDesktopServices::openUrl(QUrl("https://sonic-pi.net", QUrl::TolerantMode)); } void SettingsWidget::updateVersionInfo( QString info_string, QString visit, bool sonic_pi_net_visible, bool check_now_visible) { @@ -641,6 +764,7 @@ void SettingsWidget::updateVersionInfo( QString info_string, QString visit, bool void SettingsWidget::updateSettings() { std::cout << "[GUI] - Update Settings" << std::endl; + piSettings->language = available_languages[language_combo->currentIndex()]; piSettings->mixer_invert_stereo = mixer_invert_stereo->isChecked(); piSettings->mixer_force_mono = mixer_force_mono->isChecked(); piSettings->check_args = check_args->isChecked(); @@ -681,6 +805,18 @@ void SettingsWidget::updateSettings() { } void SettingsWidget::settingsChanged() { + language_combo->setCurrentIndex(available_languages.indexOf(piSettings->language)); + QString language_detail_text = ""; + if (!i18n) { + language_detail_text += "Failed to load language translation. Using English (UK)."; + } + if (piSettings->language == "system_language") { + language_detail_text += ( + tr("System languages: %1\n").arg(sonicPii18n->getNativeLanguageNames(sonicPii18n->getSystemLanguages()).join(", ")) + + tr("Current UI language: %1\n").arg(sonicPii18n->getNativeLanguageName(sonicPii18n->currentlyLoadedLanguage())) + ); + } + language_details_label->setText(language_detail_text); mixer_invert_stereo->setChecked(piSettings->mixer_invert_stereo); mixer_force_mono->setChecked(piSettings->mixer_force_mono); @@ -724,6 +860,8 @@ void SettingsWidget::settingsChanged() { } void SettingsWidget::connectAll() { + //connect(language_combo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSettings())); + connect(language_combo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateUILanguage(int))); connect(mixer_invert_stereo, SIGNAL(clicked()), this, SLOT(updateSettings())); connect(mixer_force_mono, SIGNAL(clicked()), this, SLOT(updateSettings())); connect(check_args, SIGNAL(clicked()), this, SLOT(updateSettings())); @@ -798,3 +936,19 @@ void SettingsWidget::connectAll() { connect(clear_output_on_run, SIGNAL(clicked()), this, SLOT(clearOutputOnRun())); connect(auto_indent_on_run, SIGNAL(clicked()), this, SLOT(autoIndentOnRun())); } + +void SettingsWidget::add_language_combo_box_entries(QComboBox* combo) { + // Add language combo entries + std::cout << "[Debug] Adding language combo box entries..." << std::endl; + std::cout << (std::to_string(static_cast(available_languages.size()))) << std::endl; + + for (auto const &language : available_languages) { + std::cout << "[Debug] Adding language " << language.toUtf8().data() << " to the combo box" << std::endl; + if (language != "system_language") { + // Add the language's name to the combo box + combo->addItem(sonicPii18n->getNativeLanguageName(language)); + } else { + combo->addItem(tr("Use system language")); + } + } +} diff --git a/app/gui/qt/widgets/settingswidget.h b/app/gui/qt/widgets/settingswidget.h index 5fd983f862..384f686b49 100644 --- a/app/gui/qt/widgets/settingswidget.h +++ b/app/gui/qt/widgets/settingswidget.h @@ -2,6 +2,7 @@ #define SETTINGSWIDGET_H #include "model/settings.h" +#include "utils/sonicpi_i18n.h" #include @@ -23,15 +24,20 @@ class SettingsWidget : public QWidget Q_OBJECT public: - SettingsWidget( int server_osc_cues_port, SonicPiSettings *piSettings, QWidget *parent = 0); + SettingsWidget(int server_osc_cues_port, bool i18n, SonicPiSettings *piSettings, SonicPii18n *sonicPii18n, QWidget *parent = nullptr); ~SettingsWidget(); void updateVersionInfo( QString info_string, QString visit, bool sonic_pi_net_visible, bool check_now_visible); void updateMidiInPorts( QString in ); void updateMidiOutPorts( QString out ); void updateScopeNames(std::vector); + void updateSelectedUILanguage(QString lang); + QSize sizeHint() const; +public slots: + void updateUILanguage(int index); + private slots: void update_mixer_invert_stereo(); void update_mixer_force_mono(); @@ -68,6 +74,8 @@ private slots: void autoIndentOnRun(); signals: + void restartApp(); + void uiLanguageChanged(QString lang); // TODO: Implement real-time language switching void mixerSettingsChanged(); void oscSettingsChanged(); void midiSettingsChanged(); @@ -100,6 +108,10 @@ private slots: private: SonicPiSettings* piSettings; + SonicPii18n* sonicPii18n; + std::map localeNames; + QStringList available_languages; + bool i18n; int server_osc_cues_port; QTabWidget *prefTabs; @@ -154,13 +166,20 @@ private slots: QSlider *system_vol_slider; QSlider *gui_transparency_slider; + QComboBox *language_combo; + QLabel *language_option_label; + QLabel *language_details_label; + QLabel *language_info_label; + // TODO - bool i18n = true; QGroupBox* createAudioPrefsTab(); QGroupBox* createIoPrefsTab(); QGroupBox* createEditorPrefsTab(); QGroupBox* createVisualizationPrefsTab(); QGroupBox* createUpdatePrefsTab(); + QGroupBox* createLanguagePrefsTab(); + + void add_language_combo_box_entries(QComboBox* combo); QString tooltipStrShiftMeta(char key, QString str); diff --git a/app/server/ruby/bin/qt-doc.rb b/app/server/ruby/bin/qt-doc.rb index 421a192f32..f63b67bec8 100755 --- a/app/server/ruby/bin/qt-doc.rb +++ b/app/server/ruby/bin/qt-doc.rb @@ -30,6 +30,63 @@ include SonicPi::Util +# List of all languages with GUI translation files +@lang_names = Hash[ + "bg" => "български", # Bulgarian + "bn" => "বাংলা", # Bengali/Bangla + "bs" => "Bosanski", # Bosnian + "ca" => "Català", # Catalan + "ca@valencia" => "Valencià", # Valencian + "cs" => "Čeština", # Czech + "da" => "Dansk", # Danish + "de" => "Deutsch", # German + "el" => "ελληνικά", # Greek + "en" => "English", # English + "en_AU" => "English (Australian)", # English (Australian) + "en_GB" => "English (UK)", # English (UK) - default language + "en_US" => "English (US)", # English (US) + "eo" => "Esperanto", # Esperanto + "es" => "Español", # Spanish + "et" => "Eesti keel", # Estonian + "eu" => "Euskara", # Basque + "fa" => "فارسی", # Persian + "fi" => "Suomi", # Finnish + "fr" => "Français", # French + "ga" => "Gaeilge", # Irish + "gl" => "Galego", # Galician + "he" => "עברית", # Hebrew + "hi" => "हिन्दी", # Hindi + "hu" => "Magyar", # Hungarian + "hy" => "Հայերեն", # Armenian + "id" => "Bahasa Indonesia", # Indonesian + "is" => "Íslenska", # Icelandic + "it" => "Italiano", # Italian + "ja" => "日本語", # Japanese + "ka" => "ქართული", # Georgian + "ko" => "한국어", # Korean + "nb" => "Norsk Bokmål", # Norwegian Bokmål + "nl" => "Nederlands", # Dutch (Netherlands) + "pl" => "Polski", # Polish + "pt" => "Português", # Portuguese + "pt_BR" => "Português do Brasil", # Brazilian Portuguese + "ro" => "Română", # Romanian + "ru" => "Pусский", # Russian + "si" => "සිංහල", # Sinhala/Sinhalese + "sk" => "Slovenčina",#/Slovenský Jazyk", # Slovak/Slovakian + "sl" => "Slovenščina",#/Slovenski Jezik", # Slovenian + "sv" => "Svenska", # Swedish + "sw" => "Kiswahili", # Swahili + "th" => "ไทย", # Thai + "tr" => "Türkçe", # Turkish + "ug" => "ئۇيغۇر تىلى", # Uyghur + "uk" => "Українська", # Ukranian + "vi" => "Tiếng Việt", # Vietnamese + "zh" => "中文", # Chinese + "zh-Hans" => "简体中文", # Chinese (Simplified) + "zh_HK" => "廣東話", # Chinese (Traditional, Hong Kong) + "zh_TW" => "臺灣華語" # Chinese (Traditional, Taiwan) +] + FileUtils::rm_rf "#{qt_gui_path}/help/" FileUtils::mkdir "#{qt_gui_path}/help/" @@ -216,11 +273,11 @@ map { |p| File.basename(p).gsub(/sonic-pi-tutorial-(.*?).po/, '\1') }. sort_by {|n| -n.length} -docs << "\n QString systemLocale = QLocale::system().uiLanguages()[0];\n\n" unless languages.empty? +docs << "\n" # first, try to match all non-default languages (those that aren't "en") languages.each do |lang| - docs << "if (systemLocale.startsWith(\"#{lang}\")) {\n" + docs << "if (this->ui_language.startsWith(\"#{lang}\")) {\n" make_tutorial.call(lang) docs << "} else " end @@ -263,6 +320,41 @@ docs << " autocomplete->addSynthArgs(\":#{k}\", fxtmp);\n\n" end +def generate_ui_lang_names + # Define the language list map ----- + ui_languages = @lang_names.keys + ui_languages = ui_languages.sort_by {|l| l.downcase} + locale_arrays = [] + locale_arrays << "std::map SonicPii18n::native_language_names = {\n" + + # # Add each language + for i in 0..(ui_languages.length()-1) do + lang = ui_languages[i] + locale_arrays << ",\n" if i != 0 + locale_arrays << "{\"#{lang}\", \"#{@lang_names[lang]}\"}" + end + + # End the map + locale_arrays << "\n};\n" + + # Write the map to lang_list.h + content = File.readlines("#{qt_gui_path}/utils/lang_list.tmpl") + lang_names_generated = content.take_while { |line| !line.start_with?("// AUTO-GENERATED")} + lang_names_generated << "// AUTO-GENERATED HEADER FILE\n" + lang_names_generated << "// Do not add any code to this file\n" + lang_names_generated << "// as it will be removed/overwritten\n" + lang_names_generated << "\n" + lang_names_generated << "#ifndef LANG_LIST_H\n" + lang_names_generated << "#define LANG_LIST_H\n" + lang_names_generated << "#include \n" + lang_names_generated << locale_arrays.join() + lang_names_generated << "#endif\n" + + File.open("#{qt_gui_path}/utils/lang_list.h", 'w') do |f| + f << lang_names_generated.join() + end +end + # update ruby_help.h if options[:output_name] then @@ -321,3 +413,5 @@ end end + +generate_ui_lang_names() diff --git a/app/server/ruby/lib/sonicpi/synths/synthinfo.rb b/app/server/ruby/lib/sonicpi/synths/synthinfo.rb index 650200152c..162c2ab218 100644 --- a/app/server/ruby/lib/sonicpi/synths/synthinfo.rb +++ b/app/server/ruby/lib/sonicpi/synths/synthinfo.rb @@ -3290,6 +3290,214 @@ def arg_defaults end end + class WinwoodLead < SonicPiSynth + def name + "Winwood Lead" + end + + def introduced + Version.new(3,3,0) + end + + def synth_name + "winwood_lead" + end + + def on_start(studio, args_h) + args_h[:rand_buf] = studio.rand_buf_id + end + + def doc + "A lead synth inspired by the Winwood songs from the early 80s. Adapted for Sonic Pi from [Steal This Sound](https://raw.githubusercontent.com/supercollider/supercollider/develop/examples/demonstrations/stealthissound.scd). Published there under [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html), so re-published under the same terms. The source code is available [on the Sonic Pi GitHub repository](https://github.com/sonic-pi-net/sonic-pi). Date of modification: 10.01.2021" + end + + def arg_defaults + { + :note => 69, + :note_slide => 0, + :note_slide_shape => 1, + :note_slide_curve => 0, + :amp => 1, + :amp_slide => 0, + :amp_slide_shape => 1, + :amp_slide_curve => 0, + :pan => 0, + :pan_slide => 0, + :pan_slide_shape => 1, + :pan_slide_curve => 0, + + :attack => 0.01, + :decay => 0, + :sustain => 0.9, + :release => 0.05, + :attack_level => 1, + :decay_level => 0.5, + :sustain_level => 0.5, + + :cutoff => 119, + :cutoff_slide => 0, + :cutoff_slide_shape => 1, + :cutoff_slide_curve => 0, + + :res => 0.8, + :res_slide => 0, + :res_slide_shape => 1, + :res_slide_curve => 0, + :lfo_width => 0.01, + :lfo_width_slide => 0, + :lfo_width_slide_shape => 1, + :lfo_width_slide_curve => 0, + :lfo_rate => 8, + :lfo_rate_slide => 0, + :lfo_rate_slide_shape => 1, + :lfo_rate_slide_curve => 0, + :seed => 0, + } + end + + def specific_arg_info + { + :seed => + { + :doc => "Seed value for rand num generator used for the phase offset of the triangle low-frequency oscillator (LFO)", + :modulatable => false + }, + :lfo_width => + { + :doc => "Width of the low-frequency oscillator (LFO) which determines how wide base tones oscillate around their base frequencies", + :modulatable => true + }, + :lfo_rate => + { + :doc => "Width of the low-frequency oscillator (LFO) which determines how fast base tones oscillate around their base frequencies", + :modulatable => true + }, + } + end + end + + class BassFoundation < SonicPiSynth + def name + "Bass Foundation" + end + + def introduced + Version.new(3,3,0) + end + + def synth_name + "bass_foundation" + end + + def doc + "A soft bass synth inspired by the sounds of the 80s. Use together with :bass_highend if you want to give it a gargling component. Adapted for Sonic Pi from [Steal This Sound](https://raw.githubusercontent.com/supercollider/supercollider/develop/examples/demonstrations/stealthissound.scd). Published there under [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html), so re-published under the same terms. The source code is available [on the Sonic Pi GitHub repository](https://github.com/sonic-pi-net/sonic-pi). Date of modification: 10.01.2021" + end + + def arg_defaults + { + :note => 40, + :note_slide => 0, + :note_slide_shape => 1, + :note_slide_curve => 0, + :amp => 1, + :amp_slide => 0, + :amp_slide_shape => 1, + :amp_slide_curve => 0, + :pan => 0, + :pan_slide => 0, + :pan_slide_shape => 1, + :pan_slide_curve => 0, + + :attack => 0.01, + :decay => 0, + :sustain => 0.9, + :release => 0.05, + :attack_level => 1, + :decay_level => 0.5, + :sustain_level => 0, + + :cutoff => 83, + :cutoff_slide => 0, + :cutoff_slide_shape => 1, + :cutoff_slide_curve => 0, + + :res => 0.5, + :res_slide => 0, + :res_slide_shape => 1, + :res_slide_curve => 0, + } + end + end + + class BassHighend < SonicPiSynth + def name + "Bass Highend" + end + + def introduced + Version.new(3,3,0) + end + + def synth_name + "bass_highend" + end + + def doc + "An addition to the :bass_foundation synth inspired by the sounds of the 80s. Use them together if you want to give it a rough, slurping, or gargling component. Adapted for Sonic Pi from [Steal This Sound](https://raw.githubusercontent.com/supercollider/supercollider/develop/examples/demonstrations/stealthissound.scd). Published there under [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html), so re-published under the same terms. The source code is available [on the Sonic Pi GitHub repository](https://github.com/sonic-pi-net/sonic-pi). Date of modification: 11.01.2021" + end + + def arg_defaults + { + :note => 40, + :note_slide => 0, + :note_slide_shape => 1, + :note_slide_curve => 0, + :amp => 1, + :amp_slide => 0, + :amp_slide_shape => 1, + :amp_slide_curve => 0, + :pan => 0, + :pan_slide => 0, + :pan_slide_shape => 1, + :pan_slide_curve => 0, + + :attack => 0.01, + :decay => 0, + :sustain => 0.9, + :release => 0.05, + :attack_level => 1, + :decay_level => 0.5, + :sustain_level => 0, + + :cutoff => 102, + :cutoff_slide => 0, + :cutoff_slide_shape => 1, + :cutoff_slide_curve => 0, + + :res => 0.1, + :res_slide => 0, + :res_slide_shape => 1, + :res_slide_curve => 0, + + :drive => 2.0, + :drive_slide => 0, + :drive_slide_shape => 1, + :drive_slide_curve => 0, + } + end + + def specific_arg_info + { + :drive => + { + :doc => "Higher drive values make the sound louder and rougher.", + :validations => [v_positive(:drive)], + :modulatable => true + }, + } + end + end + class StudioInfo < SonicPiSynth def user_facing? false @@ -7949,6 +8157,9 @@ class BaseInfo :kalimba => SynthKalimba.new, :pluck => SynthPluck.new, :tech_saws => TechSaws.new, + :winwood_lead => WinwoodLead.new, + :bass_foundation => BassFoundation.new, + :bass_highend => BassHighend.new, :sound_in => SoundIn.new, :sound_in_stereo => SoundInStereo.new, diff --git a/etc/synthdefs/compiled/sonic-pi-bass_foundation.scsyndef b/etc/synthdefs/compiled/sonic-pi-bass_foundation.scsyndef new file mode 100644 index 0000000000..e4cef4d6ba Binary files /dev/null and b/etc/synthdefs/compiled/sonic-pi-bass_foundation.scsyndef differ diff --git a/etc/synthdefs/compiled/sonic-pi-bass_highend.scsyndef b/etc/synthdefs/compiled/sonic-pi-bass_highend.scsyndef new file mode 100644 index 0000000000..633c0d9ed3 Binary files /dev/null and b/etc/synthdefs/compiled/sonic-pi-bass_highend.scsyndef differ diff --git a/etc/synthdefs/compiled/sonic-pi-winwood_lead.scsyndef b/etc/synthdefs/compiled/sonic-pi-winwood_lead.scsyndef new file mode 100644 index 0000000000..17b50e0f78 Binary files /dev/null and b/etc/synthdefs/compiled/sonic-pi-winwood_lead.scsyndef differ diff --git a/etc/synthdefs/designs/supercollider/bass_foundation.scd b/etc/synthdefs/designs/supercollider/bass_foundation.scd new file mode 100644 index 0000000000..8effc5e139 --- /dev/null +++ b/etc/synthdefs/designs/supercollider/bass_foundation.scd @@ -0,0 +1,46 @@ +// Adapted for Sonic Pi from +// https://raw.githubusercontent.com/supercollider/supercollider/develop/examples/demonstrations/stealthissound.scd +// Published there under GPL v3, so re-published under the same terms, see: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// Date of modification: 10.01.2021 + +( +SynthDef('sonic-pi-bass_foundation', {| + note = 40, note_slide = 0, note_slide_shape = 1, note_slide_curve = 0, + amp = 1, amp_slide = 0, amp_slide_shape = 1, amp_slide_curve = 0, + pan = 0, pan_slide = 0, pan_slide_shape = 1, pan_slide_curve = 0, + attack = 0.01, decay = 0, sustain = 0.9, release = 0.05, + attack_level = 1, decay_level = 0.5, sustain_level = 0, + cutoff = 83, cutoff_slide = 0, cutoff_slide_shape = 1, cutoff_slide_curve = 0, + res = 0.5, res_slide = 0, res_slide_shape = 1, res_slide_curve = 0, + out_bus = 0| + + var snd, osc, env, filterenv; + + note = note.midicps; + note = note.varlag(note_slide, note_slide_curve, note_slide_shape); + decay_level = Select.kr(decay_level < 0, [decay_level, sustain_level]); + amp = amp.varlag(amp_slide, amp_slide_curve, amp_slide_shape); + pan = pan.varlag(pan_slide, pan_slide_curve, pan_slide_shape); + + cutoff = cutoff.midicps; + cutoff = cutoff.varlag(cutoff_slide, cutoff_slide_curve, cutoff_slide_shape); + + res = res.varlag(res_slide, res_slide_curve, res_slide_shape); + + osc = Saw.ar(note); + + filterenv = EnvGen.ar(Env.adsr(0.0, 0.5, 0.2, 0.2), 1, doneAction:2); + snd = RLPF.ar(osc,cutoff*filterenv+100, res); + + env = Env.new( + [0, attack_level, decay_level, sustain_level, 0], + [attack,decay,sustain,release], + \lin + ); + + snd = Pan2.ar(Mix(snd) * EnvGen.kr(env, doneAction: 2), pan); + + Out.ar(out_bus, snd * amp); +}).writeDefFile("/Users/sam/Development/RPi/sonic-pi/etc/synthdefs/compiled/"); +) \ No newline at end of file diff --git a/etc/synthdefs/designs/supercollider/bass_highend.scd b/etc/synthdefs/designs/supercollider/bass_highend.scd new file mode 100644 index 0000000000..a73c8421d1 --- /dev/null +++ b/etc/synthdefs/designs/supercollider/bass_highend.scd @@ -0,0 +1,58 @@ +// Adapted for Sonic Pi from +// https://raw.githubusercontent.com/supercollider/supercollider/develop/examples/demonstrations/stealthissound.scd +// Published there under GPL v3, so re-published under the same terms, see: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// Date of modification: 11.01.2021 + +( +SynthDef('sonic-pi-bass_highend',{| + note = 40, note_slide = 0, note_slide_shape = 1, note_slide_curve = 0, + amp = 1, amp_slide = 0, amp_slide_shape = 1, amp_slide_curve = 0, + pan = 0, pan_slide = 0, pan_slide_shape = 1, pan_slide_curve = 0, + attack = 0.01, decay = 0, sustain = 0.9, release = 0.05, + attack_level = 1, decay_level = 0.5, sustain_level = 0, + cutoff = 102, cutoff_slide = 0, cutoff_slide_shape = 1, cutoff_slide_curve = 0, + res = 0.1, res_slide = 0, res_slide_shape = 1, res_slide_curve = 0, + drive = 2.0, drive_slide = 0, drive_slide_shape = 1, drive_slide_curve = 0, + out_bus = 0| + + var osc, snd, env, filterenv, ab; + + note = note.midicps; + note = note.varlag(note_slide, note_slide_curve, note_slide_shape); + decay_level = Select.kr(decay_level < 0, [decay_level, sustain_level]); + amp = amp.varlag(amp_slide, amp_slide_curve, amp_slide_shape); + pan = pan.varlag(pan_slide, pan_slide_curve, pan_slide_shape); + + cutoff = cutoff.midicps; + cutoff = cutoff.varlag(cutoff_slide, cutoff_slide_curve, cutoff_slide_shape); + + res = res.varlag(res_slide, res_slide_curve, res_slide_shape); + + drive = drive.varlag(drive_slide, drive_slide_curve, drive_slide_shape); + + osc = Mix(Saw.ar(note*[0.25,1,1.5],[0.5,0.4,0.1])); + filterenv = EnvGen.ar(Env.adsr(0.0,0.5,0.2,0.2), doneAction:2); + snd = RLPF.ar(osc,cutoff*filterenv+100,res); + + ab = abs(snd); + snd = (snd*(ab + drive)/(snd ** 2 + (drive - 1) * ab + 1)); + + // Remove low end + snd = BLowShelf.ar(snd, 300, 1.0, -12); + + // Dip at 1600Hz + snd = BPeakEQ.ar(snd, 1600, 1.0, -6); + + env = Env.new( + [0, attack_level, decay_level, sustain_level, 0], + [attack,decay,sustain,release], + \lin + ); + + snd = Pan2.ar(Mix(snd) * EnvGen.kr(env, doneAction: 2) * 2, pan); + + Out.ar(out_bus, snd * amp); + +}).writeDefFile("/Users/sam/Development/RPi/sonic-pi/etc/synthdefs/compiled/"); +) \ No newline at end of file diff --git a/etc/synthdefs/designs/supercollider/winwood_lead.scd b/etc/synthdefs/designs/supercollider/winwood_lead.scd new file mode 100644 index 0000000000..8f544ffb22 --- /dev/null +++ b/etc/synthdefs/designs/supercollider/winwood_lead.scd @@ -0,0 +1,58 @@ +// Adapted for Sonic Pi from +// https://raw.githubusercontent.com/supercollider/supercollider/develop/examples/demonstrations/stealthissound.scd +// Published there under GPL v3, so re-published under the same terms, see: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// Date of modification: 10.01.2021 + +( +SynthDef('sonic-pi-winwood_lead', {| + note = 69, note_slide = 0, note_slide_shape = 1, note_slide_curve = 0, + amp = 1, amp_slide = 0, amp_slide_shape = 1, amp_slide_curve = 0, + pan = 0, pan_slide = 0, pan_slide_shape = 1, pan_slide_curve = 0, + attack = 0.01, decay = 0, sustain = 0.9, release = 0.05, + attack_level = 1, decay_level = 0.5, sustain_level = 0.5, + cutoff = 119, cutoff_slide = 0, cutoff_slide_shape = 1, cutoff_slide_curve = 0, + lfo_width = 0.01, lfo_width_slide = 0, lfo_width_slide_shape = 1, lfo_width_slide_curve = 0, + lfo_rate = 8, lfo_rate_slide = 0, lfo_rate_slide_shape = 1, lfo_rate_slide_curve = 0, + res = 0.8, res_slide = 0, res_slide_shape = 1, res_slide_curve = 0, + seed = 0, rand_buf = 0, + out_bus = 0| + + var snd, pulse, env, lfo; + var rand_val; + + note = note.midicps; + note = note.varlag(note_slide, note_slide_curve, note_slide_shape); + decay_level = Select.kr(decay_level < 0, [decay_level, sustain_level]); + amp = amp.varlag(amp_slide, amp_slide_curve, amp_slide_shape); + pan = pan.varlag(pan_slide, pan_slide_curve, pan_slide_shape); + + cutoff = cutoff.midicps; + cutoff = cutoff.varlag(cutoff_slide, cutoff_slide_curve, cutoff_slide_shape); + + lfo_width = lfo_width.varlag(lfo_width_slide, lfo_width_slide_curve, lfo_width_slide_shape); + lfo_rate = lfo_rate.varlag(lfo_rate_slide, lfo_rate_slide_curve, lfo_rate_slide_shape); + + res = res.varlag(res_slide, res_slide_curve, res_slide_shape); + + rand_val = BufRd.kr(1, rand_buf, seed, 1); + + lfo = LFTri.kr(lfo_rate,(rand_val*2.0)!2); + + pulse = Mix(Pulse.ar(note*[1,1.001]*(1.0+(lfo_width*lfo)),[0.2,0.19]))*0.5; + + snd = RLPF.ar(pulse,cutoff,res); + + snd = BLowShelf.ar(snd,351,1.0,-9); + + env = Env.new( + [0, attack_level, decay_level, sustain_level, 0], + [attack,decay,sustain,release], + \lin + ); + + snd = Pan2.ar(Mix(snd) * EnvGen.kr(env, doneAction: 2), pan); + + Out.ar(out_bus, snd * amp); +}).writeDefFile("/Users/sam/Development/RPi/sonic-pi/etc/synthdefs/compiled/"); +) \ No newline at end of file