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/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 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..893fdbe70 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 = 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' + } +} + 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 @@ +