diff --git a/Metronome.cpp b/Metronome.cpp index e6decff..a2ed32f 100644 --- a/Metronome.cpp +++ b/Metronome.cpp @@ -1,19 +1,30 @@ - -#include #include #include #include #include #include +/// Ableton Link is a technology that synchronizes musical beat, tempo, +/// and phase across multiple applications running on one or more devices. +/// Applications on devices connected to a local network discover each other +/// automatically and form a musical session in which each participant can +/// perform independently: anyone can start or stop while still staying in time. +/// Anyone can change the tempo, the others will follow. +/// Anyone can join or leave without disrupting the session. +/// +/// https://ableton.github.io/link/ +/// #include +#include "Settings.h" #include "Metronome.h" namespace ableton { + /// Inspired from Dummy audio platform example + /// https://github.com/Ableton/link/blob/master/examples/linkaudio/AudioPlatform_Dummy.hpp class Engine { public: @@ -48,11 +59,24 @@ namespace ableton return sessionState.beatAtTime(now(), mQuantum); } - void setTempo(double tempo) + double phaseTime() const + { + auto sessionState = mLink.captureAppSessionState(); + return sessionState.phaseAtTime(now(), mQuantum); + } + + double tempo() const + { + auto sessionState = mLink.captureAppSessionState(); + return sessionState.tempo(); + } + + double setTempo(double tempo) { auto sessionState = mLink.captureAppSessionState(); sessionState.setTempo(tempo, now()); mLink.commitAppSessionState(sessionState); + return sessionState.tempo(); } double quantum() const @@ -86,40 +110,11 @@ namespace ableton }; - struct State - { - std::atomic running; - Link link; - Engine engine; - double beats, phase, tempo; - State() : running(true), link(120.), engine(link), beats(0.), phase(4.), tempo(120.) - { - } - }; - - void update(State& state) - { - state.link.enable(true); - - while (state.running) - { - const auto time = state.link.clock().micros(); - auto sessionState = state.link.captureAppSessionState(); - - state.beats = sessionState.beatAtTime(time, state.engine.quantum()); - state.phase = sessionState.phaseAtTime(time, state.engine.quantum()); - state.tempo = sessionState.tempo(); - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - state.link.enable(false); - } - } // namespace ableton -ableton::State link_state_; +ableton::Link link_(120.); +ableton::Engine engine_(link_); Metronome::Metronome() { @@ -128,28 +123,65 @@ Metronome::Metronome() bool Metronome::init() { - std::thread(ableton::update, std::ref(link_state_)).detach(); + // connect + link_.enable(true); - return link_state_.running; + // enable sync + link_.enableStartStopSync(true); + + // set parameters + setTempo(Settings::application.metronome.tempo); + setQuantum(Settings::application.metronome.quantum); + + // no reason for failure? + return true; } void Metronome::terminate() { - link_state_.running = false; + // save current tempo + Settings::application.metronome.tempo = tempo(); + + // disconnect + link_.enable(false); } double Metronome::beats() const { - return link_state_.beats; + return engine_.beatTime(); } double Metronome::phase() const { - return link_state_.phase; + return engine_.phaseTime(); +} + +void Metronome::setQuantum(double q) +{ + engine_.setQuantum(q); + Settings::application.metronome.quantum = engine_.quantum(); +} + +double Metronome::quantum() const +{ + return engine_.quantum(); +} + +void Metronome::setTempo(double t) +{ + // set the tempo to t + // OR + // adopt the last tempo value that have been proposed on the network + Settings::application.metronome.tempo = engine_.setTempo(t); } double Metronome::tempo() const { - return link_state_.tempo; + return engine_.tempo(); +} + +size_t Metronome::peers() const +{ + return link_.numPeers(); } diff --git a/Metronome.h b/Metronome.h index 0b8b766..8fa2831 100644 --- a/Metronome.h +++ b/Metronome.h @@ -1,6 +1,7 @@ #ifndef METRONOME_H #define METRONOME_H +#include class Metronome { @@ -21,7 +22,16 @@ public: bool init (); void terminate(); + double beats() const; + double phase() const; + void setTempo(double t); + double tempo() const; + + void setQuantum(double q); + double quantum() const; + + size_t peers() const; }; #endif // METRONOME_H diff --git a/Settings.cpp b/Settings.cpp index 7ec756c..0002ff5 100644 --- a/Settings.cpp +++ b/Settings.cpp @@ -249,6 +249,12 @@ void Settings::Save() pRoot->InsertEndChild(recent); } + // Metronome + XMLElement *metroConfNode = xmlDoc.NewElement( "Metronome" ); + metroConfNode->SetAttribute("tempo", application.metronome.tempo); + metroConfNode->SetAttribute("quantum", application.metronome.quantum); + pRoot->InsertEndChild(metroConfNode); + // First save : create filename if (settingsFilename.empty()) settingsFilename = SystemToolkit::full_filename(SystemToolkit::settings_path(), APP_SETTINGS); @@ -518,6 +524,13 @@ void Settings::Load() } } + // bloc metronome + XMLElement * metroconfnode = pRoot->FirstChildElement("Metronome"); + if (metroconfnode != nullptr) { + metroconfnode->QueryDoubleAttribute("tempo", &application.metronome.tempo); + metroconfnode->QueryDoubleAttribute("quantum", &application.metronome.quantum); + } + } void Settings::History::push(const string &filename) diff --git a/Settings.h b/Settings.h index 2c2245d..1f1926c 100644 --- a/Settings.h +++ b/Settings.h @@ -168,6 +168,16 @@ struct SourceConfig } }; +struct MetronomeConfig +{ + double tempo; + double quantum; + + MetronomeConfig() { + tempo = 120.; + quantum = 4.; + } +}; struct Application { @@ -224,6 +234,9 @@ struct Application History recentImport; std::map< std::string, std::string > dialogRecentFolder; + // Metronome + MetronomeConfig metronome; + Application() : fresh_start(false), instance_id(0), name(APP_NAME), executable(APP_NAME) { scale = 1.f; accent_color = 0; diff --git a/UserInterfaceManager.cpp b/UserInterfaceManager.cpp index 0923ccd..badb0dc 100644 --- a/UserInterfaceManager.cpp +++ b/UserInterfaceManager.cpp @@ -83,6 +83,7 @@ using namespace std; #include "PickingVisitor.h" #include "ImageShader.h" #include "ImageProcessingShader.h" +#include "Metronome.h" #include "TextEditor.h" TextEditor editor; @@ -1589,14 +1590,55 @@ void UserInterface::RenderMetrics(bool *p_open, int* p_corner, int *p_mode) ImGui::Combo("##mode", p_mode, ICON_FA_TACHOMETER_ALT " Performance\0" ICON_FA_HOURGLASS_HALF " Timers\0" - ICON_FA_VECTOR_SQUARE " Source\0"); + ICON_FA_VECTOR_SQUARE " Source\0" + ICON_FA_USER_CLOCK " Ableton link\0"); ImGui::SameLine(); if (ImGuiToolkit::IconButton(5,8)) ImGui::OpenPopup("metrics_menu"); ImGui::Spacing(); - if (*p_mode > 1) { + if (*p_mode > 2) { + // get values + double t = Metronome::manager().tempo(); + double p = Metronome::manager().phase(); + double q = Metronome::manager().quantum(); + int n = (int) Metronome::manager().peers(); + + // tempo + char buf[32]; + ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO); + ImGui::Text("Tempo %.1f BPM ", t); + + // network peers indicator + ImGui::SameLine(); + if ( n < 1) { + ImGui::TextDisabled( ICON_FA_NETWORK_WIRED ); + if (ImGui::IsItemHovered()){ + sprintf(buf, "No peer linked"); + ImGuiToolkit::ToolTip(buf); + } + } + else { + ImGui::Text( ICON_FA_NETWORK_WIRED ); + if (ImGui::IsItemHovered()){ + sprintf(buf, "%d peer%c linked", n, n < 2 ? ' ' : 's'); + ImGuiToolkit::ToolTip(buf); + } + } + + // compute and display duration of a phase + double duration = 60.0 / t * q; + guint64 time_phase = GST_SECOND * duration ; + ImGui::Text("Phase %s", GstToolkit::time_to_string(time_phase, GstToolkit::TIME_STRING_READABLE).c_str()); + ImGui::PopFont(); + + // metronome + sprintf(buf, "%d/%d", (int)(p)+1, (int)(q) ); + ImGui::ProgressBar(ceil(p)/ceil(q), ImVec2(250.f,0.f), buf); + + } + else if (*p_mode > 1) { ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO); Source *s = Mixer::manager().currentSource(); if (s) { @@ -4693,7 +4735,9 @@ void Navigator::RenderMainPannelSettings() ImGui::PopFont(); ImGui::SetCursorPosY(width_); + // // Appearance + // ImGui::Text("Appearance"); ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); if ( ImGui::DragFloat("Scale", &Settings::application.scale, 0.01, 0.5f, 2.0f, "%.1f")) @@ -4704,12 +4748,48 @@ void Navigator::RenderMainPannelSettings() if (b || o || g) ImGuiToolkit::SetAccentColor(static_cast(Settings::application.accent_color)); + // // Options + // ImGuiToolkit::Spacing(); ImGui::Text("Options"); ImGuiToolkit::ButtonSwitch( ICON_FA_MOUSE_POINTER " Smooth cursor", &Settings::application.smooth_cursor); ImGuiToolkit::ButtonSwitch( ICON_FA_TACHOMETER_ALT " Metrics", &Settings::application.widget.stats); + // + // Metronome + // + ImGuiToolkit::Spacing(); + ImGui::Text("Ableton Link"); + + ImGuiToolkit::HelpMarker("Ableton link enables time synchronization\n " + ICON_FA_ANGLE_RIGHT " Tempo is the number of beats per minute (or set by peers).\n " + ICON_FA_ANGLE_RIGHT " Quantum is the number of beats in a phase."); + ImGui::SameLine(0); + ImGui::SetCursorPosX(-1.f * IMGUI_RIGHT_ALIGN); + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + int t = (int) Metronome::manager().tempo(); + // if no other peers, user can set a tempo + if (Metronome::manager().peers() < 1) { + if ( ImGui::SliderInt("Tempo", &t, 20, 240, "%d BPM") ) + Metronome::manager().setTempo((double) t); + } + // if there are other peers, tempo cannot be changed + else { + // show static info (same size than slider) + static char dummy_str[64]; + sprintf(dummy_str, "%d BPM", t); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f, 0.14f, 0.14f, 0.9f)); + ImGui::InputText("Tempo", dummy_str, IM_ARRAYSIZE(dummy_str), ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleColor(1); + } + + ImGui::SetCursorPosX(-1.f * IMGUI_RIGHT_ALIGN); + ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); + int q = (int) Metronome::manager().quantum(); + if ( ImGui::SliderInt("Quantum", &q, 2, 100) ) + Metronome::manager().setQuantum((double) q); + #ifndef NDEBUG ImGui::Text("Expert"); // ImGuiToolkit::ButtonSwitch( IMGUI_TITLE_HISTORY, &Settings::application.widget.history); @@ -4717,7 +4797,9 @@ void Navigator::RenderMainPannelSettings() ImGuiToolkit::ButtonSwitch( IMGUI_TITLE_TOOLBOX, &Settings::application.widget.toolbox, CTRL_MOD "T"); ImGuiToolkit::ButtonSwitch( IMGUI_TITLE_LOGS, &Settings::application.widget.logs, CTRL_MOD "L"); #endif + // // Recording preferences + // ImGuiToolkit::Spacing(); ImGui::Text("Recording"); @@ -4754,7 +4836,9 @@ void Navigator::RenderMainPannelSettings() ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN); ImGui::Combo("Priority", &Settings::application.record.priority_mode, "Clock\0Framerate\0"); - // system preferences + // + // System preferences + // ImGuiToolkit::Spacing(); ImGui::Text("System"); ImGui::SameLine( ImGui::GetContentRegionAvailWidth() IMGUI_RIGHT_ALIGN * 0.8);