diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 11007c7447dc149a2c9be38056c9d6f7f1ca2eea..995c5269f8cd029c30a495d528a80f0bc8a6243e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,6 @@ .common_build: before_script: - - apt update && apt -y install make autoconf qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libavutil-dev libavcodec-dev libavformat-dev libmpv-dev cmake clang clang-format gcc g++ libomp-dev + - apt update && apt upgrade && apt -y install make autoconf qt6-base-dev qt6-multimedia-dev libavutil-dev libavcodec-dev libavformat-dev libmpv-dev cmake clang clang-format gcc g++ libomp-dev script: - mkdir build && cd build - CC=${c_compiler} CXX=${cxx_compiler} cmake .. && make diff --git a/TOFIX.md b/TOFIX.md new file mode 100644 index 0000000000000000000000000000000000000000..91e3e79ca49cab030c76f3456dd20201d23737d1 --- /dev/null +++ b/TOFIX.md @@ -0,0 +1,3 @@ +[ ] Save file: do not error on save = name +[ ] Load Ass: do not fail on Default style not defined +[ ] Save file: add .vivy extension by default diff --git a/src/Lib/AbstractMediaContext.hh b/src/Lib/AbstractMediaContext.hh index 8fbaff3dfa8c9ee430c779372a6840dcf9e58246..43b327150370eb401058d77bfc055b93b5189588 100644 --- a/src/Lib/AbstractMediaContext.hh +++ b/src/Lib/AbstractMediaContext.hh @@ -84,7 +84,7 @@ protected: VIVY_LOG_CTOR() << "Codec: " << VIVY_LOG_QUOTED(codec->name) << ", id: " << codecId; VIVY_LOG_CTOR() << "Sample rate: " << codecParams->sample_rate; VIVY_LOG_CTOR() << "Bit rate: " << codecParams->bit_rate; - VIVY_LOG_CTOR() << "Channels: " << codecParams->channels; + VIVY_LOG_CTOR() << "Channels: " << codecParams->ch_layout.nb_channels; } public: diff --git a/src/Lib/Audio.cc b/src/Lib/Audio.cc index 71cddce7a508caba2c0188bed94146ddcc9b8446..fba13efeb71860bc44d120e6358faa247adf2a9a 100644 --- a/src/Lib/Audio.cc +++ b/src/Lib/Audio.cc @@ -26,14 +26,13 @@ AudioContext::getProperties() const noexcept // AudioStream class implementation // Constructor, need an AVFormat and an AVStream -AudioStream::AudioStream(const AVCodec *streamCodec, AVFormatContext *formatPtr, AVStream *streamArg, - int index) +AudioStream::AudioStream(const AVCodec *streamCodec, AVFormatContext *formatPtr, + AVStream *streamArg, int index) : Super(streamCodec, formatPtr, streamArg, index) { SwrContext *s = dataSwrContext.get(); - av_opt_set_int(s, "in_channel_count", codecContext->channels, 0); + av_opt_set_int(s, "in_channel_count", codecContext->ch_layout.nb_channels, 0); av_opt_set_int(s, "out_channel_count", 1, 0); - av_opt_set_int(s, "in_channel_layout", static_cast<int64_t>(codecContext->channel_layout), 0); av_opt_set_int(s, "out_channel_layout", AV_CH_LAYOUT_MONO, 0); av_opt_set_int(s, "in_sample_rate", codecContext->sample_rate, 0); av_opt_set_int(s, "out_sample_rate", resamplerSampleRate, 0); @@ -59,7 +58,7 @@ AudioStream::getProperties() const noexcept QJsonObject ret = Super::getProperties(); ret.insert("Sample rate", codecParams->sample_rate); ret.insert("Bit rate", static_cast<int>(codecParams->bit_rate)); - ret.insert("Channels", codecParams->channels); + ret.insert("Channels", codecParams->ch_layout.nb_channels); return ret; } @@ -71,19 +70,19 @@ AudioStream::decodeData() throw std::logic_error("audio stream is already resampled"); logDebug() << "Launch decoding of stream " << streamIndexInContext; - AVPacket packet; - av_init_packet(&packet); + AVPacket *packet = av_packet_alloc(); + ; // Iterate through frames - while (av_read_frame(dataFormat, &packet) >= 0) { + while (av_read_frame(dataFormat, packet) >= 0) { // Only decode audio - if (packet.stream_index != streamIndexInContext) { - av_packet_unref(&packet); + if (packet->stream_index != streamIndexInContext) { + av_packet_unref(packet); continue; } // Decode one frame - int response = avcodec_send_packet(codecContext.get(), &packet); + int response = avcodec_send_packet(codecContext.get(), packet); if (response < 0) { throw std::runtime_error( QStringLiteral("error n°%1 while sending a packet to the decoder") @@ -128,14 +127,14 @@ AudioStream::decodeData() const size_t frame_count = static_cast<size_t>(frame_count_int); dataPtr = reinterpret_cast<double *>( realloc(dataPtr, (dataSize + static_cast<size_t>(dataFrame->nb_samples)) * - sizeof(double))); + sizeof(double))); memcpy(dataPtr + dataSize, buffer, frame_count * sizeof(double)); dataSize += frame_count; } } dataDeleter(buffer); - av_packet_unref(&packet); + av_packet_unref(packet); } logDebug() << "Decoding data finished for stream " << streamIndexInContext << " with dataPtr " @@ -155,7 +154,7 @@ AudioStream::cleanUpData() noexcept int AudioStream::getChannels() const noexcept { - return codecContext->channels; + return codecContext->ch_layout.nb_channels; } // Get the sample rate @@ -217,3 +216,11 @@ AudioStream::getDecodedDecalage() const noexcept constexpr size_t overlap = 128; // The overlap return getDecodedChunkSize() - overlap; } + +quint64 +AudioStream::getLength() const noexcept +{ + return quint64(std::chrono::duration_cast<std::chrono::milliseconds>( + std::chrono::microseconds(dataFormat->duration)) + .count()); +} diff --git a/src/Lib/Audio.hh b/src/Lib/Audio.hh index 689de303e0b7ab6286031472cc38a80e0f8773bd..5492f8d48aedda889a8d76ee194f61d056b326a5 100644 --- a/src/Lib/Audio.hh +++ b/src/Lib/Audio.hh @@ -33,6 +33,7 @@ public: // Some getters int getChannels() const noexcept; int getSampleRate() const noexcept; + quint64 getLength() const noexcept; qint64 getBitRate() const noexcept; QJsonObject getProperties() const noexcept override; diff --git a/src/Lib/Document/CRTPSubDocument.hh b/src/Lib/Document/CRTPSubDocument.hh index 22d9bf9b0ee67fed96692593126ca77260ba593c..f7c6c454c739de366942ccee98be14f2023c3125 100644 --- a/src/Lib/Document/CRTPSubDocument.hh +++ b/src/Lib/Document/CRTPSubDocument.hh @@ -129,6 +129,7 @@ class AssSubDocument final : public CRTPSubDocument<AssDocumentType, AssSubDocum static constexpr inline bool hasContext = false; const QStringList &suffixList = Vivy::Utils::assFileSuffix; +private: void initFromPath(const QString &); explicit AssSubDocument() noexcept = default; @@ -143,6 +144,8 @@ public: const QVector<VVLib::ASSLine *> &getLines() const noexcept { return lines; } const QVector<VVLib::ASSStyle *> &getStyles() const noexcept { return styles; } + void setFilePath(QString filepath) noexcept; + private: VVLib::ASSContainer *ass_container{ nullptr }; QVector<VVLib::ASSStyle *> styles; diff --git a/src/Lib/Document/CRTPSubDocument/AssSubDocument.cc b/src/Lib/Document/CRTPSubDocument/AssSubDocument.cc index ccd73b2a350cc1efecc3977d32a5aa992f3688e1..7d42f983ca175fe90274f1f321a40a903c4de3ee 100644 --- a/src/Lib/Document/CRTPSubDocument/AssSubDocument.cc +++ b/src/Lib/Document/CRTPSubDocument/AssSubDocument.cc @@ -13,7 +13,8 @@ AssSubDocument::initFromPath(const QString &path) this->lines.push_back(VVLib::ASSContainerGetLineAt(this->ass_container, i)); } for (size_t i = 0; i < VVLib::ASSContainerGetStylesCount(this->ass_container); i += 1) { - this->styles.push_back(VVLib::ASSContainerGetStyleAt(this->ass_container, i)); + this->styles.push_back(VVLib::ASSContainerGetStyleByName( + this->ass_container, VVLib::ASSContainerGetStyleNameAt(this->ass_container, i))); } } @@ -24,8 +25,7 @@ AssSubDocument::getProperties() const noexcept { QJsonObject styleObject; for (size_t i = 0; i < VVLib::ASSContainerGetStylesCount(this->ass_container); i += 1) { - const auto view = - VVLib::ASSStyleGetName(VVLib::ASSContainerGetStyleAt(this->ass_container, i)); + const auto view = VVLib::ASSContainerGetStyleNameAt(this->ass_container, i); styleObject.insert(QString::number(i), QString::fromUtf8(view.str, static_cast<qsizetype>(view.len))); } @@ -35,3 +35,9 @@ AssSubDocument::getProperties() const noexcept { "Styles", styleObject }, }); } + +void +AssSubDocument::setFilePath(QString filepath) noexcept +{ + filePath = filepath; +} diff --git a/src/Lib/Document/VivyDocument.cc b/src/Lib/Document/VivyDocument.cc index 39a0a2ae7a6f5c6e0051370cea48a9ba607cc365..ff10185e291241450f1e36901a07043654f05640 100644 --- a/src/Lib/Document/VivyDocument.cc +++ b/src/Lib/Document/VivyDocument.cc @@ -80,11 +80,7 @@ VivyDocument::loadSaveJsonDocumentFile_ALPHA(VivyDocument *const self, const QJs !video.isNull() && !self->loadSubDocument(video.toString(), Capabilities::VideoAble)) throw std::runtime_error("Failed to load video sub document"); - if (QJsonValue internalAssSource = json[KeySubDocuments][KeyInternalAssSource]; - !internalAssSource.isNull() && internalAssSource.toBool()) { - // ASS is inside Vivy document - throw std::runtime_error("The internal ASS feature is not supported for now"); - } else if (QJsonValue ass = json[KeySubDocuments][KeySubAss]; !ass.isNull()) { + if (QJsonValue ass = json[KeySubDocuments][KeySubAss]; !ass.isUndefined()) { // ASS in its own ASS file if (!self->loadSubDocument(ass.toString(), Capabilities::AssAble)) throw std::runtime_error("Failed to load ASS sub document"); @@ -129,17 +125,8 @@ VivyDocument::getSaveJsonDocumentFile() const if (documentType & Capabilities::VideoAble) subDocumentJson.insert(KeySubVideo, videoDocument->getFilePath()); - if (documentType & Capabilities::AssAble) { - // ASS is inside Vivy document - if ((assDocument->getFilePath() == getName())) { - subDocumentJson.insert(KeyInternalAssSource, true); - throw std::runtime_error("Unsupported save of internal ASS for now"); - } - - // ASS is ints own ASS file - else - subDocumentJson.insert(KeySubAss, assDocument->getFilePath()); - } + if (documentType & Capabilities::AssAble) + subDocumentJson.insert(KeySubAss, assDocument->getFilePath()); json.insert(KeySubDocuments, subDocumentJson); ret.setObject(json); diff --git a/src/Lib/Document/VivyDocument.hh b/src/Lib/Document/VivyDocument.hh index 35f2f57bc07c13b5741a6ed2c5bda50e37daf621..d6b0a7aa1fb5bf216636bcc98301f8098454f3a8 100644 --- a/src/Lib/Document/VivyDocument.hh +++ b/src/Lib/Document/VivyDocument.hh @@ -26,6 +26,7 @@ class VivyDocument final : public AbstractDocument { DCL_VIVY_SAVE_KEY(SubAss) DCL_VIVY_SAVE_KEY(SubVideo) DCL_VIVY_SAVE_KEY(InternalAssSource) + DCL_VIVY_SAVE_KEY(HaveInternalAss) DCL_VIVY_SAVE_KEY(DocumentVersion) using LoadFunctionForVersion = @@ -82,6 +83,7 @@ public: void save() override; bool loadSubDocument(const QString &) noexcept; bool loadSubDocument(const QString &, Capabilities) noexcept; + bool loadSubDocument(const QJsonValue &, const QString &, VivyDocument::Capabilities) noexcept; // Getters std::shared_ptr<AudioSubDocument> getAudioSubDocument() const noexcept; diff --git a/src/Lib/Log.cc b/src/Lib/Log.cc index 4cede87db6a84c071f4dc94cf543c05c55015909..cf7a94859fdf95f62c27db9002201b1e758f0fb5 100644 --- a/src/Lib/Log.cc +++ b/src/Lib/Log.cc @@ -277,12 +277,14 @@ LogMessage::operator<<(const std::string_view strv) noexcept LogMessage & LogMessage::operator<<(const char *str) noexcept { - const std::size_t length = strlen(str); - for (std::size_t i = 0; (i < length) && (indexInArray < messageBufferLength - 1); - ++i, ++indexInArray) { - textBuffer[indexInArray] = str[i]; + if (str){ + const std::size_t length = strlen(str); + for (std::size_t i = 0; (i < length) && (indexInArray < messageBufferLength - 1); + ++i, ++indexInArray) { + textBuffer[indexInArray] = str[i]; + } + textBuffer[indexInArray] = '\0'; } - textBuffer[indexInArray] = '\0'; return *this; } diff --git a/src/Lib/Log.hh b/src/Lib/Log.hh index f81054e98b1d60c3038e7f29e49397d295394eb4..fc4b9b7a29a8dd12196b157d2e3b4f622529ea76 100644 --- a/src/Lib/Log.hh +++ b/src/Lib/Log.hh @@ -70,13 +70,13 @@ struct LogLevel final { Warning, Error, Critical, // Will trigger qFatal - ___MaxAndUnused + _vv_MaxAndUnused }; static const std::string_view toStdStringView(const LogLevel::Level) noexcept; private: - using Array = std::array<const std::string_view, ___MaxAndUnused>; + using Array = std::array<const std::string_view, _vv_MaxAndUnused>; // WARN: The order matters! static inline constexpr Array LevelsStringViews = { "None", "Debug", "Info", diff --git a/src/Lib/Utils.cc b/src/Lib/Utils.cc index d77a35be2479166e6b784992490de385e32b6809..a41b1f2a750a9651e49d4f6fb717ee6d8d231158 100644 --- a/src/Lib/Utils.cc +++ b/src/Lib/Utils.cc @@ -60,7 +60,7 @@ QString Utils::OsSpecificAspects::pathWithNativeSeparators(OsType osType, const QString &pathName) { if (osType == OsTypeWindows) { - const long pos = pathName.indexOf('/'); + const qsizetype pos = pathName.indexOf('/'); if (pos >= 0) { QString n = pathName; std::replace(std::begin(n) + pos, std::end(n), '/', '\\'); @@ -133,19 +133,37 @@ Utils::decodeLineToFloating(const QString &item, const QString &error) Utils::Time Utils::Time::fromString([[maybe_unused]] const QString &str) { - // QRegExp re("^(\\d+):(\\d\\d):(\\d\\d)\\.(\\d\\d)$"); + QRegularExpression re("^(\\d+):(\\d\\d):(\\d\\d)\\.(\\d\\d)$"); + + // Here the toUint is safe because the RegExp is OK + QRegularExpressionMatch match = re.match(str); + if (match.hasMatch()) { + return { .hour = match.captured(1).toUInt(), + .minute = match.captured(2).toUInt(), + .second = match.captured(3).toUInt(), + .centisecond = match.captured(4).toUInt() }; + } else { + throw std::runtime_error("The string is not of the format `H:MM:SS.cs`"); + } +} + +Utils::Time +Utils::Time::fromUInt(const quint64 &t) +{ + quint64 rem = t; + Time ret; + + ret.hour = rem / (100 * 60 * 60); + rem = rem % (100 * 60 * 60); - // // Here the toUint is safe because the RegExp is OK - // if (re.indexIn(str) != -1) - // return { .hour = re.cap(1).toUInt(), - // .minute = re.cap(2).toUInt(), - // .second = re.cap(3).toUInt(), - // .centisecond = re.cap(4).toUInt() }; + ret.minute = rem / (100 * 60); + rem = rem % (100 * 60); - // else - // throw std::runtime_error("The string is not of the format `H:MM:SS.cs`"); + ret.second = rem / (100); - throw std::runtime_error("not implemented"); + ret.centisecond = rem % 100; + + return ret; } quint64 @@ -158,8 +176,10 @@ Utils::Time::toUInt() const noexcept QString Utils::Time::toString() const noexcept { - return QString::number(hour) + ":" + QString::number(minute) + ":" + QString::number(second) + - "." + QString::number(centisecond); + return QString("%1").arg(hour, 2, 10, QChar('0')) + ":" + + QString("%1").arg(minute, 2, 10, QChar('0')) + ":" + + QString("%1").arg(second, 2, 10, QChar('0')) + "." + + QString("%1").arg(centisecond, 2, 10, QChar('0')); } QString diff --git a/src/Lib/Utils.hh b/src/Lib/Utils.hh index 16f2ffca767472c678119cfffbb054bb48afcfe9..d7482556a2d09a9c52c1441dfd50dbe7491b2036 100644 --- a/src/Lib/Utils.hh +++ b/src/Lib/Utils.hh @@ -103,7 +103,7 @@ concept StringType = requires(T str) enum class ThemeType { QtCreator, System, QssFile }; // Prefered collor for a given type -enum class VivyThemeType { Dark, Light, ___COUNT }; +enum class VivyThemeType { Dark, Light, _vv_COUNT }; } namespace Vivy::Utils @@ -214,6 +214,7 @@ struct Time final { quint64 centisecond; static Time fromString(const QString &); + static Time fromUInt(const quint64 &); QString toString() const noexcept; quint64 toUInt() const noexcept; diff --git a/src/PreCompiledHeaders.hh b/src/PreCompiledHeaders.hh index da752cf322b757709e9434a8c26f9d17ad89e48c..2fbfac97bc92fb1da58f17306f36dee095e50e5b 100644 --- a/src/PreCompiledHeaders.hh +++ b/src/PreCompiledHeaders.hh @@ -27,6 +27,7 @@ #include <iomanip> #include <filesystem> #include <bit> +#include <cfloat> /* EXT_INC_PRIVATE */ #include <mpv/client.h> diff --git a/src/Rust/VVLib.hh b/src/Rust/VVLib.hh index a3e8f3b729c42914ffd4edee86de3d13ec0b7581..87d259ceda1ff7d87f2c9b3a22df55140dc0a4dd 100644 --- a/src/Rust/VVLib.hh +++ b/src/Rust/VVLib.hh @@ -18,6 +18,9 @@ struct ASSSyllabe; /// Represents a string slice, the user may not modify this slice, never! Note that the string is /// not null terminated and may contains null bytes in it, use the len attribute to get the length /// of this string and convert it to your linking. +/// +/// # Safety +/// Note that you must always put unicode inside a string slice! struct StringSlice { uintptr_t len; @@ -59,19 +62,31 @@ StringSlice ASSLineGetStyle(const ASSLine *this_); /// The returned pointer must be freed StringSlice ASSSyllabeGetContent(const ASSSyllabe *this_); +int64_t ASSSyllabeGetDuration(const ASSSyllabe *this_); + +void ASSSyllabeSetDuration(ASSSyllabe *this_, int64_t duration); + /// Load the ASS from a file, returns nullptr if an error was encountred. ASSContainer *ASSContainerFromFile(char *path); -ASSStyle *ASSContainerGetStyleByName(ASSContainer *this_, const char *name); +/// # Safety +/// The name must be a valid CString (null terminated.) +ASSStyle *ASSContainerGetStyleByName(ASSContainer *this_, StringSlice name); -ASSStyle *ASSContainerGetStyleAt(ASSContainer *this_, uintptr_t idx); +StringSlice ASSContainerGetStyleNameAt(const ASSContainer *this_, uintptr_t idx); -uintptr_t ASSContainerGetStylesCount(ASSContainer *this_); +uintptr_t ASSContainerGetStylesCount(const ASSContainer *this_); void ASSLineSetStartTime(ASSLine *this_, int64_t time); void ASSLineSetFiniTime(ASSLine *this_, int64_t time); +int64_t ASSLineGetDuration(const ASSLine *this_); + +int64_t ASSLineGetStartTime(const ASSLine *this_); + +int64_t ASSLineGetFiniTime(const ASSLine *this_); + /// Drop the container. It is file to pass a null pointer to this function. /// # Safety /// The use must ensure that no dangling references remains... diff --git a/src/Rust/vvs_lib/src/ass.rs b/src/Rust/vvs_lib/src/ass.rs index 6fb01408756cdc6c29960db1b01a14b092a07909..b044ca43f14e6be5f1113b7dc7e8df99590a7cb5 100644 --- a/src/Rust/vvs_lib/src/ass.rs +++ b/src/Rust/vvs_lib/src/ass.rs @@ -1,25 +1,10 @@ +use crate::StringSlice; use std::{ ffi::{c_char, CStr}, ptr::NonNull, }; use vvs_ass::{ass_container_from_file, ASSContainer, ASSLine, ASSStyle, ASSSyllabe}; -/// Represents a string slice, the user may not modify this slice, never! Note that the string is -/// not null terminated and may contains null bytes in it, use the len attribute to get the length -/// of this string and convert it to your linking. -#[repr(C)] -pub struct StringSlice { - len: usize, - str: *const c_char, -} - -impl<S: AsRef<str>> From<S> for StringSlice { - fn from(value: S) -> Self { - let value = value.as_ref(); - StringSlice { len: value.len(), str: value.as_ptr() as *const _ } - } -} - /// Contains wrappers for the styles. pub mod style { use super::*; @@ -116,6 +101,16 @@ pub mod elements { StringSlice::from(&this.content) } + #[no_mangle] + pub extern "C" fn ASSSyllabeGetDuration(this: &ASSSyllabe) -> i64 { + this.fini.saturating_sub(this.start) + } + + #[no_mangle] + pub extern "C" fn ASSSyllabeSetDuration(this: &mut ASSSyllabe, duration: i64) { + this.fini = this.start.saturating_add(duration); + } + /// Load the ASS from a file, returns nullptr if an error was encountred. #[no_mangle] pub extern "C" fn ASSContainerFromFile(path: NonNull<c_char>) -> *mut ASSContainer { @@ -133,26 +128,77 @@ pub mod elements { } } + /// # Safety + /// The name must be a valid CString (null terminated.) #[no_mangle] - pub extern "C" fn ASSContainerGetStyleByName(this: &mut ASSContainer, name: *const c_char) -> *mut ASSStyle { - todo!() + pub extern "C" fn ASSContainerGetStyleByName(this: &mut ASSContainer, name: StringSlice) -> *mut ASSStyle { + let name = unsafe { name.as_str() }; + match this.styles.get_mut(name) { + Some(style) => style as *mut _, + None => { + log::error!("no sytle {name} in ass container"); + std::ptr::null_mut() + } + } } #[no_mangle] - pub extern "C" fn ASSContainerGetStyleAt(this: &mut ASSContainer, idx: usize) -> *mut ASSStyle { - todo!() + pub extern "C" fn ASSContainerGetStyleNameAt(this: &ASSContainer, idx: usize) -> StringSlice { + let mut keys: Vec<_> = this.styles.keys().collect(); + keys.sort(); + keys.get(idx) + .map(StringSlice::from) + .unwrap_or_else(|| StringSlice::from("Default")) } #[no_mangle] - pub extern "C" fn ASSContainerGetStylesCount(this: &mut ASSContainer) -> usize { - todo!() + pub extern "C" fn ASSContainerGetStylesCount(this: &ASSContainer) -> usize { + this.styles.len() } #[no_mangle] - pub extern "C" fn ASSLineSetStartTime(this: &mut ASSLine, time: i64) {} + pub extern "C" fn ASSLineSetStartTime(this: &mut ASSLine, time: i64) { + this.start = time; + this.fini = this.fini.max(time); + for syl in &this.content { + match syl.0.write() { + Ok(mut syl) => { + syl.start = syl.start.max(time); + syl.fini = syl.fini.max(time); + } + Err(err) => log::error!("{err}"), + } + } + } + + #[no_mangle] + pub extern "C" fn ASSLineSetFiniTime(this: &mut ASSLine, time: i64) { + this.fini = time; + this.start = this.start.min(time); + for syl in &this.content { + match syl.0.write() { + Ok(mut syl) => { + syl.start = syl.start.min(time); + syl.fini = syl.fini.min(time); + } + Err(err) => log::error!("{err}"), + } + } + } + + #[no_mangle] + pub extern "C" fn ASSLineGetDuration(this: &ASSLine) -> i64 { + this.fini.saturating_sub(this.start) + } + #[no_mangle] + pub extern "C" fn ASSLineGetStartTime(this: &ASSLine) -> i64 { + this.start + } #[no_mangle] - pub extern "C" fn ASSLineSetFiniTime(this: &mut ASSLine, time: i64) {} + pub extern "C" fn ASSLineGetFiniTime(this: &ASSLine) -> i64 { + this.fini + } /// Drop the container. It is file to pass a null pointer to this function. /// # Safety diff --git a/src/Rust/vvs_lib/src/lib.rs b/src/Rust/vvs_lib/src/lib.rs index 18305b8b8cc3fc613fa62a9de2bb7c053a5a0c04..55ca5a319b76da0fd1d559629bf89afc726ece35 100644 --- a/src/Rust/vvs_lib/src/lib.rs +++ b/src/Rust/vvs_lib/src/lib.rs @@ -4,4 +4,34 @@ #![allow(non_snake_case)] +use std::ffi::c_char; + pub mod ass; + +/// Represents a string slice, the user may not modify this slice, never! Note that the string is +/// not null terminated and may contains null bytes in it, use the len attribute to get the length +/// of this string and convert it to your linking. +/// +/// # Safety +/// Note that you must always put unicode inside a string slice! +#[repr(C)] +pub struct StringSlice { + len: usize, + str: *const c_char, +} + +impl StringSlice { + pub unsafe fn as_str(&self) -> &str { + std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.str as *const _, self.len)) + } +} + +impl<S> From<S> for StringSlice +where + S: AsRef<str>, +{ + fn from(value: S) -> Self { + let value = value.as_ref(); + StringSlice { len: value.len(), str: value.as_ptr() as *const _ } + } +} diff --git a/src/UI/AbstractDocumentView.hh b/src/UI/AbstractDocumentView.hh index 9a9fe00ceea3f48bd81c34aaa76f274ea844875f..a5a6210bc58157df4d70c849c0337d1bb29fa68c 100644 --- a/src/UI/AbstractDocumentView.hh +++ b/src/UI/AbstractDocumentView.hh @@ -40,6 +40,8 @@ public: bool isType(const Utils::DocumentType t) const noexcept; const QList<QAction *> &getViewsActions() const noexcept; + virtual void loadViews() noexcept = 0; + signals: void viewActionsChanged(); void documentPropertyChanged(); diff --git a/src/UI/AbstractVivyDocumentNeeder.hh b/src/UI/AbstractVivyDocumentNeeder.hh new file mode 100644 index 0000000000000000000000000000000000000000..db9e10e859ad31a542c4db4f85b4d05bcb4324b7 --- /dev/null +++ b/src/UI/AbstractVivyDocumentNeeder.hh @@ -0,0 +1,28 @@ +#pragma once + +#ifndef __cplusplus +#error "This is a C++ header" +#endif + +#include "Lib/Document/VivyDocument.hh" +#include "VivyDocumentView.hh" + +namespace Vivy +{ +class AbstractVivyDocumentNeeder { +protected: + explicit AbstractVivyDocumentNeeder(VivyDocumentView &view) + : rootVivyDocumentView(view) + , rootVivyDocumentViewConst(rootVivyDocumentView) + , rootVivyDocument(*rootVivyDocumentView.getDocument()) + , rootVivyDocumentConst(rootVivyDocument) + { + } + + VivyDocumentView &rootVivyDocumentView; + const VivyDocumentView &rootVivyDocumentViewConst; + + VivyDocument &rootVivyDocument; + const VivyDocument &rootVivyDocumentConst; +}; +} diff --git a/src/UI/DocumentViews/AssLinesModel.cc b/src/UI/DocumentViews/AssLinesModel.cc index a59240df393d5232e18c49d0444860508fa698d1..4a63c9b2a668f4f16322637966f6021685950ae8 100644 --- a/src/UI/DocumentViews/AssLinesModel.cc +++ b/src/UI/DocumentViews/AssLinesModel.cc @@ -20,11 +20,14 @@ AssLinesModel::Item::getLineText() const noexcept { QString ret; for (size_t i = 0; i < VVLib::ASSLineGetSyllabesCount(line); i += 1) { - const auto str = VVLib::ASSSyllabeGetContent(VVLib::ASSLineGetSyllabeAt(line, i)); + const auto *syl = VVLib::ASSLineGetSyllabeAt(line, i); + const auto str = VVLib::ASSSyllabeGetContent(syl); ret.append(std::string_view(str.str, str.len)); - ret.append("|"); + ret.append("("); + ret.append(QString::number(VVLib::ASSSyllabeGetDuration(syl))); + ret.append(")|"); } - ret.remove(ret.size() - 1, 1); + ret.chop(1); return ret; } @@ -135,5 +138,12 @@ Qt::ItemFlags AssLinesModel::flags(const QModelIndex &index) const noexcept { [[maybe_unused]] const Item *item = static_cast<const Item *>(index.internalPointer()); - return Qt::ItemNeverHasChildren | Qt::ItemIsSelectable | QAbstractItemModel::flags(index); + return Qt::ItemIsEditable | Qt::ItemNeverHasChildren | Qt::ItemIsSelectable | + QAbstractItemModel::flags(index); +} + +void +AssLinesModel::updateLine(int lineIndex) +{ + emit dataChanged(index(lineIndex, 0), index(lineIndex, 0)); } diff --git a/src/UI/DocumentViews/AssLinesModel.hh b/src/UI/DocumentViews/AssLinesModel.hh index 10a4b10c9427e83a31df6118382ec9c45bd22f72..0bd64902af78f522b456797cc8f0ba2e25b4aaa9 100644 --- a/src/UI/DocumentViews/AssLinesModel.hh +++ b/src/UI/DocumentViews/AssLinesModel.hh @@ -53,5 +53,8 @@ public: private: QVector<Item *> childs; const QVector<VVLib::ASSLine *> &lineRealData; + +public slots: + void updateLine(int); }; } diff --git a/src/UI/DocumentViews/AssLinesView.cc b/src/UI/DocumentViews/AssLinesView.cc index 3537a2d1c1567bb2a0bee3c2d31f80d1892f60f0..bbd75d3d151a5b18b6b728daa51c3ccd2fc8e6f8 100644 --- a/src/UI/DocumentViews/AssLinesView.cc +++ b/src/UI/DocumentViews/AssLinesView.cc @@ -99,3 +99,9 @@ AssLinesView::Delegate::initStyleOption(QStyleOptionViewItem *option, { QStyledItemDelegate::initStyleOption(option, index); } + +void +AssLinesView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + emit updateSelectedLines(selected, deselected); +} diff --git a/src/UI/DocumentViews/AssLinesView.hh b/src/UI/DocumentViews/AssLinesView.hh index e16481f2c9a1977e088c693ca4e131f90bcc8e0b..195ffcecf142369c41039aeac4ae17e3a9524aa4 100644 --- a/src/UI/DocumentViews/AssLinesView.hh +++ b/src/UI/DocumentViews/AssLinesView.hh @@ -43,7 +43,12 @@ public: private: void mouseMoveEvent(QMouseEvent *) noexcept override; +protected slots: + virtual void selectionChanged(const QItemSelection &selected, + const QItemSelection &deselected) override; + signals: void hoverIndexChanged(QModelIndex); + void updateSelectedLines(const QItemSelection &selected, const QItemSelection &deselected); }; } diff --git a/src/UI/DocumentViews/AudioVisualizer.cc b/src/UI/DocumentViews/AudioVisualizer.cc index 821c9de2ee0ce28c25c818f6a7a702419b8616dd..ae11029c8bfc76ca111f4ac3acb0ca5a14c5c598 100644 --- a/src/UI/DocumentViews/AudioVisualizer.cc +++ b/src/UI/DocumentViews/AudioVisualizer.cc @@ -1,27 +1,37 @@ #include "PreCompiledHeaders.hh" +#include "VivyApplication.hh" #include "UI/DocumentViews/AudioVisualizer.hh" +#include "UI/DocumentViews/AudioVisualizer/TimingParams.hh" +#include "UI/DocumentViews/AudioVisualizer/TimingBar.hh" +#include "UI/DocumentViews/AudioVisualizer/TimingView.hh" #include "Lib/Audio.hh" using namespace Vivy; -#define MAXPIXVALUE 7 // Some magix AV magic stuff +#define PIXMULTIPLIER 4 // Some magic AV magic stuff -AudioVisualizer::AudioVisualizer(AudioContext::StreamPtr stream, QWidget *parent) +AudioVisualizer::AudioVisualizer(AudioContext::StreamPtr stream, VivyDocumentView &rootView, + QWidget *parent) : QWidget(parent) + , AbstractVivyDocumentNeeder(rootView) + , audioStream(stream) { - if (!stream->isDecoded()) { - stream->decodeData(); + if (!audioStream->isDecoded()) { + qDebug() << "Need to decode data for stream" << audioStream->getStreamIndex(); + audioStream->decodeData(); } - double *decodedData = stream->getDecodedData(); + double *decodedData = audioStream->getDecodedData(); if (decodedData == nullptr) throw std::logic_error("the passed stream is not decoded"); - const size_t size = stream->getDecodedDataSize(); - const size_t height = stream->getDecodedChunkSize(); - const size_t decalage = stream->getDecodedDecalage(); - const size_t width = (size - height) / decalage; - uchar *pixels = new uchar[static_cast<size_t>(width * height / 2)](); + const size_t size = audioStream->getDecodedDataSize(); + const size_t height = audioStream->getDecodedChunkSize(); + const size_t halfHeight = size_t(height/2); + const size_t decalage = audioStream->getDecodedDecalage(); + const size_t width = (size - height) / decalage; + uchar *pixels = + new uchar[static_cast<size_t>(width * height / 2 + (size - height) / decalage)](); FFTSamplePtr chunkData( reinterpret_cast<FFTSample *>(av_malloc_array(2 * height, sizeof(FFTSample))), @@ -53,22 +63,56 @@ AudioVisualizer::AudioVisualizer(AudioContext::StreamPtr stream, QWidget *parent const float re = chunkData[j * 2 + 1]; const float mag = sqrtf(im * im + re * re); const size_t index = static_cast<size_t>(j * static_cast<ulong>(width) + x); - pixels[index] = static_cast<unsigned char>((mag)*MAXPIXVALUE); + pixels[index] = static_cast<unsigned char>(mag); } } - QImage img = QImage(pixels, static_cast<int>(width), static_cast<int>(height / 2), - static_cast<int>(width), QImage::Format_Grayscale8, pixelsDeleter, pixels) - .mirrored(false, true); - printSpectrum(img); + QImage img = QImage(int(width), int(halfHeight), QImage::Format_RGB888); + parallel_for (int y = 0; y < int(halfHeight); y++){ + int tempLine = y * int(width); + parallel_for (int x = 0; x < int(width); x++){ + uint pixValue = pixels[tempLine + x]; + // TODO: See if we could make this a function provided by the user if wanted, to adapt to different spectrums + uint pixValueAdjusted = uint(PIXMULTIPLIER * double(std::pow(pixValue/255.0, 0.3)) * 255); + + uchar r,g,b; + b = uchar(std::min<uint>(255, pixValueAdjusted)); + if (Q_UNLIKELY(b == 255)){ + g = std::max<uchar>(0, uchar(std::min<uint>(255, pixValueAdjusted - 255))); + r = std::max<uchar>(0, uchar(std::min<uint>(255, pixValueAdjusted - 510))); + } else { + g = r = 0; + } + img.setPixel(x, y, qRgb(r, g, b)); + } + } + img = img.mirrored(false, true); + + printSpectrum(img, audioStream); } void -AudioVisualizer::printSpectrum(QImage pixmap) noexcept +AudioVisualizer::printSpectrum(QImage pixmap, AudioContext::StreamPtr stream) noexcept { - TimingView *timer = new TimingView(pixmap, 0, this); - QVBoxLayout *layout = new QVBoxLayout; + TimingScene *timingScene = new TimingScene(pixmap, stream, rootVivyDocumentView, this); + TimingView *timingView = new TimingView(timingScene, pixmap, stream, this); - layout->addWidget(timer); + // The only that we want to take all the space is the timing scene in itself + QGridLayout *layout = new QGridLayout; + layout->addWidget(timingView, 1, 0); + layout->setColumnStretch(0, 10); + layout->addWidget(timingScene->getParams(), 1, 1); + layout->setColumnStretch(1, 0); setLayout(layout); + + connect(timingScene->getParams()->getZoomSlider(), &QSlider::valueChanged, + timingScene->getParams(), &TimingParams::setAudioWidthScale); + // FIXME: remove for now, may freeze PC + //connect(timingScene->getParams(), &TimingParams::paramsChanged, timingScene, &TimingScene::rebuildScene); + connect(timingScene->getParams()->getRebuildSceneButton(), &QPushButton::released, timingScene, + &TimingScene::rebuildScene); + connect(&rootVivyDocumentView, &VivyDocumentView::assSubDocumentChanged, timingScene, + &TimingScene::rebuildScene); } + +#undef PIXMULTIPLIER diff --git a/src/UI/DocumentViews/AudioVisualizer.hh b/src/UI/DocumentViews/AudioVisualizer.hh index 170cb89b9296eeeb247345d013b61ae52dcd0de1..9ab291c3dffc72fb17ec9e153b9e5f5a038018d9 100644 --- a/src/UI/DocumentViews/AudioVisualizer.hh +++ b/src/UI/DocumentViews/AudioVisualizer.hh @@ -6,15 +6,18 @@ #endif #include "PreCompiledHeaders.hh" -#include "UI/DocumentViews/TimingView.hh" +#include "UI/DocumentViews/AudioVisualizer/TimingView.hh" +#include "UI/AbstractVivyDocumentNeeder.hh" #include "Lib/Audio.hh" namespace Vivy { -class AudioVisualizer final : public QWidget { +class AudioVisualizer final : public QWidget, public AbstractVivyDocumentNeeder { Q_OBJECT private: + AudioContext::StreamPtr audioStream; + static constexpr inline auto fftSampleDeleter = [](FFTSample *ptr) noexcept -> void { if (ptr) av_free(ptr); @@ -23,19 +26,16 @@ private: if (ptr) av_rdft_end(ptr); }; - static constexpr inline auto pixelsDeleter = [](void *ptr) noexcept -> void { - if (ptr) - delete[](reinterpret_cast<uchar *>(ptr)); - }; using FFTSamplePtr = std::unique_ptr<FFTSample[], decltype(fftSampleDeleter)>; using RDFTContextPtr = std::unique_ptr<RDFTContext, decltype(rdftContextDeleter)>; public: - explicit AudioVisualizer(AudioContext::StreamPtr, QWidget *parent = nullptr); + AudioVisualizer(AudioContext::StreamPtr stream, VivyDocumentView &rootView, + QWidget *parent = nullptr); ~AudioVisualizer() noexcept override = default; public slots: - void printSpectrum(QImage) noexcept; + void printSpectrum(QImage, AudioContext::StreamPtr) noexcept; }; } diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingAxis.cc b/src/UI/DocumentViews/AudioVisualizer/TimingAxis.cc new file mode 100644 index 0000000000000000000000000000000000000000..60a6d8df2d9b0bd4b5ab820ae2bf4f3f230df93d --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingAxis.cc @@ -0,0 +1,78 @@ +#include "TimingAxis.hh" +#include "TimingParams.hh" + +using namespace Vivy; + +TimingAxis::TimingAxis() noexcept + : QGraphicsObject() +{ + pen = QPen(axisColour); + pen.setWidth(penWidth); +} + +QRectF +TimingAxis::boundingRect() const +{ + return QRectF(0, 10+penWidth, getTimingParam()->audioWidth() + 2 * penWidth, + -2*getTimingParam()->axisHeight() - penWidth); +} + +void +TimingAxis::onParamsChanged() +{ + qsizetype nbAvailableTicks = availableTicks.size(); + int minorTicksIndex = 0; + int length = getTimingParam()->audioLength(); + int width = getTimingParam()->audioWidth(); + if (width != 0 && length != 0) { + for (minorTicksIndex = 0; minorTicksIndex < nbAvailableTicks; minorTicksIndex++) { + if (width * availableTicks[minorTicksIndex] / length >= minBetweenMinor) { + break; + } + } + } + + prepareGeometryChange(); + if (minorTicksIndex < nbAvailableTicks - 1) { + minorTicks = availableTicks[minorTicksIndex]; + majorTicks = availableTicks[minorTicksIndex + 1]; + } else { + minorTicks = 0; + majorTicks = availableTicks[minorTicksIndex]; + } + getTimingParam()->setTicks(minorTicks, majorTicks); +} + +void +TimingAxis::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + int audioLength = getTimingParam()->audioLength(); + if (audioLength == 0) + return; + + int yText = -majorTicksHeight - timeDigitsMargin; + + painter->setPen(pen); + painter->drawLine(0, 0, audioLength, 0); + + /* + * We may want to calculate width/audioLength + * and audioLength/width to have just a multiplication + * to do to get the position of each tick + * + * It should be faster at the cost of precision : + * should look whether this precision loss is negligible or not + */ + for (int i = 0; i < audioLength; i += majorTicks) { + int pos = getTimingParam()->posFromMs(i); + painter->drawText(QPoint(pos, yText), getTimingParam()->printMajorTicks(i)); + painter->drawLine(pos, 0, pos, -majorTicksHeight); + } + if (minorTicks > 0) { + for (int i = 0; i < audioLength; i += minorTicks) { + int pos = getTimingParam()->posFromMs(i); + if (Q_LIKELY(fmod(i, majorTicks) < DBL_EPSILON)) + painter->drawLine(pos, 0, pos, -minorTicksHeight); + } + } +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingAxis.hh b/src/UI/DocumentViews/AudioVisualizer/TimingAxis.hh new file mode 100644 index 0000000000000000000000000000000000000000..266c0431373e7e2df91b388cfe1e3d5998d642c0 --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingAxis.hh @@ -0,0 +1,50 @@ +#ifndef VIVY_TIMING_AXIS_H +#define VIVY_TIMING_AXIS_H + +#ifndef __cplusplus +#error "This is a C++ header" +#endif + +#include "PreCompiledHeaders.hh" +#include <cmath> + +namespace Vivy +{ +class TimingAxis final : public QGraphicsObject { + Q_OBJECT + +public: + explicit TimingAxis() noexcept; + ~TimingAxis() noexcept override = default; + + QRectF boundingRect() const override; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + +private: + static inline constexpr QColor axisColour = QColor(255, 220, 220); + QPen pen; + int penWidth{ 1 }; + + /* + * We use ints here because + * qPainter->drawLine() and qPainter->drawText() + * restrict us to ints anyways + */ + QVector<int> availableTicks = { 10, 100, 1000, 10000, 60000, 3600000, 86400000 }; + int minBetweenMinor{ 10 }; + int minorTicks; + int majorTicks; + + int minorTicksHeight{ 3 }; + int majorTicksHeight{ 7 }; + int timeDigitsHeight{ 20 }; + int timeDigitsMargin{ 3 }; + +protected: +public slots: + void onParamsChanged(); +}; + +} + +#endif // VIVY_TIMING_AXIS_H diff --git a/src/UI/DocumentViews/TimingBar.cc b/src/UI/DocumentViews/AudioVisualizer/TimingBar.cc similarity index 96% rename from src/UI/DocumentViews/TimingBar.cc rename to src/UI/DocumentViews/AudioVisualizer/TimingBar.cc index bc035eedf7b0eebd93bc58e0b8f3f8a8cfa98f4b..2a25dca906148ca78f25eb4fd352ca5afb769adf 100644 --- a/src/UI/DocumentViews/TimingBar.cc +++ b/src/UI/DocumentViews/AudioVisualizer/TimingBar.cc @@ -1,5 +1,5 @@ #include "PreCompiledHeaders.hh" -#include "UI/DocumentViews/TimingBar.hh" +#include "TimingBar.hh" using namespace Vivy; diff --git a/src/UI/DocumentViews/TimingBar.hh b/src/UI/DocumentViews/AudioVisualizer/TimingBar.hh similarity index 100% rename from src/UI/DocumentViews/TimingBar.hh rename to src/UI/DocumentViews/AudioVisualizer/TimingBar.hh diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingCursor.cc b/src/UI/DocumentViews/AudioVisualizer/TimingCursor.cc new file mode 100644 index 0000000000000000000000000000000000000000..1ef531d09a7a1ebf650011add8a80e452c4681f7 --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingCursor.cc @@ -0,0 +1,43 @@ +#include "TimingCursor.hh" +#include "TimingParams.hh" + +using namespace Vivy; + +TimingCursor::TimingCursor() + : QGraphicsItem() +{ + setZValue(Z_CURSOR_BAR); +} + +QRectF +TimingCursor::boundingRect() const +{ + return QRectF(-maxWidth, 0, maxWidth * 2, getTimingParam()->audioHeight()); +} + +void +TimingCursor::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + painter->drawLine(0, 0, 0, getTimingParam()->audioHeight()); + + QRectF textRectangle = textRect; + QPointF sceneLeft = mapFromScene(QPointF(0, 0)); + QPointF sceneRight = mapFromScene(QPointF(scene()->sceneRect().right(), 0)); + getTimingParam()->adjustFlip(&textRectangle, sceneLeft.rx(), sceneRight.rx()); + + //painter->drawRect(textRectangle); + painter->drawText(textRectangle, Qt::AlignHCenter | Qt::AlignTop, cursorTime); +} + +void +TimingCursor::setTime(QString str) noexcept +{ + cursorTime = str; +} + +void +TimingCursor::onParamsChanged() +{ + textRect = QRectF(0, 10, maxWidth, + getTimingParam()->audioHeight() - 20); // TODO : remove 10/20 magic numbers +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingCursor.hh b/src/UI/DocumentViews/AudioVisualizer/TimingCursor.hh new file mode 100644 index 0000000000000000000000000000000000000000..a9e2c7ca5a6472c644d3952f810305100736176d --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingCursor.hh @@ -0,0 +1,31 @@ +#pragma once + +#include "PreCompiledHeaders.hh" + +namespace Vivy +{ +class TimingCursor final : public QGraphicsItem { +public: + explicit TimingCursor(); + + QRectF boundingRect() const override; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + +private: + QString cursorTime; + // FIXME + // Forced to define a "max-width" for the text, + // as it doesn't seem possible to get the width of the text. + // We should try drawing it as a QGraphicsTextItem and see if + // boundingRect()->width() is appropriate + qreal maxWidth{ 50 }; // TODO: proper variable + QRectF textRect; + +public: + void setTime(QString str) noexcept; + +public slots: + void onParamsChanged(); +}; + +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingLine.cc b/src/UI/DocumentViews/AudioVisualizer/TimingLine.cc new file mode 100644 index 0000000000000000000000000000000000000000..3e6045647424f37447c76f98dcc2402ea66e595d --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingLine.cc @@ -0,0 +1,180 @@ +#include "TimingLine.hh" +#include "TimingParams.hh" + +using namespace Vivy; + +TimingLine::TimingLine(VVLib::ASSLine _line[], int index, QGraphicsItem *parent) + : QGraphicsObject(parent) + , line(_line) + , lineIndex(index) +{ +} + +QRectF +TimingLine::boundingRect() const +{ + const auto end = VVLib::ASSLineGetFiniTime(line); + const auto start = VVLib::ASSLineGetStartTime(line); + return QRectF(tempOffset, 0, getTimingParam()->posFromMs(int(10 * (end - start))), + getTimingParam()->audioHeight()); +} + +void +TimingLine::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + const auto end = VVLib::ASSLineGetFiniTime(line); + const auto start = VVLib::ASSLineGetStartTime(line); + painter->fillRect(QRectF(tempOffset, 0, getTimingParam()->posFromMs(int(10 * (end - start))), + getTimingParam()->audioHeight()), + QColor(0, 255, 255, 50)); +} + +void +TimingLine::timingSeparatorHasChanged(int sylIndex, qreal x) +{ +} + +void +TimingLine::sepEnterPress(int sepIndex) +{ +} + +void +TimingLine::sepExitPress(int sepIndex) +{ + if (qAbs(tempOffset) <= DBL_EPSILON) { + prepareGeometryChange(); + if (sepIndex == 0) { + moveBy(tempOffset, 0); + + timingSyls[sepIndex]->setPos(0, 0); + seps[sepIndex]->silentlySetPos(0, 0); + + for (int i = 1; i < timingSyls.size(); i++) { + timingSyls[i]->moveBy(-tempOffset, 0); + } + for (int i = 1; i < seps.size(); i++) { + seps[i]->silentlyMoveBy(-tempOffset, 0); + } + + VVLib::ASSLineSetStartTime( + line, qint64(getTimingParam()->msFromPos(mapToScene(0, 0).x()) / 10)); + } + } + + tempOffset = 0; +} + +qreal +TimingLine::requestMove(int sepIndex, qreal x) +{ + QRectF sceneRect = mapRectFromScene(scene()->sceneRect()); + const auto syls_count = VVLib::ASSLineGetSyllabesCount(line); + qreal mini, maxi, given = x; + + if (sepIndex <= 0) { + prepareGeometryChange(); + + mini = sceneRect.left(); + maxi = sepIndex < syls_count - 1 + ? timingSyls[sepIndex + 1]->pos().x() + : getTimingParam()->posFromMs(int(VVLib::ASSLineGetDuration(line) * 10)); + + given = qBound(mini, given, maxi); + tempOffset = given; + + qint64 dur1 = qint64(getTimingParam()->msFromPos(int(seps[1]->pos().x() - given)) / 10); + VVLib::ASSSyllabeSetDuration(VVLib::ASSLineGetSyllabeAt(line, 0), dur1); + timingSyls[0]->setLen(dur1); + timingSyls[0]->setPos(given, 0); + VVLib::ASSLineSetStartTime( + line, static_cast<int64_t>( + getTimingParam()->msFromPos(mapToScene(seps[0]->pos().x(), 0).x()) / 10)); + } + + else if (sepIndex >= syls_count) { + prepareGeometryChange(); + + mini = timingSyls[sepIndex - 1]->pos().x(); + maxi = sceneRect.right(); + given = qBound(mini, given, maxi); + + qint64 dur2 = qint64( + getTimingParam()->msFromPos(int(given - timingSyls[sepIndex - 1]->pos().x())) / 10); + VVLib::ASSSyllabeSetDuration(VVLib::ASSLineGetSyllabeAt(line, sepIndex - 1), dur2); + timingSyls[sepIndex - 1]->setLen(dur2); + VVLib::ASSLineSetFiniTime(line, VVLib::ASSLineGetStartTime(line) + + qint64(getTimingParam()->msFromPos(int(given)) / 10)); + } + + else { + mini = timingSyls[sepIndex - 1]->pos().x(); + maxi = sepIndex < syls_count - 1 + ? timingSyls[sepIndex + 1]->pos().x() + : getTimingParam()->posFromMs(int(VVLib::ASSLineGetDuration(line) * 10)); + given = qBound(mini, given, maxi); + + qint64 sumDur = + VVLib::ASSSyllabeGetDuration(VVLib::ASSLineGetSyllabeAt(line, sepIndex)) + + VVLib::ASSSyllabeGetDuration(VVLib::ASSLineGetSyllabeAt(line, sepIndex - 1)); + qint64 dur1 = qint64( + getTimingParam()->msFromPos(int(given) - int(timingSyls[sepIndex - 1]->pos().x())) / + 10); + dur1 = qMin(dur1, sumDur); + qint64 dur2 = sumDur - dur1; + + VVLib::ASSSyllabeSetDuration(VVLib::ASSLineGetSyllabeAt(line, sepIndex - 1), dur1); + VVLib::ASSSyllabeSetDuration(VVLib::ASSLineGetSyllabeAt(line, sepIndex), dur2); + + timingSyls[sepIndex - 1]->setLen(dur1); + timingSyls[sepIndex]->setPos(given, 0); + timingSyls[sepIndex]->setLen(dur2); + } + + emit lineChanged(lineIndex); + return given; +} + +#define CONNECT_SEP(sep) \ + connect(sep, &TimingSeparator::positionChanged, this, &TimingLine::timingSeparatorHasChanged); \ + connect(sep, &TimingSeparator::enterPress, this, &TimingLine::sepEnterPress); \ + connect(sep, &TimingSeparator::exitPress, this, &TimingLine::sepExitPress); +void +TimingLine::onParamsChanged() +{ + setPos(getTimingParam()->posFromMs(int(VVLib::ASSLineGetStartTime(line) * 10)), + getTimingParam()->axisHeight()); + int currentTime = 0; + int endSyl = 0; + size_t i; + + setZValue(Z_LINE_BACKGROUND); + + TimingSeparator *timingSeparatorStart = + new TimingSeparator(currentTime, 0, TimingSeparator::SeparatorStyle::Start, this); + seps.append(timingSeparatorStart); + CONNECT_SEP(timingSeparatorStart); + + for (i = 0; i < VVLib::ASSLineGetSyllabesCount(line); i += 1) { + auto *syllabe = VVLib::ASSLineGetSyllabeAt(line, i); + endSyl = currentTime + 10 * int(VVLib::ASSSyllabeGetDuration(syllabe)); + + TimingSyl *timingSyl = new TimingSyl(syllabe, currentTime, this); + timingSyls.append(timingSyl); + + if (i != 0) { + TimingSeparator *timingSeparator = + new TimingSeparator(currentTime, i, TimingSeparator::SeparatorStyle::Middle, this); + seps.append(timingSeparator); + CONNECT_SEP(timingSeparator); + } + + currentTime = endSyl; + } + + TimingSeparator *timingSeparatorEnd = + new TimingSeparator(currentTime, i, TimingSeparator::SeparatorStyle::End, this); + seps.append(timingSeparatorEnd); + CONNECT_SEP(timingSeparatorEnd); +} +#undef CONNECT_SEP diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingLine.hh b/src/UI/DocumentViews/AudioVisualizer/TimingLine.hh new file mode 100644 index 0000000000000000000000000000000000000000..5b9df2826979a0fa21fd9733c5310eae0f47f296 --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingLine.hh @@ -0,0 +1,40 @@ +#pragma once + +#include <PreCompiledHeaders.hh> + +#include "Rust/VVLib.hh" +#include "TimingSeparator.hh" +#include "TimingSyl.hh" + +namespace Vivy +{ +class TimingLine final : public QGraphicsObject { + Q_OBJECT +public: + explicit TimingLine(VVLib::ASSLine[], int, QGraphicsItem * = nullptr); + + QRectF boundingRect() const override; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + +private: + VVLib::ASSLine *const line; + QVector<TimingSeparator *> seps; + QVector<TimingSyl *> timingSyls; + int lineIndex; + + qreal tempOffset{ 0 }; + +public: + qreal requestMove(int, qreal); + +signals: + void lineChanged(int); + +public slots: + void timingSeparatorHasChanged(int, qreal); + void sepEnterPress(int); + void sepExitPress(int); + void onParamsChanged(); +}; + +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingParams.cc b/src/UI/DocumentViews/AudioVisualizer/TimingParams.cc new file mode 100644 index 0000000000000000000000000000000000000000..b18c1c00812f4e4535ef984441d6702ca8a0246d --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingParams.cc @@ -0,0 +1,120 @@ +#include "TimingParams.hh" + +using namespace Vivy; + +TimingParams::TimingParams(QWidget *parent) noexcept + : QWidget(parent) +{ + QVBoxLayout *vlayout = new QVBoxLayout; + QHBoxLayout *hlayout = new QHBoxLayout; + vlayout->addLayout(hlayout); + + zoomSlider = std::make_unique<QSlider>(new QSlider(Qt::Vertical)); + if (QSlider *slider = zoomSlider.get()) { + slider->setMinimum(1); + slider->setMaximum(100); + slider->setSingleStep(0); + slider->setPageStep(0); + slider->setValue(0); + // FIXME: set to false for now as it may be intensive while rebuilding the scene isn't optimized + slider->setTracking(false); + hlayout->addWidget(slider); + } + sensibilitySlider = std::make_unique<QSlider>(new QSlider(Qt::Vertical)); + if (QSlider *slider = sensibilitySlider.get()) { + slider->setMinimum(0); + slider->setMaximum(100); + slider->setSingleStep(0); + slider->setPageStep(0); + slider->setValue(0); + // FIXME: set to false for now as it may be intensive while rebuilding the scene isn't optimized + slider->setTracking(false); + hlayout->addWidget(slider); + } + + rebuildSceneButton = std::make_unique<QPushButton>(new QPushButton(QString("Rebuild scene"))); + if (QPushButton *button = rebuildSceneButton.get()) { + vlayout->addWidget(button); + } + + setLayout(vlayout); +} + +QSlider * +TimingParams::getZoomSlider() const noexcept +{ + return zoomSlider.get(); +} + +QSlider * +TimingParams::getSensibilitySlider() const noexcept +{ + return sensibilitySlider.get(); +} + +QPushButton * +TimingParams::getRebuildSceneButton() const noexcept +{ + return rebuildSceneButton.get(); +} + +void +TimingParams::adjustFlip(QRectF *rect, qreal left, qreal right) noexcept +{ + if (Q_UNLIKELY(right - rect->right() < 0)) { + qreal w = rect->width(); + rect->adjust(-w, 0, -w, 0); + } +} + +void +TimingParams::adjustTranslate(QRectF *rect, qreal left, qreal right) noexcept +{ + qreal d; + qreal hw = rect->width() / 2; + if (Q_UNLIKELY((d = hw + left) > 0)) { + rect->adjust(d, 0, d, 0); + } + if (Q_UNLIKELY((d = right - hw) < 0)) { + rect->adjust(d, 0, d, 0); + } +} + +QString +TimingParams::printMajorTicks(int t) noexcept +{ + // TODO : adapt to different ticks (not everytime with "." first) + QString ret( + QStringLiteral("%1").arg(t % 60000 / 1000, t >= 60000 ? 2 : 1, 10, QLatin1Char('0')) + + QString(".") + QString::number(t % 1000 / m_majorTicks)); + if (t >= 60000) + ret.prepend(QString::number(t / 60000) + QString(":")); + if (t >= 360000) + ret.prepend(QString::number(t / 360000) + QString(":")); + return ret; +} + +QString +TimingParams::printCursor(int t) noexcept +{ + int absT = abs(t); + // TODO : adapt to different ticks (not everytime with "." first) + QString ret( + QString::fromLatin1(t < 0 ? "-" : "" ) + + QStringLiteral("%1").arg(absT % 60000 / 1000, absT >= 60000 ? 2 : 1, 10, QLatin1Char('0')) + + QString(".") + QString::number(absT % 1000 / m_minorTicks)); + if (absT >= 60000) + ret.prepend(QString::number(absT / 60000) + QString(":")); + if (absT >= 360000) + ret.prepend(QString::number(absT / 360000) + QString(":")); + return ret; +} + +void +TimingParams::updateRatios() noexcept +{ + m_wl = m_audioLength != 0 ? qreal(m_audioWidth) / qreal(m_audioLength) : 0; + m_lw = m_audioWidth != 0 ? qreal(m_audioLength) / qreal(m_audioWidth) : 0; + + emit paramsChanged(); +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingParams.hh b/src/UI/DocumentViews/AudioVisualizer/TimingParams.hh new file mode 100644 index 0000000000000000000000000000000000000000..332d5b92d0842ed18d5a21e30966b531d493ae66 --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingParams.hh @@ -0,0 +1,110 @@ +#ifndef VIVY_TIMING_PARAMS_H +#define VIVY_TIMING_PARAMS_H + +#ifndef __cplusplus +#error "This is a C++ header" +#endif + +#include "PreCompiledHeaders.hh" +#include "Lib/Utils.hh" +#include "TimingScene.hh" + +#define getTimingScene() static_cast<TimingScene *>(scene()) +#define getTimingParam() getTimingScene()->getParams() + +#define Z_SPECTER -1000 +#define Z_AXIS 10 +#define Z_LINE_BACKGROUND -100 +#define Z_SEPARATOR_START_END 1000 +#define Z_SEPARATOR_MIDDLE 100 +#define Z_LINE_SYL_TEXT 50 +#define Z_CURSOR_BAR 500 + +namespace Vivy +{ +class TimingParams final : public QWidget { + Q_OBJECT + +public: + explicit TimingParams(QWidget * = nullptr) noexcept; + +private: + std::unique_ptr<QSlider> zoomSlider; + std::unique_ptr<QSlider> sensibilitySlider; + std::unique_ptr<QPushButton> rebuildSceneButton; + +public: + QSlider *getZoomSlider() const noexcept; + QSlider *getSensibilitySlider() const noexcept; + QPushButton *getRebuildSceneButton() const noexcept; + +private: + // Managed by TimingAxis + int m_majorTicks{1}, m_minorTicks{1}; + // Managed by TimingScene + int m_axisHeight{1}, m_audioWidth{1}, m_audioLength{1}, m_audioHeight{1}; + // Self-managed + qreal m_wl{0}, m_lw{0}; + qreal m_audioWidthScale{1}; + +public: + void adjustTranslate(QRectF *, qreal, qreal) noexcept; + void adjustFlip(QRectF *, qreal, qreal) noexcept; + + QString printMajorTicks(int) noexcept; + QString printCursor(int) noexcept; + + inline void setTicks(int t, int T) noexcept + { + m_minorTicks = t; + m_majorTicks = T; + } + inline void setAxisHeight(int x) { m_axisHeight = x; } + inline void setAudioHeight(int x) { m_audioHeight = x; } + inline void setAudioWidth(int x) + { + m_audioWidth = int(x * m_audioWidthScale); + updateRatios(); + } + inline void setAudioLength(int x) + { + m_audioLength = x; + updateRatios(); + } + + inline int axisHeight() { return m_axisHeight; } + inline int audioHeight() { return m_audioHeight; } + inline int audioWidth() { return m_audioWidth; } + inline int audioLength() { return m_audioLength; } + + inline void setAudioWidthScale(int s) + { + m_audioWidthScale = 1 + 0.01 * s; + setAudioWidth(m_audioWidth); + updateRatios(); + } + + template <typename T> T posFromMs(T t) noexcept { + return T(t * m_wl); + } + template <typename T> T posFromMs(T t, int audioWidth, int audioLength) noexcept { + return audioLength == 0 ? 0 : T(qint64(t) * qint64(audioWidth) / audioLength); + } + + template <typename T> T msFromPos(T x) noexcept { + return T(x * m_lw); + } + template <typename T> T msFromPos(T x, int audioLength, int audioWidth) noexcept { + return audioWidth == 0 ? 0 : int(qint64(x) * qint64(audioLength) / audioWidth); + } + +private: + void updateRatios() noexcept; + +signals: + void paramsChanged(); +}; + +} + +#endif // VIVY_TIMING_PARAMS_H diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingScene.cc b/src/UI/DocumentViews/AudioVisualizer/TimingScene.cc new file mode 100644 index 0000000000000000000000000000000000000000..d4b80f0ef7e9e3859390e167feab22160822d24c --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingScene.cc @@ -0,0 +1,201 @@ +#include "PreCompiledHeaders.hh" +#include "Lib/Utils.hh" +#include "TimingScene.hh" +#include "TimingAxis.hh" +#include "TimingLine.hh" +#include "TimingParams.hh" +#include "TimingCursor.hh" + +#include <QGraphicsLineItem> +#include <QGraphicsPixmapItem> +#include <QGraphicsView> +#include <QLabel> +#include <QMessageBox> +#include <QMouseEvent> +#include <QPainter> +#include <QGraphicsSceneMouseEvent> +#include <QScrollArea> +#include <QScrollBar> +#include <QVBoxLayout> + +using namespace Vivy; + +TimingScene::TimingScene(QImage img_, AudioContext::StreamPtr stream, VivyDocumentView &rootView, + QWidget *parent) noexcept + : QGraphicsScene(parent) + , AbstractVivyDocumentNeeder(rootView) + , img(img_) + , audioStream(stream) +{ + params = new TimingParams(); + // We put a common origin for the items at the bottom of the axis + params->setAxisHeight(30); // TODO: remove magic number + + rebuildScene(); +} + +void +TimingScene::mousePressEvent(QGraphicsSceneMouseEvent *event) noexcept +{ + QPointF pos = event->scenePos(); + QGraphicsItem *got = itemAt(pos, QTransform()); + + if (currentLine && (got == nullptr || got == backgroundImg)) [[likely]] { + // Handle the different cases + if (timingMode == TimingMode::Line) + handleMousePressEventLine(event, currentLine); + else if (timingMode == TimingMode::Syl) + handleMousePressEventSyl(event, currentLine); + else if (timingMode == TimingMode::Char) + handleMousePressEventChar(event, currentLine); + } + + QGraphicsScene::mousePressEvent(event); +} + +void +TimingScene::handleMousePressEventLine(QGraphicsSceneMouseEvent *event, VVLib::ASSLine p[]) noexcept +{ + const qint64 time = timeFromPos(event->scenePos().x()); + if (const auto &btn = event->button(); btn == Qt::LeftButton) { + VVLib::ASSLineSetStartTime(p, static_cast<int64_t>(time)); + } else if (btn == Qt::RightButton) { + VVLib::ASSLineSetFiniTime(p, static_cast<int64_t>(time)); + } +} + +void +TimingScene::handleMousePressEventSyl(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept +{ +} + +void +TimingScene::handleMousePressEventChar(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept +{ +} + +QGraphicsPixmapItem * +TimingScene::bg() noexcept +{ + return backgroundImg; +} + +void +TimingScene::updateSelectedLines(const QItemSelection &selected, const QItemSelection &deselected) +{ + auto indexes = selected.indexes(); + for (int i = 0; i < indexes.size(); ++i) { + timingLines[indexes[i].row()]->setVisible(true); + } + + indexes = deselected.indexes(); + for (int i = 0; i < indexes.size(); ++i) { + timingLines[indexes[i].row()]->setVisible(false); + } +} + +void +TimingScene::updateScene(QImage img_, AudioContext::StreamPtr stream_) +{ + img = img_; + audioStream = stream_; + rebuildScene(); +} + +void +TimingScene::rebuildScene() +{ + for (auto &item : items()) { + removeItem(item); + delete (item); + } + timingLines.clear(); + + QPixmap pixmap(QPixmap::fromImage(img)); + backgroundImg = addPixmap(pixmap); + backgroundImg->setZValue(Z_SPECTER); + backgroundImg->setPos(0, params->axisHeight()); + + params->setAudioHeight(pixmap.height()); + params->setAudioWidth(img.width()); + params->setAudioLength(int(audioStream->getLength())); + + ax = new TimingAxis(); + addItem(ax); + ax->onParamsChanged(); + ax->setZValue(Z_AXIS); + ax->setPos(0, params->axisHeight()); + + cursor = new TimingCursor(); + addItem(cursor); + cursor->onParamsChanged(); + cursor->setPos(0, params->axisHeight()); + + // Freeze the scene boundaries + QRectF sRect = sceneRect(); + setSceneRect(QRectF(0, sRect.top(), params->audioWidth(), sRect.height()).normalized()); + + if (auto assDocument = rootVivyDocument.getAssSubDocument()) { + QVector<VVLib::ASSLine *> lines = assDocument->getLines(); + for (int i = 0; i < lines.size(); ++i) { + if (auto line = lines.at(i)) { + TimingLine *l = new TimingLine(line, i); + addItem(l); + l->onParamsChanged(); + l->setVisible(false); + timingLines.append(l); + if (auto assLinesModel = rootVivyDocumentView.getAssLinesModel()) { + connect(l, &TimingLine::lineChanged, assLinesModel, &AssLinesModel::updateLine); + } + } + } + } + + if (auto assLinesView = rootVivyDocumentView.getAssLinesView()) { + connect(assLinesView, &AssLinesView::updateSelectedLines, this, + &TimingScene::updateSelectedLines); + } +} + +TimingParams * +TimingScene::getParams() +{ + return params; +} + +TimingAxis * +TimingScene::getAxis() +{ + return ax; +} + +void +TimingScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) noexcept +{ + int x = int(mouseEvent->scenePos().rx()); + cursor->setPos(x, 30); + cursor->setTime(params->printCursor(params->msFromPos(x))); + QGraphicsScene::mouseMoveEvent(mouseEvent); +} + +quint64 +TimingScene::timeFromPos(quint64 x) const noexcept +{ + if (const quint64 w = quint64(width()); x <= 0 || w <= 0) { + qCritical() << "Try avoid possible divide by zero in the time from position"; + return 0; + } else { + return x * audioStream->getLength() / w; + } +} + +quint64 +TimingScene::posFromTime(quint64 t) const noexcept +{ + if (const quint64 w = quint64(width()); t <= 0 || w <= 0) { + qCritical() << "Try avoid possible divide by zero in the position from time"; + return 0; + } else { + return 10 * t * w / audioStream->getLength(); + } +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingScene.hh b/src/UI/DocumentViews/AudioVisualizer/TimingScene.hh new file mode 100644 index 0000000000000000000000000000000000000000..e9102fcab6312106b9ed45384f53a97fc34058b2 --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingScene.hh @@ -0,0 +1,74 @@ +#pragma once + +#include "PreCompiledHeaders.hh" +#include "Rust/VVLib.hh" +#include "Lib/Utils.hh" +#include "Lib/Audio.hh" +#include "UI/VivyDocumentView.hh" +#include "UI/DocumentViews/AssLinesView.hh" +#include "UI/AbstractVivyDocumentNeeder.hh" + +namespace Vivy +{ +class TimingParams; +class TimingBar; +class TimingAxis; +class TimingCursor; +class TimingLine; + +class TimingScene final : public QGraphicsScene, public AbstractVivyDocumentNeeder { + Q_OBJECT + +public: + enum class TimingMode { Line = 0, Syl = (1 << 1), Char = (1 << 2) }; + Q_DECLARE_FLAGS(TimingModes, TimingMode) + +public: + static inline constexpr QColor startColour = QColor(127, 0, 127); + static inline constexpr QColor endColour = QColor(0, 127, 0); + + //explicit TimingScene(QWidget *parent = nullptr) noexcept; + explicit TimingScene(QImage, AudioContext::StreamPtr, VivyDocumentView &, + QWidget * = nullptr) noexcept; + +private: + QGraphicsPixmapItem *backgroundImg{ nullptr }; + QImage img; + TimingCursor *cursor; + AudioContext::StreamPtr audioStream; + VVLib::ASSLine *currentLine{ nullptr }; + TimingMode timingMode{ TimingMode::Line }; + + // FIXME: add destructors + TimingParams *params; + TimingAxis *ax; + + QVector<TimingLine *> timingLines; + +public: + QGraphicsPixmapItem *bg() noexcept; + void mousePressEvent(QGraphicsSceneMouseEvent *event) noexcept override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) noexcept override; + + TimingAxis *getAxis(); + TimingParams *getParams(); + +private: + void handleMousePressEventLine(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept; + void handleMousePressEventSyl(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept; + void handleMousePressEventChar(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept; + +private: + quint64 timeFromPos(quint64 x) const noexcept; + quint64 posFromTime(quint64 t) const noexcept; + +public slots: + void updateScene(QImage, AudioContext::StreamPtr); + void updateSelectedLines(const QItemSelection &selected, const QItemSelection &deselected); + void rebuildScene(); +}; + +} + +//Only if combinaisons of mode is allowed, it shouldn't be here +//Q_DECLARE_OPERATORS_FOR_FLAGS(TimingScene::TimingModes) diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.cc b/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.cc new file mode 100644 index 0000000000000000000000000000000000000000..2f0248efea2665321dd6626df663ea6ccbcd5f39 --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.cc @@ -0,0 +1,112 @@ +#include "TimingSeparator.hh" +#include "TimingParams.hh" +#include "TimingLine.hh" + +using namespace Vivy; + +TimingSeparator::TimingSeparator(int time, int index, SeparatorStyle style_, TimingLine *parent) + : QGraphicsObject(parent) + , style(style_) + , sepIndex(index) + , parentTimingLine(parent) +{ + setPos(getTimingParam()->posFromMs(time), 0); + + setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemSendsGeometryChanges); + setAcceptHoverEvents(true); + setCursor(Qt::PointingHandCursor); + + switch (style) { + case SeparatorStyle::Start: + pen = QPen(QColor(0, 0, 255)); + setZValue(Z_SEPARATOR_START_END); + break; + case SeparatorStyle::Middle: + pen = QPen(QColor(180, 0, 180)); + setZValue(Z_SEPARATOR_MIDDLE); + break; + case SeparatorStyle::End: + pen = QPen(QColor(255, 0, 0)); + setZValue(Z_SEPARATOR_START_END); + break; + } + // Putting even-size width seems to be undefined behaviour for pixel drawing : stick to odd + pen.setWidth(1); +} + +QRectF +TimingSeparator::boundingRect() const +{ + return QRectF(-widthPaw, 0, 2 * widthPaw, getTimingParam()->audioHeight()); +} + +void +TimingSeparator::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + painter->setPen(pen); + int height = getTimingParam()->audioHeight(); + + int top = 1 + pen.width() / 2; + int bottom = height - 1 - pen.width() / 2; + + painter->drawLine(0, top, 0, bottom); + + switch (style) { + case SeparatorStyle::Start: + painter->drawLine(0, top, widthPaw, top); + painter->drawLine(0, bottom, widthPaw, bottom); + break; + case SeparatorStyle::Middle: + painter->drawLine(-widthPaw, top, widthPaw, top); + painter->drawLine(-widthPaw, bottom, widthPaw, bottom); + break; + case SeparatorStyle::End: + painter->drawLine(-widthPaw, top, 0, top); + painter->drawLine(-widthPaw, bottom, 0, bottom); + break; + } +} + +void +TimingSeparator::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + emit enterPress(sepIndex); + QGraphicsObject::mousePressEvent(event); +} + +void +TimingSeparator::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + emit exitPress(sepIndex); + QGraphicsObject::mouseReleaseEvent(event); +} + +void +TimingSeparator::silentlySetPos(qreal x, qreal y) +{ + inhibMove = true; + setPos(x, y); + inhibMove = false; +} + +void +TimingSeparator::silentlyMoveBy(qreal x, qreal y) +{ + inhibMove = true; + moveBy(x, y); + inhibMove = false; +} + +QVariant +TimingSeparator::itemChange(GraphicsItemChange change, const QVariant &value) noexcept +{ + if (change == ItemPositionChange && !inhibMove) { + qreal destination = value.toPointF().x(); + destination = parentTimingLine->requestMove(sepIndex, destination); + + emit positionChanged(sepIndex, destination); + return QPointF(destination, 0); + } + + return QGraphicsObject::itemChange(change, value); +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.hh b/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.hh new file mode 100644 index 0000000000000000000000000000000000000000..ca3fa9559363d156cd9e0c0a4565e08af37eb7d4 --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.hh @@ -0,0 +1,44 @@ +#pragma once + +#include "PreCompiledHeaders.hh" + +namespace Vivy +{ +class TimingLine; + +class TimingSeparator final : public QGraphicsObject { + Q_OBJECT +public: + enum class SeparatorStyle { Start, Middle, End }; + +public: + explicit TimingSeparator(int, int, SeparatorStyle, TimingLine *); + + QRectF boundingRect() const override; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + +private: + SeparatorStyle style; + int widthPaw = 5; + QPen pen; + int sepIndex; + TimingLine *parentTimingLine; + + bool inhibMove{ false }; + +signals: + void positionChanged(int, qreal); + void enterPress(int); + void exitPress(int); + +protected: + QVariant itemChange(GraphicsItemChange change, const QVariant &value) noexcept override; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + +public: + void silentlySetPos(qreal, qreal); + void silentlyMoveBy(qreal, qreal); +}; + +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingSyl.cc b/src/UI/DocumentViews/AudioVisualizer/TimingSyl.cc new file mode 100644 index 0000000000000000000000000000000000000000..7d09bc9441d2773513405ccfd0a425d9122b3dcc --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingSyl.cc @@ -0,0 +1,35 @@ +#include "TimingSyl.hh" +#include "TimingParams.hh" +#include "TimingSeparator.hh" + +using namespace Vivy; + +TimingSyl::TimingSyl(VVLib::ASSSyllabe syl_[], int startTime, QGraphicsItem *parent) + : QGraphicsObject(parent) + , syl(syl_) +{ + setZValue(Z_LINE_SYL_TEXT); + setPos(getTimingParam()->posFromMs(startTime), 0); +} + +QRectF +TimingSyl::boundingRect() const +{ + const auto dur = VVLib::ASSSyllabeGetDuration(syl); + return QRectF(0, 0, dur, getTimingParam()->audioHeight()); +} + +void +TimingSyl::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + const auto str = VVLib::ASSSyllabeGetContent(syl); + painter->drawText(boundingRect(), Qt::AlignCenter, + QString::fromUtf8(str.str, static_cast<qsizetype>(str.len))); +} + +void +TimingSyl::setLen(quint64 len) +{ + prepareGeometryChange(); + VVLib::ASSSyllabeSetDuration(syl, len); +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingSyl.hh b/src/UI/DocumentViews/AudioVisualizer/TimingSyl.hh new file mode 100644 index 0000000000000000000000000000000000000000..84c77996ec5c88b63908d4270dff682e2d53c116 --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingSyl.hh @@ -0,0 +1,23 @@ +#pragma once + +#include "PreCompiledHeaders.hh" +#include "Rust/VVLib.hh" + +namespace Vivy +{ +class TimingSyl final : public QGraphicsObject { + Q_OBJECT +public: + explicit TimingSyl(VVLib::ASSSyllabe[], int, QGraphicsItem *parent = nullptr); + + QRectF boundingRect() const override; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + +private: + VVLib::ASSSyllabe *const syl; + +public: + void setLen(quint64 len); +}; + +} diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingView.cc b/src/UI/DocumentViews/AudioVisualizer/TimingView.cc new file mode 100644 index 0000000000000000000000000000000000000000..953b5c4211ffeb6085156995e728202893721400 --- /dev/null +++ b/src/UI/DocumentViews/AudioVisualizer/TimingView.cc @@ -0,0 +1,37 @@ +#include "PreCompiledHeaders.hh" +#include "TimingView.hh" + +using namespace Vivy; + +TimingView::TimingView(QGraphicsScene *scene, QImage img, AudioContext::StreamPtr stream, + QWidget *parent) noexcept + : QGraphicsView(scene, parent) + , audioStream(stream) +{ + setMouseTracking(true); + int interestingViewHeight = int(scene->height() + horizontalScrollBar()->height()); + setFixedHeight(interestingViewHeight); + setMaximumHeight(interestingViewHeight); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum); + QObject::connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, + &TimingView::moveScrollBarToBottom); + + // We want to see the bottom part of the spectrum, the top is less important if a few pixels get trimmed + centerOn(0, interestingViewHeight); +} + +void +TimingView::wheelEvent(QWheelEvent *event) noexcept +{ + horizontalScrollBar()->setValue(horizontalScrollBar()->value() - + event->angleDelta().y() * wheelAngleToScrollRatio); +} + +void +TimingView::moveScrollBarToBottom(int, int max) noexcept +{ + verticalScrollBar()->setValue(max); +} diff --git a/src/UI/DocumentViews/TimingView.hh b/src/UI/DocumentViews/AudioVisualizer/TimingView.hh similarity index 69% rename from src/UI/DocumentViews/TimingView.hh rename to src/UI/DocumentViews/AudioVisualizer/TimingView.hh index 18773e3fce84f3296bc6b899c337b3f904875dd0..1651777348ab0284ad8024920626ab29019fa7c9 100644 --- a/src/UI/DocumentViews/TimingView.hh +++ b/src/UI/DocumentViews/AudioVisualizer/TimingView.hh @@ -6,8 +6,8 @@ #include "PreCompiledHeaders.hh" #include "Lib/Utils.hh" -#include "UI/DocumentViews/TimingBar.hh" -#include "UI/DocumentViews/TimingScene.hh" +#include "TimingBar.hh" +#include "TimingScene.hh" namespace Vivy { @@ -15,21 +15,21 @@ class TimingView final : public QGraphicsView { Q_OBJECT static inline constexpr int wheelAngleToScrollRatio = 5; + TimingScene *currentScene{ nullptr }; + AudioContext::StreamPtr audioStream; public: static inline constexpr QColor startColour = QColor(127, 0, 127); static inline constexpr QColor endColour = QColor(0, 127, 0); - explicit TimingView(QImage, quint64, QWidget * = nullptr) noexcept; + explicit TimingView(QGraphicsScene *, QImage, AudioContext::StreamPtr, + QWidget * = nullptr) noexcept; + ~TimingView() noexcept override = default; void wheelEvent(QWheelEvent *) noexcept override; -private: - TimingScene *scene{ nullptr }; - public slots: - void mousePressEvent(QMouseEvent *event) noexcept override; void moveScrollBarToBottom(int, int) noexcept; }; } diff --git a/src/UI/DocumentViews/MpvContainer.cc b/src/UI/DocumentViews/MpvContainer.cc index 5eae3d618bf8dbfd7c1581d695da2149cf9e4349..511e08b1872c012e9600f5c79492bd84f3be8d1e 100644 --- a/src/UI/DocumentViews/MpvContainer.cc +++ b/src/UI/DocumentViews/MpvContainer.cc @@ -9,7 +9,7 @@ using namespace Vivy; using namespace std::string_literals; static void * -get_proc_address(void *ctx, const char *name) +get_proc_address([[maybe_unused]] void *ctx, const char *name) { QOpenGLContext *glctx = QOpenGLContext::currentContext(); if (!glctx) @@ -28,41 +28,34 @@ MpvContainer::MpvContainer(QWidget *parent) : QOpenGLWidget(parent) , mpv(mpv_create()) { - if (mpv == nullptr) + if (mpv == nullptr) { throw std::runtime_error("Failed to create the MPV context"); + } else { + setAttribute(Qt::WA_DontCreateNativeAncestors); + setAttribute(Qt::WA_NativeWindow); + + mpv_set_option_string(mpv, "idle", "yes"); + mpv_set_option_string(mpv, "loop-file", "inf"); + mpv_set_option_string(mpv, "no-config", "yes"); + mpv_set_option_string(mpv, "sid", "no"); + mpv_set_option_string(mpv, "input-default-bindings", "no"); + mpv_set_option_string(mpv, "input-vo-keyboard", "no"); + mpv_set_option_string(mpv, "no-ytdl", "yes"); + mpv_request_log_messages(mpv, "info"); + + connect(this, &MpvContainer::mpvEvent, this, &MpvContainer::onMpvEvent, + Qt::QueuedConnection); + + if (int rc = mpv_initialize(mpv); rc < 0) { + printMpvError(rc); + throw std::runtime_error("Failed to initialize the mpv context"); + } - setAttribute(Qt::WA_DontCreateNativeAncestors); - setAttribute(Qt::WA_NativeWindow); - - initializeMpv(); -} - -void -MpvContainer::initializeMpv() -{ - if (isMpvAlreadyInitialized) - throw std::logic_error("MPV is already initialized!"); - isMpvAlreadyInitialized = true; - - mpv_set_option_string(mpv, "idle", "yes"); - mpv_set_option_string(mpv, "loop-file", "inf"); - mpv_set_option_string(mpv, "no-config", "yes"); - mpv_set_option_string(mpv, "sid", "no"); - mpv_set_option_string(mpv, "input-default-bindings", "no"); - mpv_set_option_string(mpv, "input-vo-keyboard", "no"); - mpv_set_option_string(mpv, "no-ytdl", "yes"); - mpv_request_log_messages(mpv, "info"); - mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG); - mpv_observe_property(mpv, 0, "unpause", MPV_FORMAT_FLAG); - mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE); - mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); - - connect(this, &MpvContainer::mpvEvent, this, &MpvContainer::onMpvEvent, Qt::QueuedConnection); - mpv_set_wakeup_callback(mpv, &MpvContainer::mpvEventWakeUpCB, this); - - if (int rc = mpv_initialize(mpv); rc < 0) { - printMpvError(rc); - throw std::runtime_error("Failed to initialize the mpv context"); + mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG); + mpv_observe_property(mpv, 0, "unpause", MPV_FORMAT_FLAG); + mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); + mpv_set_wakeup_callback(mpv, &MpvContainer::mpvEventWakeUpCB, this); } } @@ -134,8 +127,7 @@ MpvContainer::~MpvContainer() noexcept registerMpvTimeCallback(nullptr); registerMpvDurationCallback(nullptr); mpv_destroy(mpv); - mpv = nullptr; // Stop all other callbacks here - isMpvAlreadyInitialized = false; // De-init + mpv = nullptr; // Stop all other callbacks here } } @@ -180,26 +172,24 @@ MpvContainer::handleMpvEvent(const mpv_event *const event) noexcept else if (checkProp(prop, "pause", MPV_FORMAT_FLAG)) { isPlaybackPaused = *reinterpret_cast<bool *>(prop->data); emit mpvPlaybackToggled(!isPlaybackPaused); - logDebug() << "MPV -> set to" << (isPlaybackPaused ? "pause" : "play"); + logDebug() << "MPV -> set to " << (isPlaybackPaused ? "pause" : "play"); } else if (checkProp(prop, "unpause", MPV_FORMAT_FLAG)) { isPlaybackPaused = !*reinterpret_cast<bool *>(prop->data); emit mpvPlaybackToggled(!isPlaybackPaused); - logDebug() << "MPV -> set to" << (isPlaybackPaused ? "pause" : "play"); + logDebug() << "MPV -> set to " << (isPlaybackPaused ? "pause" : "play"); } break; - case MPV_EVENT_START_FILE: logDebug() << "MPV: Begin of file"; break; - case MPV_EVENT_END_FILE: logDebug() << "MPV: Reached end of file!"; break; - case MPV_EVENT_COMMAND_REPLY: - logDebug() << "Got return of" << event->reply_userdata; handleMpvEventCommandReply(static_cast<AsyncCmdType>(event->reply_userdata)); break; // Explicitly ignored + case MPV_EVENT_START_FILE: + case MPV_EVENT_END_FILE: case MPV_EVENT_VIDEO_RECONFIG: case MPV_EVENT_NONE: case MPV_EVENT_GET_PROPERTY_REPLY: @@ -250,8 +240,8 @@ MpvContainer::handleMpvEventCommandReply(const AsyncCmdType type) noexcept unloadAssFile(); break; - case AsyncCmdType::SeekTime: logDebug() << "MPV - CMD: Seeked playback"; break; - case AsyncCmdType::TogglePlayback: logDebug() << "MPV - CMD: Playback was toggled"; break; + case AsyncCmdType::SeekTime: break; + case AsyncCmdType::TogglePlayback: break; } } diff --git a/src/UI/DocumentViews/MpvContainer.hh b/src/UI/DocumentViews/MpvContainer.hh index 1649ede2c47e6abd91d4ae79b0a1208f9c04d450..3adba5364b78eb62e5897e930d6190fbf971a308 100644 --- a/src/UI/DocumentViews/MpvContainer.hh +++ b/src/UI/DocumentViews/MpvContainer.hh @@ -42,7 +42,6 @@ public: void registerMpvDurationCallback(std::function<void(double)>) noexcept; private: - void initializeMpv(); void handleMpvEvent(const mpv_event *const) noexcept; void printMpvError(int) const noexcept; void asyncCommand(const AsyncCmdType, std::initializer_list<const char *>) noexcept; @@ -57,7 +56,6 @@ private: static void mpvEventWakeUpCB(void *) noexcept; bool isPlaybackPaused{ true }; - bool isMpvAlreadyInitialized{ false }; mpv_handle *mpv{ nullptr }; mpv_render_context *mpv_gl{ nullptr }; qint64 sid{ -1 }; diff --git a/src/UI/DocumentViews/TimingScene.cc b/src/UI/DocumentViews/TimingScene.cc deleted file mode 100644 index 268bc0f627e85aeccef1fcab5aa40ac8b7c3d3cf..0000000000000000000000000000000000000000 --- a/src/UI/DocumentViews/TimingScene.cc +++ /dev/null @@ -1,89 +0,0 @@ -#include "PreCompiledHeaders.hh" -#include "UI/DocumentViews/TimingScene.hh" -#include "Lib/Utils.hh" - -#include <QGraphicsLineItem> -#include <QGraphicsPixmapItem> -#include <QGraphicsView> -#include <QLabel> -#include <QMessageBox> -#include <QMouseEvent> -#include <QPainter> -#include <QGraphicsSceneMouseEvent> -#include <QScrollArea> -#include <QScrollBar> -#include <QVBoxLayout> - -using namespace Vivy; - -TimingScene::TimingScene(QWidget *parent) noexcept - : QGraphicsScene(parent) -{ -} - -TimingScene::TimingScene(QImage img_, quint64 soundLength_, QWidget *parent) noexcept - : QGraphicsScene(parent) - , backgroundImg(addPixmap(QPixmap::fromImage(img_))) - , img(img_) - , soundLength( - static_cast<qint64>(std::clamp(soundLength_, static_cast<quint64>(0), - static_cast<quint64>(std::numeric_limits<qint64>::max())))) -{ -} - -void -TimingScene::mousePressEvent(QGraphicsSceneMouseEvent *event) noexcept -{ - QPointF pos = event->scenePos(); - QGraphicsItem *got = itemAt(pos, QTransform()); - - if (currentLine && (got == nullptr || got == backgroundImg)) [[likely]] { - // Handle the different cases - if (timingMode == TimingMode::Line) - handleMousePressEventLine(event, currentLine); - else if (timingMode == TimingMode::Syl) - handleMousePressEventSyl(event, currentLine); - else if (timingMode == TimingMode::Char) - handleMousePressEventChar(event, currentLine); - } - - QGraphicsScene::mousePressEvent(event); -} - -void -TimingScene::handleMousePressEventLine(QGraphicsSceneMouseEvent *event, VVLib::ASSLine p[]) noexcept -{ - const qint64 time = timeFromPos(event->scenePos().x()); - if (const auto &btn = event->button(); btn == Qt::LeftButton) { - VVLib::ASSLineSetStartTime(p, static_cast<int64_t>(time)); - } else if (btn == Qt::RightButton) { - VVLib::ASSLineSetFiniTime(p, static_cast<int64_t>(time)); - } -} - -void -TimingScene::handleMousePressEventSyl(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept -{ -} - -void -TimingScene::handleMousePressEventChar(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept -{ -} - -qint64 -TimingScene::timeFromPos(qreal x) -{ - if (const qreal w = width(); x <= 0 || w <= 0) { - qCritical() << "Try avoid possible divide by zero in the time from position"; - return 0; - } else { - return static_cast<qint64>(x) * soundLength / static_cast<qint64>(w); - } -} - -QGraphicsPixmapItem * -TimingScene::bg() noexcept -{ - return backgroundImg; -} diff --git a/src/UI/DocumentViews/TimingScene.hh b/src/UI/DocumentViews/TimingScene.hh deleted file mode 100644 index 7bb841cbdcead04be41e603d9f28db525764d477..0000000000000000000000000000000000000000 --- a/src/UI/DocumentViews/TimingScene.hh +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include "Lib/Utils.hh" -#include "Rust/VVLib.hh" -#include "UI/DocumentViews/TimingBar.hh" - -namespace Vivy -{ -class TimingScene final : public QGraphicsScene { - Q_OBJECT - -public: - enum class TimingMode { Line = 0, Syl = (1 << 1), Char = (1 << 2) }; - Q_DECLARE_FLAGS(TimingModes, TimingMode) - -public: - static inline constexpr QColor startColour = QColor(127, 0, 127); - static inline constexpr QColor endColour = QColor(0, 127, 0); - - explicit TimingScene(QWidget *parent = nullptr) noexcept; - explicit TimingScene(QImage, quint64, QWidget * = nullptr) noexcept; - -private: - QGraphicsPixmapItem *backgroundImg{ nullptr }; - QImage img; - qint64 soundLength{ 0 }; - VVLib::ASSLine *currentLine{ nullptr }; - TimingMode timingMode{ TimingMode::Line }; - -public: - QGraphicsPixmapItem *bg() noexcept; - void mousePressEvent(QGraphicsSceneMouseEvent *event) noexcept override; - -private: - qint64 timeFromPos(qreal x); - void handleMousePressEventLine(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept; - void handleMousePressEventSyl(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept; - void handleMousePressEventChar(QGraphicsSceneMouseEvent *, VVLib::ASSLine[]) noexcept; - -public slots: -}; - -} - -//Only if combinaisons of mode is allowed, it shouldn't be here -//Q_DECLARE_OPERATORS_FOR_FLAGS(TimingScene::TimingModes) diff --git a/src/UI/DocumentViews/TimingView.cc b/src/UI/DocumentViews/TimingView.cc deleted file mode 100644 index cb8f6b1c43a6bc546d6382c6785d5df93fe4cb70..0000000000000000000000000000000000000000 --- a/src/UI/DocumentViews/TimingView.cc +++ /dev/null @@ -1,50 +0,0 @@ -#include "PreCompiledHeaders.hh" -#include "UI/DocumentViews/TimingView.hh" - -using namespace Vivy; - -TimingView::TimingView(QImage img, quint64 soundLength, QWidget *parent) noexcept - : QGraphicsView(parent) -{ - scene = new TimingScene(img, soundLength, this); - setFixedHeight(img.height()); - setMaximumHeight(img.height()); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum); - QObject::connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, - &TimingView::moveScrollBarToBottom); - verticalScrollBar()->setValue(verticalScrollBar()->maximum()); - horizontalScrollBar()->setValue(horizontalScrollBar()->minimum()); - setScene(scene); -} - -void -TimingView::wheelEvent(QWheelEvent *event) noexcept -{ - horizontalScrollBar()->setValue(horizontalScrollBar()->value() - - event->angleDelta().y() * wheelAngleToScrollRatio); -} - -void -TimingView::mousePressEvent(QMouseEvent *event) noexcept -{ - QPoint pos = event->pos(); - pos.rx() += horizontalScrollBar()->value(); - int x = event->pos().x() + horizontalScrollBar()->value(); - - QGraphicsItem *got; - if ((got = scene->itemAt(pos, QTransform())) == nullptr || got == scene->bg()) { - const int height = static_cast<int>(scene->height()); - scene->addItem(new TimingBar(QLine(x, 0, x, height), - event->button() == Qt::LeftButton ? startColour : endColour)); - } - - QGraphicsView::mousePressEvent(event); -} - -void -TimingView::moveScrollBarToBottom(int, int max) noexcept -{ - verticalScrollBar()->setValue(max); -} diff --git a/src/UI/MainWindow.cc b/src/UI/MainWindow.cc index 65222a08822abfe6c50ab771de3523a1e31687c1..d8b741c46616fc82141296c8a2d05c38ee3bfe2a 100644 --- a/src/UI/MainWindow.cc +++ b/src/UI/MainWindow.cc @@ -38,6 +38,7 @@ MainWindow::MainWindow() noexcept DCL_MENU(edit, "&Edit"); DCL_MENU(viewTmp, "&View"); viewMenu = viewTmpMenu; // Save the view menu + DCL_MENU(ass, "&ASS"); DCL_MENU(simulate, "&Simulate"); DCL_MENU(help, "&Help"); diff --git a/src/UI/ScriptDocumentView.hh b/src/UI/ScriptDocumentView.hh index 76a427906591b2428476c2c085d5a85dcbbd2f13..416e1499128f66a59c46a3970075910475d66064 100644 --- a/src/UI/ScriptDocumentView.hh +++ b/src/UI/ScriptDocumentView.hh @@ -24,6 +24,8 @@ public: void closeDocument() noexcept override; void openProperties() noexcept override; + void loadViews() noexcept override {} + QString getDocumentTabName() const noexcept override; QString getDocumentTabToolTip() const noexcept override; QIcon getDocumentTabIcon() const noexcept override; diff --git a/src/UI/VivyDocumentView.cc b/src/UI/VivyDocumentView.cc index 6cc127fa6bdcde6dac57a37aa233bf3b8d97f49a..1c86d74210f8e1da3b1bc4bae6ce2d5e6959e457 100644 --- a/src/UI/VivyDocumentView.cc +++ b/src/UI/VivyDocumentView.cc @@ -1,3 +1,12 @@ +#include "VivyDocumentView.hh" + +#include <QHeaderView> +#include <QTreeView> +#include <QVBoxLayout> +#include <QTableView> +#include <QWidget> +#include <QDockWidget> + #include "PreCompiledHeaders.hh" #include "UI/VivyDocumentView.hh" #include "UI/PropertyModel.hh" @@ -8,6 +17,7 @@ #include "VivyApplication.hh" #include "Lib/Document/VivyDocument.hh" #include "Lib/Document/VivyDocumentStore.hh" +#include "Lib/Audio.hh" using namespace Vivy; @@ -20,10 +30,6 @@ VivyDocumentView::VivyDocumentView(std::shared_ptr<VivyDocument> doc, QWidget *p setDockNestingEnabled(true); - loadAudioView(); - loadVideoView(); - loadAssView(); - // Add some actions... openPropertiesAct = new QAction("Open properties", this); connect(openPropertiesAct, &QAction::triggered, this, &VivyDocumentView::openProperties); @@ -42,6 +48,14 @@ VivyDocumentView::VivyDocumentView(std::shared_ptr<VivyDocument> doc, QWidget *p VIVY_LOG_CTOR() << "View for " << doc->getUuid() << " done!"; } +void +VivyDocumentView::loadViews() noexcept +{ + loadAudioView(); + loadVideoView(); + loadAssView(); +} + VivyDocumentView::~VivyDocumentView() noexcept { VIVY_LOG_DTOR() << "Deleting view for " << document->getUuid() << ", file name is " @@ -95,6 +109,8 @@ VivyDocumentView::loadVideoView() noexcept videoView->setWidget(new VideoView(videoView)); qobject_cast<VideoView *>(videoView->widget()) ->loadFile(document->getVideoSubDocument()->getFilePath()); + + emit videoSubDocumentChanged(); } void @@ -114,6 +130,8 @@ VivyDocumentView::loadAssView() noexcept assModel.reset(new AssLinesModel(document->getAssSubDocument()->getLines())); Utils::deleteInternalWidget(assLines); assLines->setWidget(new AssLinesView(assModel.get(), assLines)); + + emit assSubDocumentChanged(); } void @@ -141,10 +159,12 @@ VivyDocumentView::loadAudioView() noexcept // Kubat: don't check, may throw an error but don't think we can // recover from it. - AudioVisualizer *visualizerInner = new AudioVisualizer(stream, visualizer); + AudioVisualizer *visualizerInner = new AudioVisualizer(stream, *this, visualizer); Utils::deleteInternalWidget(visualizer); visualizer->setWidget(visualizerInner); visualizer->layout()->setAlignment(visualizerInner, Qt::AlignTop); + + emit audioSubDocumentChanged(); } void diff --git a/src/UI/VivyDocumentView.hh b/src/UI/VivyDocumentView.hh index 47ce593ab458a42d1d7e79c9282a683f1c704645..417b1623d77537f3c935574ea430585c4a0ce4f1 100644 --- a/src/UI/VivyDocumentView.hh +++ b/src/UI/VivyDocumentView.hh @@ -41,6 +41,8 @@ public: VivyDocument *getDocument() const noexcept override; + void loadViews() noexcept override; + public slots: void loadAudioView() noexcept; void loadVideoView() noexcept; @@ -55,5 +57,19 @@ private: UnclosableDockWidget *assLines{ nullptr }; UnclosableDockWidget *videoView{ nullptr }; QAction *openPropertiesAct{ nullptr }; + +public: + AssLinesView *getAssLinesView() const noexcept + { + return assLines == nullptr ? nullptr : reinterpret_cast<AssLinesView *>(assLines->widget()); + } + + AssLinesModel *getAssLinesModel() const noexcept { return assModel.get(); } + +signals: + // FIXME: fix default value or just send void? + void audioSubDocumentChanged(std::shared_ptr<AudioSubDocument> = nullptr); + void videoSubDocumentChanged(std::shared_ptr<VideoSubDocument> = nullptr); + void assSubDocumentChanged(std::shared_ptr<AssSubDocument> = nullptr); }; } diff --git a/src/VivyApplication.cc b/src/VivyApplication.cc index ad6711521fb21ddf46ca80de7cdcbb52ed32bdc9..34229c1cfcb90773c657bc35a96c9a91d8a64082 100644 --- a/src/VivyApplication.cc +++ b/src/VivyApplication.cc @@ -44,7 +44,6 @@ VivyApplication::~VivyApplication() noexcept {} int VivyApplication::exec() noexcept { - // For MPV & Qt std::setlocale(LC_NUMERIC, "C"); // Add fonts