From 582b42388e07a7004713defc63e2e49c3b4126ad Mon Sep 17 00:00:00 2001 From: Avery King Date: Tue, 27 May 2025 22:09:01 -0700 Subject: [PATCH] mod-mka: Add back utilities These utilities are required for the acutal exporter to work. Add them back first to get things started. Signed-off-by: Avery King --- modules/import-export/mod-mka/CMakeLists.txt | 1 + modules/import-export/mod-mka/ExportMka.cpp | 289 +++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 modules/import-export/mod-mka/ExportMka.cpp diff --git a/modules/import-export/mod-mka/CMakeLists.txt b/modules/import-export/mod-mka/CMakeLists.txt index c8b817eb4..5aedb2da6 100644 --- a/modules/import-export/mod-mka/CMakeLists.txt +++ b/modules/import-export/mod-mka/CMakeLists.txt @@ -6,6 +6,7 @@ set(TARGET mod-mka) set(SOURCES ImportMka.cpp + ExportMka.cpp Mka.cpp ) diff --git a/modules/import-export/mod-mka/ExportMka.cpp b/modules/import-export/mod-mka/ExportMka.cpp new file mode 100644 index 000000000..63ded1847 --- /dev/null +++ b/modules/import-export/mod-mka/ExportMka.cpp @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/********************************************************************** + + Tenacity: A Digital Audio Editor + + ExportMka.cpp + + Steve Lhomme + +**********************************************************************/ + +#include "Tags.h" +#if defined(_CRTDBG_MAP_ALLOC) && LIBMATROSKA_VERSION < 0x010702 +// older libmatroska headers use std::nothrow which is incompatible with +#undef new +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#if LIBMATROSKA_VERSION < 0x010700 +typedef enum { + MATROSKA_TRACK_TYPE_VIDEO = 0x1, // An image. + MATROSKA_TRACK_TYPE_AUDIO = 0x2, // Audio samples. + MATROSKA_TRACK_TYPE_COMPLEX = 0x3, // A mix of different other TrackType. The codec needs to define how the `Matroska Player` should interpret such data. + MATROSKA_TRACK_TYPE_LOGO = 0x10, // An image to be rendered over the video track(s). + MATROSKA_TRACK_TYPE_SUBTITLE = 0x11, // Subtitle or closed caption data to be rendered over the video track(s). + MATROSKA_TRACK_TYPE_BUTTONS = 0x12, // Interactive button(s) to be rendered over the video track(s). + MATROSKA_TRACK_TYPE_CONTROL = 0x20, // Metadata used to control the player of the `Matroska Player`. + MATROSKA_TRACK_TYPE_METADATA = 0x21, // Timed metadata that can be passed on to the `Matroska Player`. +} MatroskaTrackType; + +typedef enum { + MATROSKA_TARGET_TYPE_COLLECTION = 70, // The highest hierarchical level that tags can describe. + MATROSKA_TARGET_TYPE_EDITION = 60, // A list of lower levels grouped together. + MATROSKA_TARGET_TYPE_ALBUM = 50, // The most common grouping level of music and video (equals to an episode for TV series). + MATROSKA_TARGET_TYPE_PART = 40, // When an album or episode has different logical parts. + MATROSKA_TARGET_TYPE_TRACK = 30, // The common parts of an album or movie. + MATROSKA_TARGET_TYPE_SUBTRACK = 20, // Corresponds to parts of a track for audio (like a movement). + MATROSKA_TARGET_TYPE_SHOT = 10, // The lowest hierarchy found in music or movies. +} MatroskaTargetTypeValue; +#endif + +#ifdef USE_LIBFLAC +#include "FLAC++/encoder.h" +#endif + +using namespace libmatroska; + +//// Utilities for the exporter /////////////////////////////////////////////// +static uint64_t GetRandomUID64() +{ + uint64_t uid = 0; + for (size_t i=0; i<(64-7); i+=7) + { + auto r = static_cast(std::rand() & 0x7fff); + uid = uid << 7 | r; + } + return uid; +} + +static void FillRandomUUID(binary UID[16]) +{ + uint64_t rand; + rand = GetRandomUID64(); + memcpy(&UID[0], &rand, 8); + rand = GetRandomUID64(); + memcpy(&UID[8], &rand, 8); +} + +static void SetMetadata(const Tags *tags, KaxTag * & PrevTag, KaxTags & Tags, const wxChar *tagName, const MatroskaTargetTypeValue TypeValue, const wchar_t *mkaName) +{ + if (tags != nullptr && tags->HasTag(tagName)) + { + KaxTag &tag = AddNewChild(Tags); + KaxTagTargets &tagTarget = GetChild(tag); + (EbmlUInteger &) GetChild(tagTarget) = TypeValue; + + KaxTagSimple &simpleTag = GetChild(tag); + (EbmlUnicodeString &) GetChild(simpleTag) = (UTFstring)mkaName; + (EbmlUnicodeString &) GetChild(simpleTag) = (UTFstring)tags->GetTag(tagName); + } +} + +constexpr uint64 MS_PER_FRAME = 40; + +class ClusterMuxer +{ +public: + struct MuxerTime + { + uint64 prevEndTime{0}; + uint64 samplesRead{0}; + + /*const*/ uint64 TimeUnit; + /*const*/ double rate; + + // MuxerTime(uint64 _TimeUnit, double _rate) + // :TimeUnit(_TimeUnit) + // ,rate(_rate) + // { + // } + + // MuxerTime(const MuxerTime & source) = default; + + void AddSample(size_t samplesThisRun) + { + samplesRead += samplesThisRun; + // in rounded TimeUnit as the drift with the actual time accumulates + prevEndTime = std::llround(samplesRead * 1000000000. / (TimeUnit * rate)); + } + }; + +public: + ClusterMuxer(KaxSegment &_FileSegment, KaxTrackEntry &_Track, KaxCues &_AllCues, MuxerTime _muxerTime) + :FileSegment(_FileSegment) + ,MyTrack1(_Track) + ,AllCues(_AllCues) + ,Time(_muxerTime) + ,maxFrameSamples(MS_PER_FRAME * _muxerTime.rate / 1000) // match mkvmerge + { + + Cluster = std::make_unique(); + Cluster->SetParent(FileSegment); // mandatory to store references in this Cluster + Cluster->InitTimecode(Time.prevEndTime, Time.TimeUnit); + Cluster->EnableChecksum(); + + framesBlob = std::make_unique(BLOCK_BLOB_SIMPLE_AUTO); + framesBlob->SetParent(*Cluster); + AllCues.AddBlockBlob(*framesBlob); + } + + // return true when the Muxer is full + bool AddBuffer(DataBuffer &dataBuff, size_t samplesThisRun) + { + if (!framesBlob) + { + framesBlob = std::make_unique(BLOCK_BLOB_SIMPLE_AUTO); + framesBlob->SetParent(*Cluster); + } + // TODO check the timestamp is OK within the Cluster first + if (!framesBlob->AddFrameAuto(MyTrack1, Time.prevEndTime * Time.TimeUnit, dataBuff, LACING_NONE)) + { + // last frame allowed in the lace, we need a new frame blob + FinishFrameBlock(maxFrameSamples - samplesThisRun); + } + + Time.AddSample(samplesThisRun); + + sampleWritten += samplesThisRun; + + if (sampleWritten >= (uint64)(10 * maxFrameSamples)) + return true; + return false; + } + + MuxerTime Finish(IOCallback & mka_file, KaxSeekHead & MetaSeek) + { + if (sampleWritten) + { + FinishFrameBlock(maxFrameSamples); + Cluster->Render(mka_file, AllCues); + // No need to use SeekHead for Cluster, there's Cues for that + // MetaSeek.IndexThis(*Cluster, FileSegment); + } + return Time; + } + +private: + void FinishFrameBlock(const size_t lessSamples) + { + if (framesBlob) + { + if (lessSamples != 0) + { + // last block, write the duration + // TODO get frames from the blob and add them in a BLOCK_BLOB_NO_SIMPLE one + // framesBlob->SetBlockDuration(lessSamples * Time.TimestampScale / Time.rate); + } + Cluster->AddBlockBlob(framesBlob.release()); + } + } + + KaxSegment &FileSegment; + KaxTrackEntry &MyTrack1; + KaxCues &AllCues; + const size_t maxFrameSamples; + + MuxerTime Time; + + std::unique_ptr Cluster; + std::unique_ptr framesBlob; + uint64_t sampleWritten{0}; +}; + +#ifdef USE_LIBFLAC +class MkaFLACEncoder : public FLAC::Encoder::Stream +{ +public: + ::FLAC__StreamEncoderInitStatus init() override + { + auto ret = FLAC::Encoder::Stream::init(); + initializing = false; + return ret; + } + + const std::vector & GetInitBuffer() + { + return initBuffer; + } + + bool Process(std::unique_ptr & Muxer, const FLAC__int32 * const buffer[], uint32_t samples) + { + muxer = &Muxer; + muxer_full = false; + bool ret = process(buffer, samples); + if (!ret) + return true; // on error assume the Cluster is full + return muxer_full; + } + + bool finish() override + { + finishing = true; + return FLAC::Encoder::Stream::finish(); + } + + bool hasNewCodecPrivate() const + { + return initializing || overwriting; + } + +protected: + ::FLAC__StreamEncoderWriteStatus write_callback(const FLAC__byte buffer[], size_t bytes, uint32_t samples, uint32_t /*current_frame*/) override + { + if (initializing) + { + std::size_t size = initBuffer.size(); + initBuffer.resize(size + bytes); + memcpy(&initBuffer[size], buffer, bytes); + return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; + } + if (overwriting) + { + if (initOffset + bytes > initBuffer.size()) + { + // we can't expand the CodePrivate + return FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR; + } + memcpy(&initBuffer[initOffset], buffer, bytes); + return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; + } + + DataBuffer *dataBuff = new DataBuffer((binary*)buffer, bytes, nullptr, true); + if ((*muxer)->AddBuffer(*dataBuff, samples)) + muxer_full = true; + + return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; + } + + ::FLAC__StreamEncoderSeekStatus seek_callback(FLAC__uint64 absolute_byte_offset) override + { + // support overwriting the CodecPrivate with the checksum + if (finishing && absolute_byte_offset < (initBuffer.size() + initOffset)) + { + overwriting = true; + initOffset = absolute_byte_offset; + return FLAC__STREAM_ENCODER_SEEK_STATUS_OK; + } + return FLAC__STREAM_ENCODER_SEEK_STATUS_UNSUPPORTED; + } + + bool initializing{true}; + bool finishing{false}; + bool overwriting{false}; + std::vector initBuffer; + FLAC__uint64 initOffset{0}; + + std::unique_ptr *muxer = nullptr; + bool muxer_full; +}; +#endif +///////////////////////////////////////////////////////////////////////////////