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