From 0516b2e8c734ae4c96d37fd8bc19897385a23efe Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 20:14:23 +0900 Subject: [PATCH] mcpplibs.mwrap.opus 1.6.1: opus-m (libopus, opusfile and libogg as C++23 modules) Descriptor pkgs/mwrap/opus.lua (form A, the immutable mwrap-1.6.1 release asset, tagged on opus-m's merged main; CN is the gitcode mcpp-res/mwrap-opus mirror with the same bytes), over compat.opus 1.6.1, compat.opusfile 0.12 and compat.ogg 1.3.6. Consumer tests/mwrap/opus-ogg: a chord encoded through the typed CTL requests, paged into an Ogg Opus stream with libogg and read back with opusfile from memory (headers, tags, the exact length, seeking), a 5.1 multistream encoder's per-stream state, and the side header beside the import. The installed-template check in validate.yml with its deployment contract (the wrapper's LICENSE/NOTICE and the three libraries' READMEs and COPYINGs, byte for byte in a relocated bundle), and the plan. --- .../docs/2026-10-28-add-opus-mwrap-plan.md | 62 +++++++++++++ .github/workflows/validate.yml | 5 + CHANGELOG.md | 9 ++ mcpp.toml | 1 + pkgs/mwrap/opus.lua | 52 +++++++++++ tests/mwrap/check_installed_template.py | 24 +++++ tests/mwrap/opus-ogg/mcpp.toml | 11 +++ tests/mwrap/opus-ogg/tests/behavior.cpp | 93 +++++++++++++++++++ tests/mwrap/opus-ogg/tests/side_header.cpp | 31 +++++++ 9 files changed, 288 insertions(+) create mode 100644 .agents/docs/2026-10-28-add-opus-mwrap-plan.md create mode 100644 pkgs/mwrap/opus.lua create mode 100644 tests/mwrap/opus-ogg/mcpp.toml create mode 100644 tests/mwrap/opus-ogg/tests/behavior.cpp create mode 100644 tests/mwrap/opus-ogg/tests/side_header.cpp diff --git a/.agents/docs/2026-10-28-add-opus-mwrap-plan.md b/.agents/docs/2026-10-28-add-opus-mwrap-plan.md new file mode 100644 index 00000000..2f11c838 --- /dev/null +++ b/.agents/docs/2026-10-28-add-opus-mwrap-plan.md @@ -0,0 +1,62 @@ +# Add mcpplibs.mwrap.opus (opus-m 1.6.1) + +## Form + +- Third-party upstreams, all BSD-3-Clause: [libopus](https://opus-codec.org) 1.6.1, [opusfile](https://github.com/xiph/opusfile) 0.12 + and [libogg](https://github.com/xiph/ogg) 1.3.6, the release tarballs compat.opus, compat.opusfile and compat.ogg pin (the + parent change). +- Form A external mwrap module repository [mcpplibs/opus-m](https://github.com/mcpplibs/opus-m), package `mcpplibs.mwrap.opus`, + version 1.6.1 (libopus's version; the first release of the identity, no revision suffix; opusfile 0.12 and libogg 1.3.6 in + `[package.metadata.mwrap]`), tag series `mwrap-${version}`, tagged on the merged main commit (`96fb761`); release asset + `opus-m-1.6.1.tar.gz` (`b20d3991…8dd6`), CN mirror `gitcode.com/mcpp-res/mwrap-opus` with identical bytes. +- Implementation ownership: the libraries are compat.opus, compat.opusfile and compat.ogg (static libraries from the release + tarballs; their configuration and ABI are sufficient). The wrapper vendors no library code. It retains each upstream's + README and COPYING under `upstream/{opus,opusfile,ogg}/`, with checksums in `upstream/documents.json`, and deploys them with + its own LICENSE/NOTICE (`runtime.deploy`). Its conformance example compiles libopus's `src/opus_compare.c`, unchanged and + checked byte for byte against the tarball. + +## Modules + +- `mcpplibs.mwrap.opus`: libopus — 13 types and 87 functions of ``, ``, `` + (with `` and ``), by `export using`; re-exports the three modules below. +- `mcpplibs.mwrap.opus.file`: opusfile — 10 types and 54 functions of ``. Not exported: libopusurl's 9 + declarations (HTTP over OpenSSL; compat.opusfile builds libopusfile only) and its 16 macros. +- `mcpplibs.mwrap.opus.ogg`: libogg — 12 types and 71 functions of `` and ``. +- `mcpplibs.mwrap.opus.macros`: 115 integer macros as `inline constexpr int`; the 6 type macros of `` + (`opus_int`, `opus_uint`, and `opus_int8` / `opus_uint8` / `opus_int64` / `opus_uint64`, which the header leaves macros or + replaces with `` typedefs depending on the compiler) as type aliases; the 54 CTL macros as constexpr functions + returning a `ctl_request` of the request and the arguments the macro's checks require, with a `ctl_request` overload of + each of the 7 variadic `*_ctl` functions (found by argument-dependent lookup). +- Stay macros, in `` (``, then the headers, in the modules' order): attributes and keyword + spellings (`OPUS_EXPORT`, `OPUS_WARN_UNUSED_RESULT`, `OPUS_ARG_NONNULL`, `OPUS_RESTRICT`, `OPUS_INLINE`, `OP_*`), the + compiler-version tests, `OPUS_HAVE_OPUS_PROJECTION_H`, the 8 `opus_check_*` argument checks, libogg's `_ogg_*` allocator + names. Not mapped: 8 include guards. + +`tools/check_exports.py` fails when the generated sources are stale, a declaration is not exported, a macro is neither mapped +nor listed, or a variadic `*_ctl` function lacks its overload; every mapping is checked against the raw headers in a +header-mixing test, and every exported function's address is taken in a link test. + +## Verification + +Wrapper CI (Linux x86_64 GCC 16.1 and LLVM 23.1.3, Linux arm64 GCC 16.1, macOS LLVM, Windows LLVM, all required): build, +the five test programs, the export audit, and RFC 8251's decoder test vectors on `examples/opus` (programs written against +the module): 12 vectors × 5 output rates × mono/stereo = 120 decodes, the range decoder's final state checked against the +encoder's after every packet, each output passing libopus's own `opus_compare` against the reference (`.dec` or RFC 8251's +`m.dec`); 120 passed, 0 failed on every row. Then the Ogg Opus tool encodes, describes and decodes a 48 kHz stereo and a +16 kHz mono WAV, each decode exactly the input's length. The templates job scaffolds the default `tone` template as an +installed package and runs it. Until compat.opus is on this index's main, the wrapper's CI resolves the compat namespace +from the parent change's branch (`tools/candidate_index.py`, a no-op once published). + +Here, with the CI-pinned mcpp 2026.10.26.1: + +- `bash tests/run_members.sh opus-ogg`: the real release archive through the candidate index; a chord encoded through the + typed CTL requests, paged with libogg, read back with opusfile from memory (headers, tags, the exact length, seeking), + a 5.1 multistream encoder's per-stream state, and the side header beside the import: `test result ok`. +- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.opus@1.6.1 --deploy`: the unmodified template + from the real archive (38.6 dB SNR, 96000 samples in and out), then a relocated self-contained bundle whose + `licenses/opus/` holds the wrapper's LICENSE/NOTICE and the three libraries' READMEs and COPYINGs byte for byte. + +## Motivation + +openmui's coming audio engine (media.audio) decodes Opus; libopus, opusfile and libogg are BSD-3-Clause and link +statically under the project's licence policy. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index a6d65157..d0a6d50f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1638,6 +1638,11 @@ jobs: shell: bash run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.sheenbidi@3.0.0 --deploy + - name: Installed Opus wrapper archive, actual default tone template and its relocated BSD notices + if: contains(format(' {0} ', env.MEMBERS), ' opus-ogg ') && matrix.toolchain == 'default' + shell: bash + run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.opus@1.6.1 --deploy + - name: Installed Box2D wrapper archive, actual default physics template and its relocated notices if: contains(format(' {0} ', env.MEMBERS), ' box2d-physics ') && matrix.toolchain == 'default' shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c9b3257..81a5b50c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ ### Added +- **mcpplibs.mwrap.opus 1.6.1(新包)。** opus-m:以 C++23 模块(`import mcpplibs.mwrap.opus;`)使用 compat.opus 1.6.1、 + compat.opusfile 0.12 与 compat.ogg 1.3.6。`mcpplibs.mwrap.opus` 导出 libopus 的 13 个类型与 87 个函数,并重新导出 + `.file`(opusfile:10 个类型、54 个函数;不导出未构建的 libopusurl 的 9 个声明)、`.ogg`(libogg:12 个类型、71 个函数) + 与 `.macros`(115 个整数常量、6 个类型别名、54 个 CTL 宏映射为返回带类型 `ctl_request` 的 constexpr 函数,7 个 `*_ctl` + 函数各有接受它的重载,`opus_encoder_ctl(enc, OPUS_SET_BITRATE(64000))` 写法不变);属性、版本测试与 CTL 参数检查等 + 22 个宏留在 ``。包装仓库的 CI(Linux x86_64 GCC/LLVM、Linux arm64、macOS、Windows)经由模块 + 解码 RFC 8251 的 12 个测试向量(5 种采样率、单声道与立体声,共 120 次,逐包核对区间解码器最终状态,并以 libopus 的 + `opus_compare` 对照参考输出),0 失败;212 个导出函数全部可链接。GLOBAL 为 `mwrap-1.6.1` 发布资产,CN 为 GitCode + `mcpp-res/mwrap-opus` 镜像,sha256 一致。新增测试成员 `tests/mwrap/opus-ogg` 与已安装模板及其许可文件的部署检查。 - **compat.ogg 1.3.6、compat.opus 1.6.1、compat.opusfile 0.12(新包)。** Opus 音频的三个 Xiph.Org 库,均为 BSD-3-Clause,均以上游发布 tarball 从源码构建为静态库。compat.ogg:Ogg 容器的分页与位打包(`src/framing.c`、 `src/bitwise.c`),`` 按上游 CMake 的结果由 `generated_files` 生成(int16_t…uint64_t)。 diff --git a/mcpp.toml b/mcpp.toml index d389bcde..ecab4106 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -23,6 +23,7 @@ members = [ "tests/mwrap/concurrentqueue-mpmc", "tests/mwrap/fribidi-bidi", "tests/mwrap/sheenbidi-bidi", + "tests/mwrap/opus-ogg", "tests/mwrap/box2d-physics", "tests/mwrap/miniaudio-engine", "tests/mwrap/freetype-render", diff --git a/pkgs/mwrap/opus.lua b/pkgs/mwrap/opus.lua new file mode 100644 index 00000000..4a734ea1 --- /dev/null +++ b/pkgs/mwrap/opus.lua @@ -0,0 +1,52 @@ +-- Immutable upstream-aligned mwrap source release (opus-m mwrap-1.6.1); +-- GLOBAL and CN bytes are identical. +-- +-- The wrapper is Apache-2.0. The libraries it links are BSD-3-Clause, linked +-- statically: compat.opus (libopus 1.6.1), compat.opusfile (opusfile 0.12) and +-- compat.ogg (libogg 1.3.6). The package deploys each one's README and COPYING +-- (libopus's with its royalty-free patent license references) beside programs +-- (runtime.deploy). +package = { + spec = "1", + namespace = "mcpplibs.mwrap", + name = "opus", + description = "opus-m: Opus audio as C++23 modules — the libopus 1.6.1 codec, the opusfile 0.12 Ogg Opus reader and the libogg 1.3.6 container", + licenses = { "Apache-2.0" }, + repo = "https://github.com/mcpplibs/opus-m", + type = "package", + extensions = { + autoupdate = { + tag = "mwrap-${version}", + url = "https://github.com/mcpplibs/opus-m/releases/download/${tag}/opus-m-${version}.tar.gz", + }, + }, + xpm = { + linux = { + ["1.6.1"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/opus-m/releases/download/mwrap-1.6.1/opus-m-1.6.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-opus/releases/download/1.6.1/opus-m-1.6.1.tar.gz", + }, + sha256 = "b20d3991aea134f0dceb090f60b700b36c99a94e70b8236304fddd0d53718dd6", + }, + }, + macosx = { + ["1.6.1"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/opus-m/releases/download/mwrap-1.6.1/opus-m-1.6.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-opus/releases/download/1.6.1/opus-m-1.6.1.tar.gz", + }, + sha256 = "b20d3991aea134f0dceb090f60b700b36c99a94e70b8236304fddd0d53718dd6", + }, + }, + windows = { + ["1.6.1"] = { + url = { + GLOBAL = "https://github.com/mcpplibs/opus-m/releases/download/mwrap-1.6.1/opus-m-1.6.1.tar.gz", + CN = "https://gitcode.com/mcpp-res/mwrap-opus/releases/download/1.6.1/opus-m-1.6.1.tar.gz", + }, + sha256 = "b20d3991aea134f0dceb090f60b700b36c99a94e70b8236304fddd0d53718dd6", + }, + }, + }, +} diff --git a/tests/mwrap/check_installed_template.py b/tests/mwrap/check_installed_template.py index eb9d57c3..8585600b 100644 --- a/tests/mwrap/check_installed_template.py +++ b/tests/mwrap/check_installed_template.py @@ -183,6 +183,30 @@ def main(): notice = (roots[0] / "NOTICE").read_bytes() assert b"mcpp-community" in notice and b"Muhammad Tayyab Akram" in notice print(f"Verified {len(declared)} byte-identical wrapper/SheenBidi license files", flush=True) + elif identity == "mcpplibs.mwrap.opus": + # libopus, opusfile and libogg are BSD-3-Clause and linked + # statically: each one's README and COPYING (libopus's with its + # patent license references) travel with the program beside the + # wrapper's LICENSE and NOTICE. + sources = [path for store in (home / "xpkgs", home / "registry/data/xpkgs") + for path in store.glob(f"mcpplibs.mwrap-x-opus/{version}/*/src/opus.cppm")] + assert len(sources) == 1, sources + source = sources[0].parent.parent + declared = tomllib.loads((source / "mcpp.toml").read_text(encoding="utf-8"))["runtime"]["deploy"] + roots = list(relocated.rglob("licenses/opus")) + assert len(roots) == 1, f"Missing Opus runtime license tree: {roots}" + for entry in declared: + notice = source / entry["from"] + copied = roots[0].parent.parent / entry["to"] / notice.name + assert copied.is_file() and copied.read_bytes() == notice.read_bytes(), copied + for library in ("opus", "opusfile", "ogg"): + assert b"Redistribution and use in source and binary forms" in ( + roots[0] / "upstream" / library / "COPYING").read_bytes(), library + assert b"royalty-free patent licenses" in (roots[0] / "upstream/opus/COPYING").read_bytes() + assert b"Apache License" in (roots[0] / "LICENSE").read_bytes() + notice = (roots[0] / "NOTICE").read_bytes() + assert b"mcpp-community" in notice and b"Xiph.Org" in notice + print(f"Verified {len(declared)} byte-identical wrapper/libopus/opusfile/libogg license files", flush=True) elif identity == "mcpplibs.mwrap.box2d": # Box2D is MIT and linked statically: its LICENSE and README # travel with the program beside the wrapper's LICENSE/NOTICE. diff --git a/tests/mwrap/opus-ogg/mcpp.toml b/tests/mwrap/opus-ogg/mcpp.toml new file mode 100644 index 00000000..e032afaa --- /dev/null +++ b/tests/mwrap/opus-ogg/mcpp.toml @@ -0,0 +1,11 @@ +[package] +name = "opus-ogg" +version = "1.6.1" +description = "Consume the immutable Opus wrapper: a chord encoded with libopus, paged with libogg and read back with opusfile from memory (headers, tags, exact length, seeking), the typed CTL requests, a 5.1 multistream frame, the side header beside the import" + +[indices] +# Use the complete candidate built-in index prepared by tests/run_members.sh. +# An explicit empty table overrides the legacy workspace redirect. + +[dependencies] +"mcpplibs.mwrap.opus" = "1.6.1" diff --git a/tests/mwrap/opus-ogg/tests/behavior.cpp b/tests/mwrap/opus-ogg/tests/behavior.cpp new file mode 100644 index 00000000..c25100b0 --- /dev/null +++ b/tests/mwrap/opus-ogg/tests/behavior.cpp @@ -0,0 +1,93 @@ +import std; +import mcpplibs.mwrap.opus; + +using namespace mcpplibs::mwrap::opus::macros; + +namespace { + +void put(std::vector& v, std::uint32_t x, int n) { + for (int i = 0; i < n; ++i) v.push_back((x >> (8 * i)) & 0xFF); +} +void append(std::vector& file, const ogg_page& page) { + file.insert(file.end(), page.header, page.header + page.header_len); + file.insert(file.end(), page.body, page.body + page.body_len); +} + +} // namespace + +int main() { + if (std::string_view(opus_get_version_string()) != "libopus 1.6.1") return 1; + + // A chord, encoded at 64 kb/s through the typed CTL requests. + constexpr int rate = 48000, ch = 2, frame = 960, samples = 48000 + 77; + int err = 0; + OpusEncoder* enc = opus_encoder_create(rate, ch, OPUS_APPLICATION_AUDIO, &err); + if (err != OPUS_OK || opus_encoder_ctl(enc, OPUS_SET_BITRATE(64000)) != OPUS_OK) return 2; + opus_int32 bitrate = 0, preskip = 0; + opus_encoder_ctl(enc, OPUS_GET_BITRATE(&bitrate)); + opus_encoder_ctl(enc, OPUS_GET_LOOKAHEAD(&preskip)); + if (bitrate != 64000 || preskip <= 0) return 3; + + // An Ogg Opus stream (RFC 7845), paged with libogg. + std::vector file; + ogg_stream_state os; + ogg_stream_init(&os, 7); + ogg_page page; + std::vector head = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd', 1, ch}; + put(head, static_cast(preskip), 2); + put(head, rate, 4); + put(head, 0, 3); + ogg_packet op{head.data(), static_cast(head.size()), 1, 0, 0, 0}; + ogg_stream_packetin(&os, &op); + while (ogg_stream_flush(&os, &page)) append(file, page); + std::vector tags = {'O', 'p', 'u', 's', 'T', 'a', 'g', 's', 2, 0, 0, 0, 'm', 'w', 1, 0, 0, 0, 7, 0, 0, 0}; + for (char c : std::string_view("TITLE=x")) tags.push_back(static_cast(c)); + op = ogg_packet{tags.data(), static_cast(tags.size()), 0, 0, 0, 1}; + ogg_stream_packetin(&os, &op); + while (ogg_stream_flush(&os, &page)) append(file, page); + const int total = samples + preskip, frames = (total + frame - 1) / frame; + std::vector pcm(static_cast(frame) * ch); + unsigned char packet[4000]; + for (int f = 0; f < frames; ++f) { + for (int i = 0; i < frame; ++i) + for (int c = 0; c < ch; ++c) + pcm[static_cast(i) * ch + c] = + static_cast(0.3 * std::sin(2 * std::numbers::pi * (440 + 220 * c) * (f * frame + i) / rate)); + const int n = opus_encode_float(enc, pcm.data(), frame, packet, sizeof packet); + if (n <= 0) return 4; + const bool last = f == frames - 1; + op = ogg_packet{packet, n, 0, last ? 1 : 0, last ? total : static_cast(f + 1) * frame, 2 + f}; + ogg_stream_packetin(&os, &op); + while (ogg_stream_pageout(&os, &page)) append(file, page); + } + while (ogg_stream_flush(&os, &page)) append(file, page); + ogg_stream_clear(&os); + opus_encoder_destroy(enc); + + // Read back with opusfile: headers, tags, the exact length, seeking. + OggOpusFile* of = op_open_memory(file.data(), file.size(), &err); + if (!of || err != 0) return 5; + if (op_channel_count(of, -1) != 2 || op_head(of, -1)->pre_skip != static_cast(preskip)) return 6; + if (std::string_view(op_tags(of, -1)->vendor) != "mw" || std::string_view(opus_tags_query(op_tags(of, -1), "title", 0)) != "x") return 7; + if (op_pcm_total(of, -1) != samples) return 8; + std::vector out(5760 * 2); + long read = 0; + for (int n; (n = op_read_float(of, out.data(), static_cast(out.size()), nullptr)) > 0;) read += n; + if (read != samples) return 9; + if (op_pcm_seek(of, 12345) != 0 || op_pcm_tell(of) != 12345) return 10; + op_free(of); + if (op_open_memory(file.data(), 16, &err) != nullptr || err == 0) return 11; + + // 5.1 surround: four streams, two coupled. + int streams = 0, coupled = 0; + unsigned char mapping[6]; + OpusMSEncoder* ms = opus_multistream_surround_encoder_create(rate, 6, 1, &streams, &coupled, mapping, OPUS_APPLICATION_AUDIO, &err); + if (err != OPUS_OK || streams != 4 || coupled != 2) return 12; + OpusEncoder* first = nullptr; + if (opus_multistream_encoder_ctl(ms, OPUS_MULTISTREAM_GET_ENCODER_STATE(0, &first)) != OPUS_OK || !first) return 13; + opus_multistream_encoder_destroy(ms); + + static_assert(OPUS_SET_BITRATE(OPUS_AUTO).request == OPUS_SET_BITRATE_REQUEST && OP_EOF == -2); + static_assert(std::is_same_v>); + return 0; +} diff --git a/tests/mwrap/opus-ogg/tests/side_header.cpp b/tests/mwrap/opus-ogg/tests/side_header.cpp new file mode 100644 index 00000000..3808c2e2 --- /dev/null +++ b/tests/mwrap/opus-ogg/tests/side_header.cpp @@ -0,0 +1,31 @@ +// The side header from the installed package, beside the import: the raw +// headers' macros (CTL comma lists, constants, attributes) with the module's +// entities and mappings. +#include + +import std; +import mcpplibs.mwrap.opus; + +extern "C" OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int mwrap_opus_consumer(int value); +extern "C" int mwrap_opus_consumer(int value) { return value * 2; } + +int main() { + namespace m = mcpplibs::mwrap::opus::macros; + int err = 0; + OpusDecoder* dec = opus_decoder_create(48000, 2, &err); + if (err != OPUS_OK) return 1; + // The header's comma list and the mapped request reach the same decoder. + if (opus_decoder_ctl(dec, OPUS_SET_GAIN(512)) != OPUS_OK) return 2; + opus_int32 gain = 0; + if (opus_decoder_ctl(dec, (m::OPUS_GET_GAIN)(&gain)) != OPUS_OK || gain != 512) return 3; + opus_decoder_destroy(dec); + if (&::op_open_memory != &op_open_memory) return 4; + // opus_int64 is a macro or a typedef, depending on the compiler: + // set the macro aside to name the module's alias. + using raw_int64 = opus_int64; +#pragma push_macro("opus_int64") +#undef opus_int64 + static_assert(std::is_same_v); +#pragma pop_macro("opus_int64") + return mwrap_opus_consumer(21) == 42 ? 0 : 5; +}