Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,19 @@ add_test(NAME parser_fallback_check COMMAND parser_fallback_check)
set_tests_properties(parser_fallback_check PROPERTIES FIXTURES_REQUIRED assets)
set_tests_properties(parser_fallback_check PROPERTIES LABELS "core")

# Safety regression: refuse absurd atom sizes / overflows without crashing.
# Safety regressions: reject malformed sizes and accept large media payloads without crashing.
add_executable(parser_safety_check tests/parser_safety.cpp)
target_link_libraries(parser_safety_check PRIVATE chapterforge)
target_include_directories(parser_safety_check PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
add_test(NAME parser_safety_check COMMAND parser_safety_check)
set_tests_properties(parser_safety_check PROPERTIES LABELS "core")

add_executable(aac_extractor_unit tests/aac_extractor_unit.cpp)
target_link_libraries(aac_extractor_unit PRIVATE chapterforge)
target_include_directories(aac_extractor_unit PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
add_test(NAME aac_extractor_unit COMMAND aac_extractor_unit)
set_tests_properties(aac_extractor_unit PROPERTIES LABELS "unit")

if(nlohmann_json_FOUND)
add_executable(image_fixtures tests/image_fixtures.cpp)
target_link_libraries(image_fixtures PRIVATE chapterforge nlohmann_json::nlohmann_json)
Expand Down
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
[![Docs](https://img.shields.io/badge/docs-Doxygen-blueviolet.svg)](https://tillt.github.io/ChapterForge/)
[![Homebrew](https://img.shields.io/badge/homebrew-tap-181717.svg)](#macos-homebrew)

ChapterForge is a library and CLI to mux chapters (text and optional images) into AAC/M4A files while preserving metadata and handling Apple-compatible chapter tracks.
<p align="center"><img src="site/images/chapterforge_logo.png" /></p>

ChapterForge is a library and CLI to mux chapters (text and optional images) into MP4 files while preserving metadata and handling Apple-compatible chapter tracks.

<!-- TOC START (generated by scripts/update_readme_toc.py) -->
## Table of Contents
Expand Down Expand Up @@ -217,12 +219,6 @@ Notes:
- If top-level metadata fields are omitted and the input file already contains metadata (`ilst`), that metadata is preserved automatically.
- Paths for `cover` and per-chapter `image` are resolved relative to the JSON file location.

> **First chapter behavior (Apple/VLC)**
> The chapter tracks are duration-based (`stts`), but most players force the first sample to start
> at t=0. A non-zero first `start_ms` will be snapped to 0 in QuickTime, Music.app, AVFoundation,
> and VLC. If you need silence/blank time before your “real” first chapter, add a leading placeholder
> chapter that covers 0..gap_ms and then start your first “real” chapter after that.


## Output

Expand Down
6 changes: 4 additions & 2 deletions include/aac_extractor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
struct AacExtractResult {
std::vector<std::vector<uint8_t>> frames; // raw AAC frames (ADTS header stripped)
std::vector<uint32_t> sizes; // raw frame sizes
std::vector<uint32_t> chunk_sizes; // samples per source chunk

uint32_t sample_rate = 0;
uint8_t sampling_index = 0;
Expand All @@ -27,6 +28,7 @@ struct AacExtractResult {
std::vector<uint8_t> stsc_payload;
std::vector<uint8_t> stsz_payload;
std::vector<uint8_t> stco_payload;
std::vector<uint8_t> co64_payload;

// Optional: original meta/ilst payloads (when source is MP4/M4A)
std::vector<uint8_t> meta_payload;
Expand All @@ -41,7 +43,7 @@ AacExtractResult extract_adts_frames(const std::vector<uint8_t> &data);
/**
* @brief Extract AAC frames and related tables from an MP4/M4A source.
*
* Preferred when the input is already an MP4 container so we can reuse stsd/stts/stsc/stsz/stco and
* any meta/ilst payloads.
* Preferred when the input is already an MP4 container so we can reuse its audio sample tables
* (including stco or co64) and any meta/ilst payloads.
*/
std::optional<AacExtractResult> extract_from_mp4(const std::string &path);
24 changes: 15 additions & 9 deletions include/mdat_writer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@

#include "mp4_atoms.hpp"

// Stores final chunk offsets per track, used for STCO patching.
// Stores final chunk offsets per track, used for stco/co64 patching.
struct MdatOffsets {
std::vector<uint32_t> audio_offsets;
std::vector<std::vector<uint32_t>> text_offsets; // one entry per text track
std::vector<uint32_t> image_offsets;
std::vector<uint64_t> audio_offsets;
std::vector<std::vector<uint64_t>> text_offsets; // one entry per text track
std::vector<uint64_t> image_offsets;

uint64_t payload_start = 0; // absolute file offset where mdat payload begins
};
Expand All @@ -30,14 +30,20 @@ MdatOffsets write_mdat(std::ofstream &out, const std::vector<std::vector<uint8_t
const std::vector<std::vector<uint32_t>> &text_chunk_sizes,
const std::vector<uint32_t> &image_chunk_sizes);

// Patch a single stco atom.
void patch_stco_table(Atom *stco, const std::vector<uint32_t> &offsets,
uint64_t mdat_payload_start);
// Promote all 32-bit chunk-offset tables before layout when output offsets may exceed 32 bits.
void promote_stco_to_co64(Atom *root);

// Patch stco boxes in moov (audio, text tracks, image). If patch_audio is false,
// audio stco (first one) is left untouched.
// Patch stco/co64 boxes in moov (audio, text tracks, image). If patch_audio is false,
// the first audio offset table is left untouched.
void patch_all_stco(Atom *moov, const MdatOffsets &offs, bool patch_audio = true);

uint64_t media_payload_size(
const std::vector<std::vector<uint8_t>> &audio_samples,
const std::vector<std::vector<std::vector<uint8_t>>> &text_tracks_samples,
const std::vector<std::vector<uint8_t>> &image_samples);

uint64_t mdat_header_size(uint64_t payload_size);

// Compute chunk offsets without writing, given starting payload offset.
MdatOffsets compute_mdat_offsets(uint64_t payload_start,
const std::vector<std::vector<uint8_t>> &audio_samples,
Expand Down
11 changes: 7 additions & 4 deletions include/parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
#include <vector>

struct Mp4AtomInfo {
uint32_t type;
uint64_t size; // total atom size.
uint64_t offset; // offset in file.
uint32_t type = 0;
uint64_t size = 0; // Total atom size, including its header.
uint64_t offset = 0; // Offset of the atom header in the file or enclosing stream.
uint64_t header_size = 8; // 8 normally, 16 when a 64-bit extended size is present.
};

namespace parser_detail {
Expand All @@ -35,12 +36,13 @@ struct TrackParseResult {
std::vector<uint8_t> stsc;
std::vector<uint8_t> stsz;
std::vector<uint8_t> stco;
std::vector<uint8_t> co64;
};
} // namespace parser_detail

// Minimal parsed MP4 data for our authoring needs.
struct ParsedMp4 {
bool used_fallback_stbl = false; // true if stbl atoms were recovered via flat scan.
bool used_fallback_stbl = false; // Reserved for reporting parser recovery paths.

// All parsed tracks (audio/text/video).
std::vector<parser_detail::TrackParseResult> tracks;
Expand All @@ -60,6 +62,7 @@ struct ParsedMp4 {
std::vector<uint8_t> stsc;
std::vector<uint8_t> stsz;
std::vector<uint8_t> stco;
std::vector<uint8_t> co64;
};

// Utility: read big-endian 32-bit value.
Expand Down
5 changes: 3 additions & 2 deletions include/stbl_audio_builder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ std::unique_ptr<Atom> build_audio_stbl(const Mp4aConfig &cfg,
uint32_t num_samples,
const std::vector<uint8_t> *raw_stsd = nullptr);

// Build stbl from pre-existing box payloads (stsd/stts/stsc/stsz/stco)
// Build stbl from pre-existing box payloads (stsd/stts/stsc/stsz and stco or co64).
std::unique_ptr<Atom> build_audio_stbl_raw(const std::vector<uint8_t> &stsd_payload,
const std::vector<uint8_t> &stts_payload,
const std::vector<uint8_t> &stsc_payload,
const std::vector<uint8_t> &stsz_payload,
const std::vector<uint8_t> &stco_payload);
const std::vector<uint8_t> &stco_payload,
const std::vector<uint8_t> &co64_payload = {});
Binary file added site/images/chapterforge_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading