From 4d12bbfb21b556b2c1a6f364df5dc33ed0d52a5a Mon Sep 17 00:00:00 2001 From: Vincent Wong Date: Sun, 21 Oct 2018 00:42:33 -0700 Subject: [PATCH] Audio/Timing: implement tap-to-time Tap-to-time provides the user the ability to tap to the lyrics/syllables of the song in order to time lines or karaoke. It consists of these extra UI interactions: - **Indicator**: tap marker: a designated marker that can be moved to the current audio position; indicated in: - the audio display by a green arrow underneath a marker - the karaoke display by a green-colored syllable - **Control**: tap marker: the tap marker can be changed by selecting syllables on audio display in karaoke mode, or clicking the markers on audio display in dialogue mode - **Control**: ctrl-right-click audio display: starts playing the audio from that exact position until the end of the file - **Option**: Timing/Tap To Time: enables the tap marker indicator and commands - **Button**: time_opt_tap_to_time: toggles the Timing/Tap To Time option - **Button**: time_tap_connect (hotkey I): a command that: - moves the tap marker's position to the current playing audio position - sets the next marker to be the tap marker - if the tap marker is already the last marker AND BOTH autocommit AND next-line-on-commit is ON, will move onto the next line - if moved on to the next line, also sets the start marker to the current audio position, so the two lines are connected, and moves to the next tap marker (essentially reinvoking time_tap_connect once) - **Button**: time_tap_no_connect (hotkey O): similar to time_tap_connect, except it will not set the next line's start position even if moved to the next line Expected workflow: 1) User loads song lyrics 2) User splits each line into syllables 3) User turns on tap-to-time, autocommit, and next-line-on-commit 4) User plays audio from beginning, tapping time_tap_connect to each syllable, occasionally tapping time_tap_no_connect when a break between lines is desired 5) If user messes up a line, they can set the tap marker to where they want to restart from, and ctrl-right-click to start the audio a few seconds before it 6) Syllables can be split/merged at will, and adjustments to timing can be done using normal karaoke timing controls --- src/audio_display.cpp | 23 +++ src/audio_display.h | 6 + src/audio_karaoke.cpp | 32 +++- src/audio_karaoke.h | 5 + src/audio_timing.h | 19 ++ src/audio_timing_dialogue.cpp | 90 ++++++++- src/audio_timing_karaoke.cpp | 181 ++++++++++++++++-- .../button/time_opt_tap_to_time_16.png | Bin 0 -> 1594 bytes .../button/time_opt_tap_to_time_24.png | Bin 0 -> 2635 bytes .../button/time_opt_tap_to_time_32.png | Bin 0 -> 3616 bytes .../button/time_opt_tap_to_time_48.png | Bin 0 -> 5703 bytes .../button/time_opt_tap_to_time_64.png | Bin 0 -> 8397 bytes src/bitmaps/button/time_tap_connect_16.png | Bin 0 -> 1036 bytes src/bitmaps/button/time_tap_connect_24.png | Bin 0 -> 1293 bytes src/bitmaps/button/time_tap_connect_32.png | Bin 0 -> 1482 bytes src/bitmaps/button/time_tap_connect_48.png | Bin 0 -> 1997 bytes src/bitmaps/button/time_tap_connect_64.png | Bin 0 -> 3527 bytes src/bitmaps/button/time_tap_no_connect_16.png | Bin 0 -> 859 bytes src/bitmaps/button/time_tap_no_connect_24.png | Bin 0 -> 1026 bytes src/bitmaps/button/time_tap_no_connect_32.png | Bin 0 -> 1152 bytes src/bitmaps/button/time_tap_no_connect_48.png | Bin 0 -> 1571 bytes src/bitmaps/button/time_tap_no_connect_64.png | Bin 0 -> 3464 bytes src/bitmaps/manifest.respack | 15 ++ src/command/time.cpp | 95 +++++++++ src/hotkey.cpp | 11 ++ src/libresrc/default_config.json | 3 +- src/libresrc/default_hotkey.json | 6 + src/libresrc/default_toolbar.json | 4 + src/libresrc/osx/default_config.json | 3 +- 29 files changed, 467 insertions(+), 26 deletions(-) create mode 100644 src/bitmaps/button/time_opt_tap_to_time_16.png create mode 100644 src/bitmaps/button/time_opt_tap_to_time_24.png create mode 100644 src/bitmaps/button/time_opt_tap_to_time_32.png create mode 100644 src/bitmaps/button/time_opt_tap_to_time_48.png create mode 100644 src/bitmaps/button/time_opt_tap_to_time_64.png create mode 100644 src/bitmaps/button/time_tap_connect_16.png create mode 100644 src/bitmaps/button/time_tap_connect_24.png create mode 100644 src/bitmaps/button/time_tap_connect_32.png create mode 100644 src/bitmaps/button/time_tap_connect_48.png create mode 100644 src/bitmaps/button/time_tap_connect_64.png create mode 100644 src/bitmaps/button/time_tap_no_connect_16.png create mode 100644 src/bitmaps/button/time_tap_no_connect_24.png create mode 100644 src/bitmaps/button/time_tap_no_connect_32.png create mode 100644 src/bitmaps/button/time_tap_no_connect_48.png create mode 100644 src/bitmaps/button/time_tap_no_connect_64.png diff --git a/src/audio_display.cpp b/src/audio_display.cpp index 2756ac3ff..3bf67e720 100644 --- a/src/audio_display.cpp +++ b/src/audio_display.cpp @@ -900,6 +900,14 @@ void AudioDisplay::PaintMarkers(wxDC &dc, TimeRange updtime) if (marker->GetFeet() & AudioMarker::Feet_Right) PaintFoot(dc, marker_x, 1); } + + + if (OPT_GET("Timing/Tap To Time")->GetBool()) { + dc.SetBrush(wxBrush(*wxGREEN)); + dc.SetPen(*wxTRANSPARENT_PEN); + int marker_x = RelativeXFromTime(controller->GetTimingController()->GetTapMarkerPosition()); + PaintTapMarker(dc, marker_x); + } } void AudioDisplay::PaintFoot(wxDC &dc, int marker_x, int dir) @@ -910,6 +918,12 @@ void AudioDisplay::PaintFoot(wxDC &dc, int marker_x, int dir) dc.DrawPolygon(3, foot_bot, marker_x, audio_top+audio_height); } +void AudioDisplay::PaintTapMarker(wxDC &dc, int marker_x) +{ + wxPoint arrow[3] = { wxPoint(-foot_size * 2, 0), wxPoint(0, -foot_size * 2), wxPoint(foot_size * 2, 0) }; + dc.DrawPolygon(3, arrow, marker_x, audio_top+audio_height); +} + void AudioDisplay::PaintLabels(wxDC &dc, TimeRange updtime) { std::vector labels; @@ -1239,6 +1253,7 @@ void AudioDisplay::OnAudioOpen(agi::AudioProvider *provider) OPT_SUB("Colour/Audio Display/Waveform", &AudioDisplay::ReloadRenderingSettings, this), OPT_SUB("Audio/Renderer/Spectrum/Quality", &AudioDisplay::ReloadRenderingSettings, this), OPT_SUB("Audio/Renderer/Spectrum/FreqCurve", &AudioDisplay::ReloadRenderingSettings, this), + OPT_SUB("Timing/Tap To Time", &AudioDisplay::OnTapMarkerChanged, this), }); OnTimingController(); } @@ -1264,10 +1279,12 @@ void AudioDisplay::OnTimingController() timing_controller->AddMarkerMovedListener(&AudioDisplay::OnMarkerMoved, this); timing_controller->AddUpdatedPrimaryRangeListener(&AudioDisplay::OnSelectionChanged, this); timing_controller->AddUpdatedStyleRangesListener(&AudioDisplay::OnStyleRangesChanged, this); + timing_controller->AddUpdatedTapMarkerListener(&AudioDisplay::OnTapMarkerChanged, this); OnStyleRangesChanged(); OnMarkerMoved(); OnSelectionChanged(); + OnTapMarkerChanged(); } } @@ -1351,6 +1368,12 @@ void AudioDisplay::OnStyleRangesChanged() RefreshRect(wxRect(0, audio_top, GetClientSize().GetWidth(), audio_height), false); } +void AudioDisplay::OnTapMarkerChanged() +{ + RefreshRect(wxRect(0, audio_top, GetClientSize().GetWidth(), audio_height), false); +} + + void AudioDisplay::OnMarkerMoved() { RefreshRect(wxRect(0, audio_top, GetClientSize().GetWidth(), audio_height), false); diff --git a/src/audio_display.h b/src/audio_display.h index 4c8e26a5f..272472d86 100644 --- a/src/audio_display.h +++ b/src/audio_display.h @@ -169,6 +169,11 @@ class AudioDisplay: public wxWindow { /// @param dir -1 for left, 1 for right void PaintFoot(wxDC &dc, int marker_x, int dir); + /// Draw an indicator for the tap marker + /// @param dc DC to paint to + /// @param marker_x Position of the tap marker + void PaintTapMarker(wxDC &dc, int marker_x); + /// Paint the labels in a time range /// @param dc DC to paint to /// @param updtime Time range to repaint @@ -205,6 +210,7 @@ class AudioDisplay: public wxWindow { void OnStyleRangesChanged(); void OnTimingController(); void OnMarkerMoved(); + void OnTapMarkerChanged(); public: AudioDisplay(wxWindow *parent, AudioController *controller, agi::Context *context); diff --git a/src/audio_karaoke.cpp b/src/audio_karaoke.cpp index 2abeb79d8..4643d500c 100644 --- a/src/audio_karaoke.cpp +++ b/src/audio_karaoke.cpp @@ -64,6 +64,7 @@ AudioKaraoke::AudioKaraoke(wxWindow *parent, agi::Context *c) , file_changed(c->ass->AddCommitListener(&AudioKaraoke::OnFileChanged, this)) , audio_opened(c->project->AddAudioProviderListener(&AudioKaraoke::OnAudioOpened, this)) , active_line_changed(c->selectionController->AddActiveLineListener(&AudioKaraoke::OnActiveLineChanged, this)) +, tap_to_time_toggled(OPT_SUB("Timing/Tap To Time", &AudioKaraoke::OnTapMarkerChanged, this)) , kara(agi::make_unique()) { using std::bind; @@ -134,6 +135,7 @@ void AudioKaraoke::SetEnabled(bool en) { if (enabled) { LoadFromLine(); c->audioController->SetTimingController(CreateKaraokeTimingController(c, kara.get(), file_changed)); + c->audioController->GetTimingController()->AddUpdatedTapMarkerListener(&AudioKaraoke::OnTapMarkerChanged, this); Refresh(false); } else { @@ -218,7 +220,15 @@ void AudioKaraoke::RenderText() { // Draw each character in the line int y = (bmp_size.GetHeight() - char_height) / 2; - for (size_t i = 0; i < spaced_text.size(); ++i) + for (size_t i = 0; i < spaced_text.size(); ++i) { + if (!(marked_syl_start <= i && i < marked_syl_end)) { + dc.DrawText(spaced_text[i], char_x[i], y); + } + } + + // Draw marked syllable + dc.SetTextForeground(*wxGREEN); + for (size_t i = marked_syl_start; i < marked_syl_end; ++i) dc.DrawText(spaced_text[i], char_x[i], y); // Draw the lines between each syllable @@ -332,6 +342,26 @@ void AudioKaraoke::OnScrollTimer(wxTimerEvent&) { OnScrollTimer(); } +void AudioKaraoke::OnTapMarkerChanged() { + marked_syl_start = 0; + marked_syl_end = 0; + + if (OPT_GET("Timing/Tap To Time")->GetBool() && kara->size() > 0) { + const AudioTimingController *tc = c->audioController->GetTimingController(); + const size_t marker_idx = tc->GetTapMarkerIndex(); + if (marker_idx > 0) { + marked_syl_start = syl_start_points[marker_idx - 1]; + marked_syl_end = + (marker_idx < syl_start_points.size() ? + syl_start_points[marker_idx] : + spaced_text.size()); + } + } + + RenderText(); + Refresh(false); +} + void AudioKaraoke::LoadFromLine() { scroll_x = 0; scroll_timer.Stop(); diff --git a/src/audio_karaoke.h b/src/audio_karaoke.h index 520f48682..c9a071d40 100644 --- a/src/audio_karaoke.h +++ b/src/audio_karaoke.h @@ -67,6 +67,7 @@ class AudioKaraoke final : public wxWindow { agi::signal::Connection audio_opened; ///< Audio opened connection agi::signal::Connection audio_closed; ///< Audio closed connection agi::signal::Connection active_line_changed; + agi::signal::Connection tap_to_time_toggled; /// Currently active dialogue line AssDialogue *active_line = nullptr; @@ -105,6 +106,9 @@ class AudioKaraoke final : public wxWindow { wxFont split_font; ///< Font used in the split/join interface + size_t marked_syl_start = 0; + size_t marked_syl_end = 0; + bool enabled = false; ///< Is karaoke mode enabled? wxButton *accept_button; ///< Accept pending splits button @@ -144,6 +148,7 @@ class AudioKaraoke final : public wxWindow { void OnAudioOpened(agi::AudioProvider *provider); void OnScrollTimer(); void OnScrollTimer(wxTimerEvent& event); + void OnTapMarkerChanged(); public: /// Constructor diff --git a/src/audio_timing.h b/src/audio_timing.h index 4c0ecf9cb..780a6c26a 100644 --- a/src/audio_timing.h +++ b/src/audio_timing.h @@ -57,6 +57,9 @@ protected: /// One or more rendering style ranges have changed in the timing controller. agi::signal::Signal<> AnnounceUpdatedStyleRanges; + /// The tap marker has changed in the timing controller. + agi::signal::Signal<> AnnounceUpdatedTapMarker; + public: /// @brief Get any warning message to show in the audio display /// @return The warning message to show, may be empty if there is none @@ -86,6 +89,12 @@ public: /// @param[out] ranges Rendering ranges will be added to this virtual void GetRenderingStyles(AudioRenderingStyleRanges &ranges) const = 0; + /// @brief Return the position of the tap marker + virtual int GetTapMarkerPosition() const = 0; + + /// @brief Return the index of the tap marker + virtual size_t GetTapMarkerIndex() const = 0; + enum NextMode { /// Advance to the next timing unit, whether it's a line or a sub-part /// of a line such as a karaoke syllable @@ -138,6 +147,15 @@ public: /// @param delta Amount to add in centiseconds virtual void ModifyStart(int delta) = 0; + /// Move tap marker position to given position + /// @param position to move marker to + virtual void MoveTapMarker(int ms) = 0; + + /// Go to next tap marker + /// @return True if moved to the next marker, False if tap marker is already + /// the last marker of the line + virtual bool NextTapMarker() = 0; + /// @brief Determine if a position is close to a draggable marker /// @param ms The time in milliseconds to test /// @param sensitivity Distance in milliseconds to consider markers as nearby @@ -178,6 +196,7 @@ public: DEFINE_SIGNAL_ADDERS(AnnounceUpdatedPrimaryRange, AddUpdatedPrimaryRangeListener) DEFINE_SIGNAL_ADDERS(AnnounceUpdatedStyleRanges, AddUpdatedStyleRangesListener) + DEFINE_SIGNAL_ADDERS(AnnounceUpdatedTapMarker, AddUpdatedTapMarkerListener) }; /// @brief Create a standard dialogue audio timing controller diff --git a/src/audio_timing_dialogue.cpp b/src/audio_timing_dialogue.cpp index 44e9c39b2..dce66b1ac 100644 --- a/src/audio_timing_dialogue.cpp +++ b/src/audio_timing_dialogue.cpp @@ -29,6 +29,7 @@ #include "ass_dialogue.h" #include "ass_file.h" +#include "audio_controller.h" #include "audio_marker.h" #include "audio_rendering_style.h" #include "audio_timing.h" @@ -327,6 +328,12 @@ class AudioTimingControllerDialogue final : public AudioTimingController { /// The time which was clicked on for alt-dragging mode, or INT_MIN if not in alt-draging mode int clicked_ms = INT_MIN; + /// Index of marker serving as tap marker + /// For AudioTimingControllerDialogue: + /// - 0 is left marker + /// - 1 is right marker + size_t tap_marker_idx = 0; + /// Autocommit option const agi::OptionValue *auto_commit = OPT_GET("Audio/Auto/Commit"); const agi::OptionValue *inactive_line_mode = OPT_GET("Audio/Inactive Lines Display Mode"); @@ -384,6 +391,8 @@ class AudioTimingControllerDialogue final : public AudioTimingController { public: // AudioMarkerProvider interface void GetMarkers(const TimeRange &range, AudioMarkerVector &out_markers) const override; + int GetTapMarkerPosition() const override; + size_t GetTapMarkerIndex() const override; // AudioTimingController interface void GetRenderingStyles(AudioRenderingStyleRanges &ranges) const override; @@ -395,9 +404,11 @@ public: void AddLeadOut() override; void ModifyLength(int delta, bool shift_following) override; void ModifyStart(int delta) override; + void MoveTapMarker(int ms) override; + bool NextTapMarker() override; bool IsNearbyMarker(int ms, int sensitivity, bool alt_down) const override; std::vector OnLeftClick(int ms, bool ctrl_down, bool alt_down, int sensitivity, int snap_range) override; - std::vector OnRightClick(int ms, bool, int sensitivity, int snap_range) override; + std::vector OnRightClick(int ms, bool ctrl_down, int sensitivity, int snap_range) override; void OnMarkerDrag(std::vector const& markers, int new_position, int snap_range) override; // We have no warning messages currently, maybe add the old "Modified" message back later? @@ -447,6 +458,23 @@ void AudioTimingControllerDialogue::GetMarkers(const TimeRange &range, AudioMark video_position_provider.GetMarkers(range, out_markers); } +int AudioTimingControllerDialogue::GetTapMarkerPosition() const +{ + assert(tap_marker_idx <= 1); + + if (tap_marker_idx == 0) { + return *active_line.GetLeftMarker(); + } else { + return *active_line.GetRightMarker(); + } +} + +size_t AudioTimingControllerDialogue::GetTapMarkerIndex() const +{ + assert(tap_marker_idx <= 1); + return tap_marker_idx; +} + void AudioTimingControllerDialogue::OnSelectedSetChanged() { RegenerateSelectedLines(); @@ -526,6 +554,7 @@ void AudioTimingControllerDialogue::DoCommit(bool user_triggered) void AudioTimingControllerDialogue::Revert() { commit_id = -1; + tap_marker_idx = 0; if (AssDialogue *line = context->selectionController->GetActiveLine()) { @@ -535,6 +564,7 @@ void AudioTimingControllerDialogue::Revert() AnnounceUpdatedPrimaryRange(); if (inactive_line_mode->GetInt() == 0) AnnounceUpdatedStyleRanges(); + AnnounceUpdatedTapMarker(); } else { @@ -570,6 +600,34 @@ void AudioTimingControllerDialogue::ModifyStart(int delta) { std::min(*m + delta * 10, *active_line.GetRightMarker()), 0); } +void AudioTimingControllerDialogue::MoveTapMarker(int ms) { + // Fix rounding error + ms = (ms + 5) / 10 * 10; + + DialogueTimingMarker *left = active_line.GetLeftMarker(); + DialogueTimingMarker *right = active_line.GetRightMarker(); + + clicked_ms = INT_MIN; + if (tap_marker_idx == 0) { + // Moving left marker (start time of the line) + if (ms > *right) SetMarkers({ right }, ms, 0); + SetMarkers({ left }, ms, 0); + } else { + // Moving right marker (end time of the line) + if (ms < *left) SetMarkers({ left }, ms, 0); + SetMarkers({ right }, ms, 0); + } +} + +bool AudioTimingControllerDialogue::NextTapMarker() { + if (tap_marker_idx == 0) { + tap_marker_idx = 1; + AnnounceUpdatedTapMarker(); + return true; + } + return false; +} + bool AudioTimingControllerDialogue::IsNearbyMarker(int ms, int sensitivity, bool alt_down) const { assert(sensitivity >= 0); @@ -609,6 +667,8 @@ std::vector AudioTimingControllerDialogue::OnLeftClick(int ms, boo ret = drag_timing->GetBool() ? GetRightMarkers() : jump; // Get ret before setting as setting may swap left/right SetMarkers(jump, ms, snap_range); + // Also change tap marker to left marker + tap_marker_idx = 0; return ret; } @@ -627,18 +687,34 @@ std::vector AudioTimingControllerDialogue::OnLeftClick(int ms, boo // Left-click within drag range should still move the left marker to the // clicked position, but not the right marker - if (clicked == left) + if (clicked == left) { SetMarkers(ret, ms, snap_range); + } + + // Also change tap marker + if (clicked == left) { + tap_marker_idx = 0; + } + else { + tap_marker_idx = 1; + } return ret; } -std::vector AudioTimingControllerDialogue::OnRightClick(int ms, bool, int sensitivity, int snap_range) +std::vector AudioTimingControllerDialogue::OnRightClick(int ms, bool ctrl_down, int sensitivity, int snap_range) { - clicked_ms = INT_MIN; - std::vector ret = GetRightMarkers(); - SetMarkers(ret, ms, snap_range); - return ret; + if (ctrl_down) { + context->audioController->PlayToEnd(ms); + return {}; + + } else { + clicked_ms = INT_MIN; + std::vector ret = GetRightMarkers(); + SetMarkers(ret, ms, snap_range); + tap_marker_idx = 1; + return ret; + } } void AudioTimingControllerDialogue::OnMarkerDrag(std::vector const& markers, int new_position, int snap_range) diff --git a/src/audio_timing_karaoke.cpp b/src/audio_timing_karaoke.cpp index 510c272c0..41f0e3b3c 100644 --- a/src/audio_timing_karaoke.cpp +++ b/src/audio_timing_karaoke.cpp @@ -81,6 +81,13 @@ class AudioTimingControllerKaraoke final : public AudioTimingController { size_t cur_syl = 0; ///< Index of currently selected syllable in the line + /// Index of marker serving as tap marker + /// For AudioControllerTimingKaraoke: + /// - 0 is start marker + /// - 1 to markers.size() is a regular syllable marker + /// - markers.size() + 1 is end marker + size_t tap_marker_idx = 0; + /// Pen used for the mid-syllable markers Pen separator_pen{"Colour/Audio Display/Syllable Boundaries", "Audio/Line Boundaries Thickness", wxPENSTYLE_DOT}; /// Pen used for the start-of-line marker @@ -112,11 +119,16 @@ class AudioTimingControllerKaraoke final : public AudioTimingController { void DoCommit(); void ApplyLead(bool announce_primary); int MoveMarker(KaraokeMarker *marker, int new_position); - void AnnounceChanges(int syl); + void MoveStartMarker(int new_position); + void MoveEndMarker(int new_position); + void CompressMarkers(size_t from, size_t to, int new_position); + void AnnounceChanges(bool announce_primary); public: // AudioTimingController implementation void GetMarkers(const TimeRange &range, AudioMarkerVector &out_markers) const override; + int GetTapMarkerPosition() const override; + size_t GetTapMarkerIndex() const override; wxString GetWarningMessage() const override { return ""; } TimeRange GetIdealVisibleTimeRange() const override; void GetRenderingStyles(AudioRenderingStyleRanges &ranges) const override; @@ -131,9 +143,11 @@ public: void AddLeadOut() override; void ModifyLength(int delta, bool shift_following) override; void ModifyStart(int delta) override; + void MoveTapMarker(int ms) override; + bool NextTapMarker() override; bool IsNearbyMarker(int ms, int sensitivity, bool) const override; std::vector OnLeftClick(int ms, bool, bool, int sensitivity, int) override; - std::vector OnRightClick(int ms, bool, int, int) override; + std::vector OnRightClick(int ms, bool ctrl_down, int, int) override; void OnMarkerDrag(std::vector const& marker, int new_position, int) override; AudioTimingControllerKaraoke(agi::Context *c, AssKaraoke *kara, agi::signal::Connection& file_changed); @@ -235,10 +249,29 @@ void AudioTimingControllerKaraoke::GetMarkers(TimeRange const& range, AudioMarke video_position_provider.GetMarkers(range, out); } +int AudioTimingControllerKaraoke::GetTapMarkerPosition() const { + assert(tap_marker_idx <= markers.size() + 1); + + if (tap_marker_idx == 0) { + return start_marker; + } + else if (tap_marker_idx < markers.size() + 1) { + return markers[tap_marker_idx-1]; + } + else { + return end_marker; + } +} + +size_t AudioTimingControllerKaraoke::GetTapMarkerIndex() const { + assert(tap_marker_idx <= markers.size() + 1); + return tap_marker_idx; +} + void AudioTimingControllerKaraoke::DoCommit() { active_line->Text = kara->GetText(); file_changed_slot.Block(); - commit_id = c->ass->Commit(_("karaoke timing"), AssFile::COMMIT_DIAG_TEXT, commit_id, active_line); + commit_id = c->ass->Commit(_("karaoke timing"), AssFile::COMMIT_DIAG_TEXT | AssFile::COMMIT_DIAG_TIME, commit_id, active_line); file_changed_slot.Unblock(); pending_changes = false; } @@ -254,6 +287,7 @@ void AudioTimingControllerKaraoke::Revert() { cur_syl = 0; commit_id = -1; pending_changes = false; + tap_marker_idx = 0; start_marker.Move(active_line->Start); end_marker.Move(active_line->End); @@ -273,6 +307,7 @@ void AudioTimingControllerKaraoke::Revert() { AnnounceUpdatedPrimaryRange(); AnnounceUpdatedStyleRanges(); AnnounceMarkerMoved(); + AnnounceUpdatedTapMarker(); } void AudioTimingControllerKaraoke::AddLeadIn() { @@ -293,7 +328,7 @@ void AudioTimingControllerKaraoke::ApplyLead(bool announce_primary) { kara->SetLineTimes(start_marker, end_marker); if (!announce_primary) AnnounceUpdatedStyleRanges(); - AnnounceChanges(announce_primary ? cur_syl : cur_syl + 2); + AnnounceChanges(announce_primary); } void AudioTimingControllerKaraoke::ModifyLength(int delta, bool shift_following) { @@ -314,13 +349,63 @@ void AudioTimingControllerKaraoke::ModifyLength(int delta, bool shift_following) for (; cur != end; cur += step) { MoveMarker(&markers[cur], markers[cur] + delta * 10); } - AnnounceChanges(cur_syl); + AnnounceChanges(true); } void AudioTimingControllerKaraoke::ModifyStart(int delta) { if (cur_syl == 0) return; MoveMarker(&markers[cur_syl - 1], markers[cur_syl - 1] + delta * 10); - AnnounceChanges(cur_syl); + AnnounceChanges(true); +} + +void AudioTimingControllerKaraoke::MoveTapMarker(int ms) { + // Fix rounding error + ms = (ms + 5) / 10 * 10; + + // Get syllable this time falls within + const size_t syl = distance(markers.begin(), lower_bound(markers.begin(), markers.end(), ms)); + + // Tapping automatically all of the necessary markers for the tap marker to + // land at the current audio position. Intuitively, it "pushes" or + // "compresses" all of the markers in front of the tap marker. The + // expectation is that the markers will reach their proper position once the + // user finishes tapping to the line + if (tap_marker_idx == 0) { + // Moving the start time of first syllable (i.e. start time of the line) + if (ms > end_marker) MoveEndMarker(ms); + if (syl > 0) CompressMarkers(syl-1, 0, ms); + MoveStartMarker(ms); + } + else if (tap_marker_idx < markers.size() + 1) { + // Moving the end time of a non-end syllable + if (ms < start_marker) MoveStartMarker(ms); + else if (ms > end_marker) MoveEndMarker(ms); + if (syl < tap_marker_idx) { + // Moving marker left + CompressMarkers(syl, tap_marker_idx-1, ms); + } + else { + // Moving marker right + CompressMarkers(syl-1, tap_marker_idx-1, ms); + } + } + else { + // Moving the end time of last syllable (i.e. end time of the line) + if (ms < start_marker) MoveStartMarker(ms); + if (syl < markers.size()) CompressMarkers(0, markers.size()-1, ms); + MoveEndMarker(ms); + } + + AnnounceChanges(true); +} + +bool AudioTimingControllerKaraoke::NextTapMarker() { + if (tap_marker_idx < markers.size() + 1) { + ++tap_marker_idx; + AnnounceUpdatedTapMarker(); + return true; + } + return false; } bool AudioTimingControllerKaraoke::IsNearbyMarker(int ms, int sensitivity, bool) const { @@ -350,18 +435,34 @@ std::vector AudioTimingControllerKaraoke::OnLeftClick(int ms, bool cur_syl = syl; + // Change tap marker + // Selecting a syllable moves the marker to the _end_ of that syllable, such + // that the next tap determines when that syllable ends. This behavior is + // more intuitive when coupled with AudioKaraoke's tap syllable highlight. + if (ms < start_marker.GetPosition()) { + tap_marker_idx = 0; + } + else { + tap_marker_idx = cur_syl + 1; + } + AnnounceUpdatedPrimaryRange(); AnnounceUpdatedStyleRanges(); + AnnounceUpdatedTapMarker(); return {}; } -std::vector AudioTimingControllerKaraoke::OnRightClick(int ms, bool, int, int) { - cur_syl = distance(markers.begin(), lower_bound(markers.begin(), markers.end(), ms)); +std::vector AudioTimingControllerKaraoke::OnRightClick(int ms, bool ctrl_down, int, int) { + if (ctrl_down) { + c->audioController->PlayToEnd(ms); - AnnounceUpdatedPrimaryRange(); - AnnounceUpdatedStyleRanges(); - c->audioController->PlayPrimaryRange(); + } else { + cur_syl = distance(markers.begin(), lower_bound(markers.begin(), markers.end(), ms)); + AnnounceUpdatedPrimaryRange(); + AnnounceUpdatedStyleRanges(); + c->audioController->PlayPrimaryRange(); + } return {}; } @@ -387,10 +488,57 @@ int AudioTimingControllerKaraoke::MoveMarker(KaraokeMarker *marker, int new_posi return syl; } -void AudioTimingControllerKaraoke::AnnounceChanges(int syl) { - if (syl < 0) return; +void AudioTimingControllerKaraoke::MoveStartMarker(int new_position) { + // No rearranging of syllables allowed + new_position = mid( + 0, + new_position, + markers[0].GetPosition()); - if (syl == cur_syl || syl == cur_syl + 1) { + if (new_position == start_marker.GetPosition()) + return; + + start_marker.Move(new_position); + + active_line->Start = (int)start_marker; + kara->SetLineTimes(start_marker, end_marker); + + labels.front().range = TimeRange(start_marker, labels.front().range.end()); +} + +void AudioTimingControllerKaraoke::MoveEndMarker(int new_position) { + // No rearranging of syllables allowed + new_position = mid( + markers.back().GetPosition(), + new_position, + INT_MAX); + + if (new_position == end_marker.GetPosition()) + return; + + end_marker.Move(new_position); + + active_line->End = (int)end_marker; + kara->SetLineTimes(start_marker, end_marker); + + labels.back().range = TimeRange(labels.back().range.begin(), end_marker); +} + +void AudioTimingControllerKaraoke::CompressMarkers(size_t from, size_t to, int new_position) { + int incr = (from < to ? 1 : -1); + size_t i = from; + for (;;) { + MoveMarker(&markers[i], new_position); + if (i == to) { + break; + } else { + i += incr; + } + } +} + +void AudioTimingControllerKaraoke::AnnounceChanges(bool announce_primary) { + if (announce_primary) { AnnounceUpdatedPrimaryRange(); AnnounceUpdatedStyleRanges(); } @@ -412,14 +560,15 @@ void AudioTimingControllerKaraoke::OnMarkerDrag(std::vector const& int syl = MoveMarker(static_cast(m[0]), new_position); if (syl < 0) return; + bool announce_primary = (syl == cur_syl || syl == cur_syl + 1); if (m.size() > 1) { int delta = m[0]->GetPosition() - old_position; for (AudioMarker *marker : m | boost::adaptors::sliced(1, m.size())) MoveMarker(static_cast(marker), marker->GetPosition() + delta); - syl = cur_syl; + announce_primary = true; } - AnnounceChanges(syl); + AnnounceChanges(announce_primary); } void AudioTimingControllerKaraoke::GetLabels(TimeRange const& range, std::vector &out) const { diff --git a/src/bitmaps/button/time_opt_tap_to_time_16.png b/src/bitmaps/button/time_opt_tap_to_time_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0e433cd229463d8a43152864ea1256b351adc314 GIT binary patch literal 1594 zcmZA1dpy%?90%}cxh%^ZLJEa+Su}p5va=di=5jEX&2hgDxn*umu9+#M`yxeId8H)x zTfG)ijf&`kh$0d#cBrk9+xa=?uXCRF>-&6O&+~dd|32A{6e~1J9R&bD+gMvT!>PDB zin8$TK-b+0hxB2RJqdv7v&joi9QdqFw|2G%AaOeYX>0)ISK#~#z)=VQfgb>bd;ruK z7k&Ab@LSmcTPur|f0mq^WC*^Ii?Vi)1z?@>>L9??!j14HGS0@Hj2uO2B9t)PY(xwI zGP`UnNHptqey&pp$6ftoQS7prj{U9v8z(Bz_2r>e*7wf)B^n!a3809ashZuVzA<<6 zvZ_!l+<{O)pQ3CnPL0 zl#=IW${UZM6tzsJlio|}D!)C;yMWd8wCrjKpZ4JODGW`RN=P9Gl?m1@q7J=RgJR}u zdH%-$LoIgik)xhsKko(u&mt+dywTdg=NC_0p&H)~rVux)&-^xrT+|{~4mi~o2V0FA z)`Z`HkxtZ%4}6b!Fh3X&R9)jH7jV!nOYRj<9NDCyx%aGkrm`mcENQ~mzv5^`?>ti5 zq4{%4lo!*BiK+5l1)?o9m8{HjLUCBx*Zj$Vxae;SORQz{<0ESJ#LOfGiKgs_6h2JG z?($b7?>-%uiWe@wn?)Q@?H~JjpQz9vH<(eb@%_hqLhz@K8-|&{)Ts!A=*jKPeXgA) zd72cULc1-XU3(<8Wd@w~r}uBnTk#6_f%P3<(G$U>or$j9y|LU2mg=RdDK{NNtY-V+ z5{6U?+4r?;TIJj+_mQM#Bou#jzZ18aI@}c9gL}1KOleYvAczZj=Ns6uC)Jj27LmJR z@@fTIUSsO-%$s!>>nFKvh4Z#CHU_Nw+YGX8SNLs_Ltt)JDjyrX4t;)Mk0i7Ly}s1~ zQpy+=!;UJt@?E0E*roEclqtHF+mpVkz49f)qJmUz_KvF`H_aimfXYeKA;2l)lws!H zFV$5G5eFSbmrpQ?{x>zWmkGT!(_jt z$F@Guo)QaT=~>zNS*%6NR4K=u0%`MLg7KO3B{SiQ65@KKgNgY~f>V=V4ZU-taVpxw z**46Ed)nwl8mI8{=lyH?Uq&+7gyOC{#)?8z{0Sa6u^KjORvm0CnsNVByJk%trCb>i z_zXaN73xgLuh*jDbt5snp$hqj_+O)wImKKLT25F(ybQ0OZ{Qf3`)6of`2fs4>e{Nb zf>PKnw@5FTwBU}^tD*O8LK9>eU640Vdr&O*j47n)6EiizKn`XrBr zI$`odNzA_nnhq;cajMiPOd^LQd zpWF{)zlpsy@kci)QJZ*vOPZI_<<9Lnrr65y`s7r%4M9#DND&pI#U<_b=L)toifGdM z>8h<}%ifzEkv*(8!%EubX@$vH-;Q_rVdU~Vs+fLMLpu6%z9xsx_3befsI{Wpb@Zjf ztkI=9)l}v)g#mw;O+VTm;?*AzqVFZQ-C;Y!)Lf8ZSTOb({hv5RSAyX_W4*I444GnG9 A0{{R3 literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/time_opt_tap_to_time_24.png b/src/bitmaps/button/time_opt_tap_to_time_24.png new file mode 100644 index 0000000000000000000000000000000000000000..5056a5fa06a03cf3f1e7c19ee9022255d44fa6d7 GIT binary patch literal 2635 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 E4-=94YXATM literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/time_opt_tap_to_time_64.png b/src/bitmaps/button/time_opt_tap_to_time_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2abe07f52efe6be31fe0856e5eba6868fb6ef2a1 GIT binary patch literal 8397 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 C10U=F literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/time_tap_connect_16.png b/src/bitmaps/button/time_tap_connect_16.png new file mode 100644 index 0000000000000000000000000000000000000000..da5b51afc07330ef5797366308149ac03933681c GIT binary patch literal 1036 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 V|9^nr<&O{mz|zddw9?oo<{vy`j}ia? literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/time_tap_connect_64.png b/src/bitmaps/button/time_tap_connect_64.png new file mode 100644 index 0000000000000000000000000000000000000000..e7801eda18cef43f5bb1b9cc3f7f17232cb00669 GIT binary patch literal 3527 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 BgT4R& literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/time_tap_no_connect_16.png b/src/bitmaps/button/time_tap_no_connect_16.png new file mode 100644 index 0000000000000000000000000000000000000000..e685f10935f9c1b682546def308be616e90324b9 GIT binary patch literal 859 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 VFfbmXIAH()002ovPDHLkV1n_bz=;3= literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/time_tap_no_connect_64.png b/src/bitmaps/button/time_tap_no_connect_64.png new file mode 100644 index 0000000000000000000000000000000000000000..a56b48142f08e952f8bc9a71c702f5e7d5184907 GIT binary patch literal 3464 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 z_KccYE{YHSetBool(!OPT_GET(opt)->GetBool()); +} + struct validate_video_loaded : public Command { CMD_TYPE(COMMAND_VALIDATE) bool Validate(const agi::Context *c) override { @@ -385,6 +390,93 @@ struct time_prev final : public Command { c->audioController->GetTimingController()->Prev(); } }; + +struct time_tap_connect final : public Command { + CMD_NAME("time/tap/connect") + CMD_ICON(time_tap_connect) + STR_MENU("Time tap connect") + STR_DISP("Time tap connect") + STR_HELP("Set tap marker to audio position, connect next line's start") + CMD_TYPE(COMMAND_VALIDATE) + + bool Validate(const agi::Context *c) override { + return + OPT_GET("Timing/Tap To Time")->GetBool() && + c->audioController->IsPlaying(); + } + + void operator()(agi::Context *c) override { + if (c->audioController->IsPlaying()) { + AudioTimingController *tc = c->audioController->GetTimingController(); + if (tc) { + int ms = c->audioController->GetPlaybackPosition(); + + tc->MoveTapMarker(ms); + bool moved_marker = tc->NextTapMarker(); + if (!moved_marker && + OPT_GET("Audio/Auto/Commit")->GetBool() && + OPT_GET("Audio/Next Line on Commit")->GetBool()) { + // go to next line, and then tap again to connect start to the same + // time + c->selectionController->NextLine(); + tc->MoveTapMarker(ms); + tc->NextTapMarker(); + } + } + } + } +}; + +struct time_tap_no_connect final : public Command { + CMD_NAME("time/tap/no_connect") + CMD_ICON(time_tap_no_connect) + STR_MENU("Tap marker no connect") + STR_DISP("Tap marker no connect") + STR_HELP("Set tap marker to audio position, do not connect next line's start") + CMD_TYPE(COMMAND_VALIDATE) + + bool Validate(const agi::Context *c) override { + return + OPT_GET("Timing/Tap To Time")->GetBool() && + c->audioController->IsPlaying(); + } + + void operator()(agi::Context *c) override { + if (c->audioController->IsPlaying()) { + AudioTimingController *tc = c->audioController->GetTimingController(); + if (tc) { + int ms = c->audioController->GetPlaybackPosition(); + + tc->MoveTapMarker(ms); + bool moved_marker = tc->NextTapMarker(); + if (!moved_marker && + OPT_GET("Audio/Auto/Commit")->GetBool() && + OPT_GET("Audio/Next Line on Commit")->GetBool()) { + // go to next line, but don't do anything more + c->selectionController->NextLine(); + } + } + } + } +}; + +struct time_opt_tap_to_time final : public Command { + CMD_NAME("time/opt/tap_to_time") + CMD_ICON(time_opt_tap_to_time) + STR_MENU("Enable tap-to-time UI") + STR_DISP("Enable tap-to-time UI") + STR_HELP("Enable tap-to-time UI") + CMD_TYPE(COMMAND_TOGGLE) + + bool IsActive(const agi::Context *) override { + return OPT_GET("Timing/Tap To Time")->GetBool(); + } + + void operator()(agi::Context *) override { + toggle("Timing/Tap To Time"); + } +}; + } namespace cmd { @@ -399,7 +491,10 @@ namespace cmd { reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); reg(agi::make_unique()); + reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); diff --git a/src/hotkey.cpp b/src/hotkey.cpp index deb624628..3283c1eb4 100644 --- a/src/hotkey.cpp +++ b/src/hotkey.cpp @@ -57,6 +57,12 @@ namespace { {nullptr} }; + const char *added_hotkeys_time_tap[][3] = { + {"time/tap/connect", "Audio", "I"}, + {"time/tap/no_connect", "Audio", "O"}, + {nullptr} + }; + void migrate_hotkeys(const char *added[][3]) { auto hk_map = hotkey::inst->GetHotkeyMap(); bool changed = false; @@ -131,6 +137,11 @@ void init() { } #endif + if (boost::find(migrations, "time/tap") == end(migrations)) { + migrate_hotkeys(added_hotkeys_time_tap); + migrations.emplace_back("time/tap"); + } + OPT_SET("App/Hotkey Migrations")->SetListString(std::move(migrations)); } diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 240998fdb..41b04d5b8 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -471,7 +471,8 @@ }, "Timing" : { - "Default Duration" : 3000 + "Default Duration" : 3000, + "Tap To Time" : false }, "Tool" : { diff --git a/src/libresrc/default_hotkey.json b/src/libresrc/default_hotkey.json index 789ab0d78..43ecca9c9 100644 --- a/src/libresrc/default_hotkey.json +++ b/src/libresrc/default_hotkey.json @@ -32,6 +32,12 @@ ], "time/start/increase" : [ "KP_6" + ], + "time/mark/connect" : [ + "I" + ], + "time/mark/no_connect" : [ + "O" ] }, "Audio" : { diff --git a/src/libresrc/default_toolbar.json b/src/libresrc/default_toolbar.json index ad52b2e5b..a47a0fb23 100644 --- a/src/libresrc/default_toolbar.json +++ b/src/libresrc/default_toolbar.json @@ -15,6 +15,9 @@ "time/lead/in", "time/lead/out", "", + "time/tap/connect", + "time/tap/no_connect", + "", "audio/commit", "audio/go_to", "", @@ -23,6 +26,7 @@ "audio/opt/autoscroll", "audio/opt/spectrum", "app/toggle/global_hotkeys", + "time/opt/tap_to_time", "", "audio/karaoke" ], diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 01acf37da..e39820c82 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -471,7 +471,8 @@ }, "Timing" : { - "Default Duration" : 3000 + "Default Duration" : 3000, + "Tap To Time" : false }, "Tool" : {