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
@ -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<AudioLabelProvider::AudioLabel> 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);
|
||||
|
@ -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);
|
||||
|
@ -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<AssKaraoke>())
|
||||
{
|
||||
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();
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<AudioMarker*> OnLeftClick(int ms, bool ctrl_down, bool alt_down, int sensitivity, int snap_range) override;
|
||||
std::vector<AudioMarker*> OnRightClick(int ms, bool, int sensitivity, int snap_range) override;
|
||||
std::vector<AudioMarker*> OnRightClick(int ms, bool ctrl_down, int sensitivity, int snap_range) override;
|
||||
void OnMarkerDrag(std::vector<AudioMarker*> 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<int>(*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<AudioMarker*> 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<AudioMarker*> 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<AudioMarker*> AudioTimingControllerDialogue::OnRightClick(int ms, bool, int sensitivity, int snap_range)
|
||||
std::vector<AudioMarker*> AudioTimingControllerDialogue::OnRightClick(int ms, bool ctrl_down, int sensitivity, int snap_range)
|
||||
{
|
||||
clicked_ms = INT_MIN;
|
||||
std::vector<AudioMarker*> ret = GetRightMarkers();
|
||||
SetMarkers(ret, ms, snap_range);
|
||||
return ret;
|
||||
if (ctrl_down) {
|
||||
context->audioController->PlayToEnd(ms);
|
||||
return {};
|
||||
|
||||
} else {
|
||||
clicked_ms = INT_MIN;
|
||||
std::vector<AudioMarker*> ret = GetRightMarkers();
|
||||
SetMarkers(ret, ms, snap_range);
|
||||
tap_marker_idx = 1;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioTimingControllerDialogue::OnMarkerDrag(std::vector<AudioMarker*> const& markers, int new_position, int snap_range)
|
||||
|
@ -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<AudioMarker*> OnLeftClick(int ms, bool, bool, int sensitivity, int) override;
|
||||
std::vector<AudioMarker*> OnRightClick(int ms, bool, int, int) override;
|
||||
std::vector<AudioMarker*> OnRightClick(int ms, bool ctrl_down, int, int) override;
|
||||
void OnMarkerDrag(std::vector<AudioMarker*> 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<AudioMarker*> 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<AudioMarker*> AudioTimingControllerKaraoke::OnRightClick(int ms, bool, int, int) {
|
||||
cur_syl = distance(markers.begin(), lower_bound(markers.begin(), markers.end(), ms));
|
||||
std::vector<AudioMarker*> 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<AudioMarker*> const&
|
||||
int syl = MoveMarker(static_cast<KaraokeMarker *>(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<KaraokeMarker *>(marker), marker->GetPosition() + delta);
|
||||
syl = cur_syl;
|
||||
announce_primary = true;
|
||||
}
|
||||
|
||||
AnnounceChanges(syl);
|
||||
AnnounceChanges(announce_primary);
|
||||
}
|
||||
|
||||
void AudioTimingControllerKaraoke::GetLabels(TimeRange const& range, std::vector<AudioLabel> &out) const {
|
||||
|
BIN
src/bitmaps/button/time_opt_tap_to_time_16.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
src/bitmaps/button/time_opt_tap_to_time_24.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
src/bitmaps/button/time_opt_tap_to_time_32.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/bitmaps/button/time_opt_tap_to_time_48.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/bitmaps/button/time_opt_tap_to_time_64.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
src/bitmaps/button/time_tap_connect_16.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
src/bitmaps/button/time_tap_connect_24.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/bitmaps/button/time_tap_connect_32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/bitmaps/button/time_tap_connect_48.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src/bitmaps/button/time_tap_connect_64.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/bitmaps/button/time_tap_no_connect_16.png
Normal file
After Width: | Height: | Size: 859 B |
BIN
src/bitmaps/button/time_tap_no_connect_24.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
src/bitmaps/button/time_tap_no_connect_32.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src/bitmaps/button/time_tap_no_connect_48.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/bitmaps/button/time_tap_no_connect_64.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
@ -446,6 +446,21 @@ button/substart_to_video_24.png
|
||||
button/substart_to_video_32.png
|
||||
button/substart_to_video_48.png
|
||||
button/substart_to_video_64.png
|
||||
button/time_tap_connect_16.png
|
||||
button/time_tap_connect_24.png
|
||||
button/time_tap_connect_32.png
|
||||
button/time_tap_connect_48.png
|
||||
button/time_tap_connect_64.png
|
||||
button/time_tap_no_connect_16.png
|
||||
button/time_tap_no_connect_24.png
|
||||
button/time_tap_no_connect_32.png
|
||||
button/time_tap_no_connect_48.png
|
||||
button/time_tap_no_connect_64.png
|
||||
button/time_opt_tap_to_time_16.png
|
||||
button/time_opt_tap_to_time_24.png
|
||||
button/time_opt_tap_to_time_32.png
|
||||
button/time_opt_tap_to_time_48.png
|
||||
button/time_opt_tap_to_time_64.png
|
||||
button/timing_processor_toolbutton_16.png
|
||||
button/timing_processor_toolbutton_24.png
|
||||
button/timing_processor_toolbutton_32.png
|
||||
|
@ -39,6 +39,7 @@
|
||||
#include "../dialogs.h"
|
||||
#include "../include/aegisub/context.h"
|
||||
#include "../libresrc/libresrc.h"
|
||||
#include "../options.h"
|
||||
#include "../project.h"
|
||||
#include "../selection_controller.h"
|
||||
#include "../video_controller.h"
|
||||
@ -50,6 +51,10 @@
|
||||
namespace {
|
||||
using cmd::Command;
|
||||
|
||||
static inline void toggle(const char *opt) {
|
||||
OPT_SET(opt)->SetBool(!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<time_length_decrease_shift>());
|
||||
reg(agi::make_unique<time_length_increase>());
|
||||
reg(agi::make_unique<time_length_increase_shift>());
|
||||
reg(agi::make_unique<time_tap_connect>());
|
||||
reg(agi::make_unique<time_tap_no_connect>());
|
||||
reg(agi::make_unique<time_next>());
|
||||
reg(agi::make_unique<time_opt_tap_to_time>());
|
||||
reg(agi::make_unique<time_prev>());
|
||||
reg(agi::make_unique<time_shift>());
|
||||
reg(agi::make_unique<time_snap_end_video>());
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -470,7 +470,8 @@
|
||||
},
|
||||
|
||||
"Timing" : {
|
||||
"Default Duration" : 3000
|
||||
"Default Duration" : 3000,
|
||||
"Tap To Time" : false
|
||||
},
|
||||
|
||||
"Tool" : {
|
||||
|
@ -32,6 +32,12 @@
|
||||
],
|
||||
"time/start/increase" : [
|
||||
"KP_6"
|
||||
],
|
||||
"time/mark/connect" : [
|
||||
"I"
|
||||
],
|
||||
"time/mark/no_connect" : [
|
||||
"O"
|
||||
]
|
||||
},
|
||||
"Audio" : {
|
||||
|
@ -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"
|
||||
],
|
||||
|
@ -470,7 +470,8 @@
|
||||
},
|
||||
|
||||
"Timing" : {
|
||||
"Default Duration" : 3000
|
||||
"Default Duration" : 3000,
|
||||
"Tap To Time" : false
|
||||
},
|
||||
|
||||
"Tool" : {
|
||||
|