From e4cfe4ea0d6c6f2c86e2c770000f2b78a4dfc616 Mon Sep 17 00:00:00 2001 From: Victor Arvidsson Date: Wed, 23 Oct 2024 17:57:56 +0200 Subject: [PATCH 1/3] Make the "Save dive data as subtitles" feature more configurable. Previously, the subtitle generation was hardcoded, making it unsuitable if you didn't want all of the displayed values. This has been replaced by a format string that is configurable in the settings, using predefined tags that are replaced with the values. The default value for this has been set to (mostly) match the currently generated subtitle string. This also provides a good starting point for users that want to modify the string. Signed-off-by: Victor Arvidsson --- CHANGELOG.md | 1 + core/pref.h | 1 + core/save-profiledata.cpp | 224 ++++++++++++- core/settings/qPrefMedia.cpp | 2 + core/settings/qPrefMedia.h | 5 + core/subsurfacestartup.cpp | 1 + .../preferences/preferences_media.cpp | 2 + .../preferences/preferences_media.ui | 314 ++++++++++++++++++ 8 files changed, 535 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 031f1f2f9..cc4db1ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +desktop: update "Save dive data as subtitles" feature to make it more configurable. bluetooth: fix crash on MacOS when doing first download from a new BLE device statistics: show proper dates in January desktop: add country to the fields indexed for full text search diff --git a/core/pref.h b/core/pref.h index a0d450de3..437cc74c8 100644 --- a/core/pref.h +++ b/core/pref.h @@ -113,6 +113,7 @@ struct preferences { bool extract_video_thumbnails; int extract_video_thumbnails_position; // position in stream: 0=first 100=last second std::string ffmpeg_executable; // path of ffmpeg binary + std::string subtitles_format_string; // Format string for subtitles generated from the dive data int defaultsetpoint; // default setpoint in mbar std::string default_filename; enum def_file_behavior default_file_behavior; diff --git a/core/save-profiledata.cpp b/core/save-profiledata.cpp index 4befa7d0b..5715c698f 100644 --- a/core/save-profiledata.cpp +++ b/core/save-profiledata.cpp @@ -6,7 +6,9 @@ #include "core/errorhelper.h" #include "core/file.h" #include "core/format.h" +#include "core/gettext.h" #include "core/membuffer.h" +#include "core/pref.h" #include "core/subsurface-string.h" #include "core/version.h" #include @@ -45,6 +47,16 @@ static std::string video_time(int secs) return format_string_std("%d:%02d:%02d.000,", hours, mins, secs); } +void replace_all(std::string& str, const std::string& old_value, const std::string& new_value) { + if (old_value.empty()) + return; + size_t start_pos = std::string::npos; + while ((start_pos = str.find(old_value, start_pos)) != std::string::npos) { + str.replace(start_pos, old_value.length(), new_value); + start_pos += new_value.length(); // In case 'new_value' contains 'old_value', like replacing 'x' with 'yx' + } +} + static void put_pd(struct membuffer *b, const struct plot_info &pi, int idx) { const struct plot_data &entry = pi.entry[idx]; @@ -175,32 +187,214 @@ static std::string format_st_event(const plot_data &entry, const plot_data &next double value; int decimals; const char *unit; - - if (entry.sec < offset || entry.sec > offset + length) + if (next_entry.sec < offset || entry.sec > offset + length) return {}; std::string res = "Dialogue: 0,"; - res += video_time(entry.sec - offset); - res += video_time(next_entry.sec - offset < length ? next_entry.sec - offset : length); + res += video_time(std::max(entry.sec - offset, 0)); + res += video_time(std::min(next_entry.sec - offset, length)); res += "Default,,0,0,0,,"; - res += format_string_std("%d:%02d ", FRACTION_TUPLE(entry.sec, 60)); + + std::string format_string = prefs.subtitles_format_string; + + replace_all(format_string, "[time]", format_string_std("%d:%02d", FRACTION_TUPLE(entry.sec, 60))); value = get_depth_units(entry.depth, &decimals, &unit); - res += format_string_std("D=%02.2f %s ", value, unit); + replace_all(format_string, "[depth]", format_string_std("%02.2f %s", value, unit)); + if (entry.temperature) { value = get_temp_units(entry.temperature, &unit); - res += format_string_std("T=%.1f%s ", value, unit); + replace_all(format_string,"[temperature]", format_string_std("%.1f%s", value, unit)); + } else { + replace_all(format_string, "[temperature]", ""); } - // Only show NDL if it is not essentially infinite, show TTS for mandatory stops. - if (entry.ndl_calc < 3600) { - if (entry.ndl_calc > 0) - res += format_string_std("NDL=%d:%02d ", FRACTION_TUPLE(entry.ndl_calc, 60)); - else - if (entry.tts_calc > 0) - res += format_string_std("TTS=%d:%02d ", FRACTION_TUPLE(entry.tts_calc, 60)); + + if (entry.ceiling) { + value = get_depth_units(entry.ceiling, &decimals, &unit); + replace_all(format_string,"[ceiling]", format_string_std("%02.2f %s", value, unit)); + } else { + replace_all(format_string, "[ceiling]", ""); } + + if (entry.ndl > 0) { + if (entry.ndl < 7200) { + replace_all(format_string,"[ndl]", format_string_std("%d:%02d", FRACTION_TUPLE(entry.ndl, 60))); + } else { + replace_all(format_string,"[ndl]", ">2h"); + } + } else { + replace_all(format_string, "[ndl]", ""); + } + + if (entry.tts > 0) { + replace_all(format_string,"[tts]", format_string_std("%d:%02d", FRACTION_TUPLE(entry.tts, 60))); + } else { + replace_all(format_string, "[tts]", ""); + } + + if (entry.rbt > 0) { + replace_all(format_string,"[rbt]", format_string_std("%d:%02d", FRACTION_TUPLE(entry.rbt, 60))); + } else { + replace_all(format_string, "[rbt]", ""); + } + + if (entry.stoptime > 0) { + replace_all(format_string,"[stoptime]", format_string_std("%d:%02d", FRACTION_TUPLE(entry.stoptime, 60))); + } else { + replace_all(format_string, "[stoptime]", ""); + } + + if (entry.stopdepth > 0) { + value = get_depth_units(entry.stopdepth, &decimals, &unit); + replace_all(format_string, "[stopdepth]", format_string_std("%02.2f %s", value, unit)); + } else { + replace_all(format_string, "[stopdepth]", ""); + } + + replace_all(format_string, "[cns]", format_string_std("%u%%", entry.cns)); + + if (entry.sac > 0) { + value = get_volume_units(entry.sac, &decimals, &unit); + replace_all(format_string, "[sac]", format_string_std("%02.2f %s", value, unit)); + } else { + replace_all(format_string, "[sac]", ""); + } + + if (entry.pressures.o2 > 0) { + replace_all(format_string, "[p_o2]", format_string_std("%.2fbar", entry.pressures.o2)); + } else { + replace_all(format_string, "[p_o2]", ""); + } + + if (entry.pressures.n2 > 0) { + replace_all(format_string, "[p_n2]", format_string_std("%.2fbar", entry.pressures.n2)); + } else { + replace_all(format_string, "[p_n2]", ""); + } + + if (entry.pressures.he > 0) { + replace_all(format_string, "[p_he]", format_string_std("%.2fbar", entry.pressures.he)); + } else { + replace_all(format_string, "[p_he]", ""); + } + + if (entry.o2pressure.mbar > 0) { + replace_all(format_string, "[o2_pressure]", format_string_std("%.2fbar", entry.o2pressure.mbar/1000.0)); + } else { + replace_all(format_string, "[o2_pressure]", ""); + } + + if (entry.o2setpoint.mbar > 0) { + replace_all(format_string, "[o2_setpoint]", format_string_std("%.2fbar", entry.o2setpoint.mbar/1000.0)); + } else { + replace_all(format_string, "[o2_setpoint]", ""); + } + + if (entry.scr_OC_pO2.mbar > 0 && entry.pressures.o2 > 0) { + replace_all(format_string, "[scr_oc_po2]", format_string_std("%.2fbar", entry.scr_OC_pO2.mbar/1000.0 - entry.pressures.o2)); + } else { + replace_all(format_string, "[scr_oc_po2]", ""); + } + + if (entry.mod > 0) { + value = get_depth_units(entry.mod, &decimals, &unit); + replace_all(format_string, "[mod]", format_string_std("%02.2f %s", value, unit)); + } else { + replace_all(format_string, "[mod]", ""); + } + + if (entry.ead > 0) { + value = get_depth_units(entry.ead, &decimals, &unit); + replace_all(format_string, "[ead]", format_string_std("%02.2f %s", value, unit)); + } else { + replace_all(format_string, "[ead]", ""); + } + + if (entry.end > 0) { + value = get_depth_units(entry.end, &decimals, &unit); + replace_all(format_string, "[end]", format_string_std("%02.2f %s", value, unit)); + } else { + replace_all(format_string, "[end]", ""); + } + + if (entry.eadd > 0) { + value = get_depth_units(entry.eadd, &decimals, &unit); + replace_all(format_string, "[eadd]", format_string_std("%02.2f %s", value, unit)); + } else { + replace_all(format_string, "[eadd]", ""); + } + + value = get_vertical_speed_units(entry.speed, &decimals, &unit); + if (entry.speed > 0) + /* Ascending speeds are positive, descending are negative */ + value *= -1; + replace_all(format_string, "[speed]", format_string_std("%02.2f %s", value, unit)); + + if (entry.in_deco) { + replace_all(format_string, "[in_deco]", translate("gettextFromC", "In deco")); + } else { + replace_all(format_string, "[in_deco]", ""); + } + + if (entry.ndl_calc > 0) { + if (entry.ndl_calc < 7200) { + replace_all(format_string,"[ndl_calc]", format_string_std("%d:%02d", FRACTION_TUPLE(entry.ndl_calc, 60))); + } else { + replace_all(format_string,"[ndl_calc]", ">2h"); + } + } else { + replace_all(format_string, "[ndl_calc]", ""); + } + + if (entry.tts_calc > 0) { + replace_all(format_string,"[tts_calc]", format_string_std("%d:%02d", FRACTION_TUPLE(entry.tts_calc, 60))); + } else { + replace_all(format_string, "[tts_calc]", ""); + } + + if (entry.stoptime_calc > 0) { + replace_all(format_string,"[stoptime_calc]", format_string_std("%d:%02d", FRACTION_TUPLE(entry.stoptime_calc, 60))); + } else { + replace_all(format_string, "[stoptime_calc]", ""); + } + + if (entry.stopdepth_calc > 0) { + value = get_depth_units(entry.stopdepth_calc, &decimals, &unit); + replace_all(format_string, "[stopdepth_calc]", format_string_std("%02.2f %s", value, unit)); + } else { + replace_all(format_string, "[stopdepth_calc]", ""); + } + + if (entry.heartbeat > 0) { + replace_all(format_string, "[heartrate]", format_string_std("%d", entry.heartbeat)); + } else { + replace_all(format_string, "[heartrate]", ""); + } + if (entry.surface_gf > 0.0) { - res += format_string_std("sGF=%.1f%% ", entry.surface_gf); + replace_all(format_string, "[surface_gf]", format_string_std("%.1f%%", entry.surface_gf)); + } else { + replace_all(format_string, "[surface_gf]", ""); } + + if (entry.current_gf > 0.0) { + replace_all(format_string, "[current_gf]", format_string_std("%.1f%%", entry.current_gf)); + } else { + replace_all(format_string, "[current_gf]", ""); + } + + if (entry.density > 0) { + replace_all(format_string, "[density]", format_string_std("%.1fg/ℓ", entry.density)); + } else { + replace_all(format_string, "[density]", ""); + } + + if (entry.icd_warning) { + replace_all(format_string, "[icd_warning]", translate("gettextFromC", "ICD in leading tissue")); + } else { + replace_all(format_string, "[icd_warning]", ""); + } + + res += format_string; res += "\n"; return res; } diff --git a/core/settings/qPrefMedia.cpp b/core/settings/qPrefMedia.cpp index 6d86c441c..07b77f8ab 100644 --- a/core/settings/qPrefMedia.cpp +++ b/core/settings/qPrefMedia.cpp @@ -15,6 +15,7 @@ void qPrefMedia::loadSync(bool doSync) disk_extract_video_thumbnails(doSync); disk_extract_video_thumbnails_position(doSync); disk_ffmpeg_executable(doSync); + disk_subtitles_format_string(doSync); disk_auto_recalculate_thumbnails(doSync); disk_auto_recalculate_thumbnails(doSync); } @@ -23,3 +24,4 @@ HANDLE_PREFERENCE_BOOL(Media, "auto_recalculate_thumbnails", auto_recalculate_th HANDLE_PREFERENCE_BOOL(Media, "extract_video_thumbnails", extract_video_thumbnails); HANDLE_PREFERENCE_INT(Media, "extract_video_thumbnails_position", extract_video_thumbnails_position); HANDLE_PREFERENCE_TXT(Media, "ffmpeg_executable", ffmpeg_executable); +HANDLE_PREFERENCE_TXT(Media, "subtitles_format_string", subtitles_format_string); diff --git a/core/settings/qPrefMedia.h b/core/settings/qPrefMedia.h index 66d538cbd..f517637b5 100644 --- a/core/settings/qPrefMedia.h +++ b/core/settings/qPrefMedia.h @@ -11,6 +11,7 @@ class qPrefMedia : public QObject { Q_PROPERTY(bool extract_video_thumbnails READ extract_video_thumbnails WRITE set_extract_video_thumbnails NOTIFY extract_video_thumbnailsChanged) Q_PROPERTY(int extract_video_thumbnails_position READ extract_video_thumbnails_position WRITE set_extract_video_thumbnails_position NOTIFY extract_video_thumbnails_positionChanged) Q_PROPERTY(QString ffmpeg_executable READ ffmpeg_executable WRITE set_ffmpeg_executable NOTIFY ffmpeg_executableChanged) + Q_PROPERTY(QString subtitles_format_string READ subtitles_format_string WRITE set_subtitles_format_string NOTIFY subtitles_format_stringChanged) public: static qPrefMedia *instance(); @@ -25,18 +26,21 @@ public: static bool extract_video_thumbnails() { return prefs.extract_video_thumbnails; } static int extract_video_thumbnails_position() { return prefs.extract_video_thumbnails_position; } static QString ffmpeg_executable() { return QString::fromStdString(prefs.ffmpeg_executable); } + static QString subtitles_format_string() { return QString::fromStdString(prefs.subtitles_format_string); } public slots: static void set_auto_recalculate_thumbnails(bool value); static void set_extract_video_thumbnails(bool value); static void set_extract_video_thumbnails_position(int value); static void set_ffmpeg_executable(const QString& value); + static void set_subtitles_format_string(const QString& value); signals: void auto_recalculate_thumbnailsChanged(bool value); void extract_video_thumbnailsChanged(bool value); void extract_video_thumbnails_positionChanged(int value); void ffmpeg_executableChanged(const QString& value); + void subtitles_format_stringChanged(const QString& value); private: qPrefMedia() {} @@ -45,6 +49,7 @@ private: static void disk_extract_video_thumbnails(bool doSync); static void disk_extract_video_thumbnails_position(bool doSync); static void disk_ffmpeg_executable(bool doSync); + static void disk_subtitles_format_string(bool doSync); }; diff --git a/core/subsurfacestartup.cpp b/core/subsurfacestartup.cpp index 502bc5010..94b24c9ca 100644 --- a/core/subsurfacestartup.cpp +++ b/core/subsurfacestartup.cpp @@ -197,6 +197,7 @@ void setup_system_prefs() default_prefs.divelist_font = system_divelist_default_font; default_prefs.font_size = system_divelist_default_font_size; default_prefs.ffmpeg_executable = "ffmpeg"; + default_prefs.subtitles_format_string = "[time] D=[depth] T=[temperature] sGF=[surface_gf]"; #if !defined(SUBSURFACE_MOBILE) default_prefs.default_filename = system_default_filename(); diff --git a/desktop-widgets/preferences/preferences_media.cpp b/desktop-widgets/preferences/preferences_media.cpp index 7d669f69c..950173a5a 100644 --- a/desktop-widgets/preferences/preferences_media.cpp +++ b/desktop-widgets/preferences/preferences_media.cpp @@ -71,6 +71,7 @@ void PreferencesMedia::refreshSettings() ui->extractVideoThumbnails->setChecked(qPrefMedia::extract_video_thumbnails()); ui->videoThumbnailPosition->setValue(qPrefMedia::extract_video_thumbnails_position()); ui->ffmpegExecutable->setText(qPrefMedia::ffmpeg_executable()); + ui->subtitlesFormatString->setText(qPrefMedia::subtitles_format_string()); ui->auto_recalculate_thumbnails->setChecked(prefs.auto_recalculate_thumbnails); } @@ -81,5 +82,6 @@ void PreferencesMedia::syncSettings() media->set_extract_video_thumbnails(ui->extractVideoThumbnails->isChecked()); media->set_extract_video_thumbnails_position(ui->videoThumbnailPosition->value()); media->set_ffmpeg_executable(ui->ffmpegExecutable->text()); + media->set_subtitles_format_string(ui->subtitlesFormatString->text()); qPrefMedia::set_auto_recalculate_thumbnails(ui->auto_recalculate_thumbnails->isChecked()); } diff --git a/desktop-widgets/preferences/preferences_media.ui b/desktop-widgets/preferences/preferences_media.ui index 404a934b3..1e5d5d82d 100644 --- a/desktop-widgets/preferences/preferences_media.ui +++ b/desktop-widgets/preferences/preferences_media.ui @@ -170,6 +170,319 @@ + + + + Dive data as subtitles + + + + 5 + + + 5 + + + 5 + + + + + + Subtitle format string: + + + + + + + + + + + + + + + + + + true + + + Available tags: + + + + + + + + Dive time: [time] + + + + + + + + Depth: [depth] + + + + + + + + Temperature: [temperature] + + + + + + + + Ceiling: [ceiling] + + + + + + + + NDL: [ndl] + + + + + + + + TTS: [tts] + + + + + + + + RBT: [rbt] + + + + + + + + Stop time: [stoptime] + + + + + + + + Stop depth: [stopdepth] + + + + + + + + CNS: [cns] + + + + + + + + SAC: [sac] + + + + + + + + pO₂: [p_o2] + + + + + + + + pN₂: [p_n2] + + + + + + + + pHe: [p_he] + + + + + + + + O₂ pressure (rebreather): [o2_pressure] + + + + + + + + O₂ setpoint: [o2_setpoint] + + + + + + + + SCR ΔpO₂: [scr_oc_po2] + + + + + + + + MOD: [mod] + + + + + + + + EAD: [ead] + + + + + + + + END: [end] + + + + + + + + EADD: [eadd] + + + + + + + + Vertical speed: [speed] + + + + + + + + In deco (calculated): [in_deco] + + + + + + + + NDL (calculated): [ndl_calc] + + + + + + + + TTS (calculated): [tts_calc] + + + + + + + + Stop time (calculated): [stoptime_calc] + + + + + + + + Stop depth (calculated): [stopdepth_calc] + + + + + + + + Heartrate: [heartrate] + + + + + + + + Bearing: [bearing] + + + + + + + + Surface GF: [surface_gf] + + + + + + + + Current GF: [current_gf] + + + + + + + + Density: [density] + + + + + + + + ICD Warning: [icd_warning] + + + + + + + + @@ -186,6 +499,7 @@ + From 51cf9e9d1c749c053ffe640b4a60bd5c9886fec5 Mon Sep 17 00:00:00 2001 From: Victor Arvidsson Date: Thu, 24 Oct 2024 07:26:12 +0200 Subject: [PATCH 2/3] Fixed small bug in replace_all. Start_pos should of course be 0... Signed-off-by: Victor Arvidsson --- core/save-profiledata.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/save-profiledata.cpp b/core/save-profiledata.cpp index 5715c698f..893fdbe70 100644 --- a/core/save-profiledata.cpp +++ b/core/save-profiledata.cpp @@ -50,7 +50,7 @@ static std::string video_time(int secs) void replace_all(std::string& str, const std::string& old_value, const std::string& new_value) { if (old_value.empty()) return; - size_t start_pos = std::string::npos; + size_t start_pos = 0; while ((start_pos = str.find(old_value, start_pos)) != std::string::npos) { str.replace(start_pos, old_value.length(), new_value); start_pos += new_value.length(); // In case 'new_value' contains 'old_value', like replacing 'x' with 'yx' From c36e6ffa41e50f2505cfe3aba1942a0907dc322c Mon Sep 17 00:00:00 2001 From: Victor Arvidsson Date: Thu, 24 Oct 2024 07:39:45 +0200 Subject: [PATCH 3/3] Updated user-maunal with new instructions for subtitles. Signed-off-by: Victor Arvidsson --- Documentation/user-manual.txt | 43 ++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt index 8feea6b7f..b22b00b4c 100644 --- a/Documentation/user-manual.txt +++ b/Documentation/user-manual.txt @@ -1816,15 +1816,52 @@ from the _Media_ tab as well as the dive profile. By right-clicking on a video and selecting the "Save dive data as subtitles" option, a subtitles file with the same name as the video but with an ".ass" extension is created that contains -time dependent dive data (runtime, depth, temperature, NDL, TTS, surface GF) to be overlayed -with the video. The VLC video player automatically finds this file upon playing the video -and overlays the dive data. Alternatively, the ffmpeg video encoder can be used to create a +time dependent dive data to be overlayed with the video. The format of the subtitle is specified +in the _Media preferences_ (_File->Preferences->Media_). The tags used in the format string are +replaced with the actual values, if present, or removed if not present in the data. +The VLC video player automatically finds this file upon playing +the video and overlays the dive data. Alternatively, the ffmpeg video encoder can be used to create a new video file with the dive data encoded in the video stream. To do so run ffmpeg -v video.mp4 -vf "ass=video.ass" video_with_data.mp4 from the command line. You need to have the libass library installed. +Available tags are: +* Dive time: [time] +* Depth: [depth] +* Temperature: [temperature] +* Ceiling: [ceiling] +* NDL: [ndl] +* TTS: [tts] +* RBT: [rbt] +* Stop time: [stoptime] +* Stop depth: [stopdepth] +* CNS: [cns] +* SAC: [sac] +* pO₂: [p_o2] +* pN₂: [p_n2] +* pHe: [p_he] +* O₂ pressure (rebreather): [o2_pressure] +* O₂ setpoint: [o2_setpoint] +* SCR ΔpO₂: [scr_oc_po2] +* MOD: [mod] +* EAD: [ead] +* END: [end] +* EADD: [eadd] +* Vertical speed: [speed] +* In deco (calculated): [in_deco] +* NDL (calculated): [ndl_calc] +* TTS (calculated): [tts_calc] +* Stop time (calculated): [stoptime_calc] +* Stop depth (calculated): [stopdepth_calc] +* Heartrate: [heartrate] +* Bearing: [bearing] +* Surface GF: [surface_gf] +* Current GF: [current_gf] +* Density: [density] +* ICD Warning: [icd_warning] + ==== Media on an external hard disk Most underwater photographers store media on an external drive. If such a drive can be mapped by the operating system (almost always the case) the media can be directly accessed by _Subsurface_. This eases the interaction