diff options
author | Daniil Cherednik <dan.cherednik@gmail.com> | 2025-05-31 22:39:38 +0200 |
---|---|---|
committer | Daniil Cherednik <dan.cherednik@gmail.com> | 2025-05-31 22:39:38 +0200 |
commit | 1c7f2f821fb965af468cdf2a14df3ff75cc1c352 (patch) | |
tree | 1bc92237122b75c67afc326af207cf3cc9eb3d6c /src/lib | |
parent | 272af27a3d148bd13e8f15640e53ca70c64ccb9b (diff) | |
parent | 6dfc60e9d4791c3385908c61ad75c4a0093ea1eb (diff) | |
download | atracdenc-1c7f2f821fb965af468cdf2a14df3ff75cc1c352.tar.gz |
Merge branch 'at3plus-dev'
It looks like we are able to encode ATRAC3PLUS compatible bitstream so we can merge at3p development branch in to the main branch.
Current limitation for AT3P mode:
- Only 352 Kbps (proper bit allocation and some psychoacoustic must be implemented)
- GHA sometime works with error (but huge bitrate hide it)
- No VLC, VQ, delta encoding
- No noise substitution
- No gain control
- No window shape switching
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/bs_encode/encode.cpp | 184 | ||||
-rw-r--r-- | src/lib/bs_encode/encode.h | 71 | ||||
-rw-r--r-- | src/lib/bs_encode/encode_ut.cpp | 178 | ||||
-rw-r--r-- | src/lib/fft/kissfft_impl/kiss_fft.h | 1 | ||||
-rw-r--r-- | src/lib/fft/kissfft_impl/tools/kiss_fftr.c | 153 | ||||
-rw-r--r-- | src/lib/fft/kissfft_impl/tools/kiss_fftr.h | 54 | ||||
m--------- | src/lib/libgha | 0 |
7 files changed, 640 insertions, 1 deletions
diff --git a/src/lib/bs_encode/encode.cpp b/src/lib/bs_encode/encode.cpp new file mode 100644 index 0000000..230ae0f --- /dev/null +++ b/src/lib/bs_encode/encode.cpp @@ -0,0 +1,184 @@ +/* + * This file is part of AtracDEnc. + * + * AtracDEnc is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * AtracDEnc is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with AtracDEnc; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "encode.h" + +#include <iostream> + +namespace NAtracDEnc { + +using NBitStream::TBitStream; + +class TBitStreamEncoder::TImpl : public TBitAllocHandler { +public: + explicit TImpl(std::vector<IBitStreamPartEncoder::TPtr>&& encoders); + void DoStart(size_t targetBits, float minLambda, float maxLambda) noexcept; + float DoContinue() noexcept; + void DoSubmit(size_t gotBits) noexcept; + bool DoCheck(size_t gotBits) const noexcept; + void DoRun(void* frameData, TBitStream& bs); + uint32_t DoGetCurGlobalConsumption() const noexcept; +private: + std::vector<IBitStreamPartEncoder::TPtr> Encoders; + size_t CurEncPos; + size_t RepeatEncPos; + + size_t TargetBits; + float MinLambda; + float MaxLambda; + float CurLambda; + float LastLambda; + + bool NeedRepeat = false; +}; + +TBitStreamEncoder::TImpl::TImpl(std::vector<IBitStreamPartEncoder::TPtr>&& encoders) + : Encoders(std::move(encoders)) + , CurEncPos(0) + , RepeatEncPos(0) +{ +} + +void TBitStreamEncoder::TImpl::DoStart(size_t targetBits, float minLambda, float maxLambda) noexcept +{ + TargetBits = targetBits; + MinLambda = minLambda; + MaxLambda = maxLambda; +} + +float TBitStreamEncoder::TImpl::DoContinue() noexcept +{ + if (MaxLambda <= MinLambda) { + return LastLambda; + } + + CurLambda = (MaxLambda + MinLambda) / 2.0; + RepeatEncPos = CurEncPos; + return CurLambda; +} + +void TBitStreamEncoder::TImpl::DoSubmit(size_t gotBits) noexcept +{ + if (MaxLambda <= MinLambda) { + NeedRepeat = false; + } else { + if (gotBits < TargetBits) { + LastLambda = CurLambda; + MaxLambda = CurLambda - 0.01f; + NeedRepeat = true; + } else if (gotBits > TargetBits) { + MinLambda = CurLambda + 0.01f; + NeedRepeat = true; + } else { + NeedRepeat = false; + } + } +} + +bool TBitStreamEncoder::TImpl::DoCheck(size_t gotBits) const noexcept +{ + return gotBits < TargetBits; +} + +void TBitStreamEncoder::TImpl::DoRun(void* frameData, TBitStream& bs) +{ + bool cont = false; + do { + for (CurEncPos = RepeatEncPos; CurEncPos < Encoders.size(); CurEncPos++) { + auto status = Encoders[CurEncPos]->Encode(frameData, *this); + if (NeedRepeat) { + NeedRepeat = false; + cont = true; + break; + } else { + cont = false; + } + if (status == IBitStreamPartEncoder::EStatus::Repeat) { + cont = true; + for (size_t i = 0; i < CurEncPos; i++) { + Encoders[i]->Reset(); + } + RepeatEncPos = 0; + break; + } + } + } while (cont); + + for (size_t i = 0; i < Encoders.size(); i++) { + Encoders[i]->Dump(bs); + } +} + +uint32_t TBitStreamEncoder::TImpl::DoGetCurGlobalConsumption() const noexcept +{ + uint32_t consumption = 0; + for (size_t i = 0; i < CurEncPos; i++) { + consumption += Encoders[i]->GetConsumption(); + } + return consumption; +} + +///// + +TBitStreamEncoder::TBitStreamEncoder(std::vector<IBitStreamPartEncoder::TPtr>&& encoders) + : Impl(new TBitStreamEncoder::TImpl(std::move(encoders))) +{} + +TBitStreamEncoder::~TBitStreamEncoder() +{ + delete Impl; +} + +void TBitStreamEncoder::Do(void* frameData, TBitStream& bs) +{ + Impl->DoRun(frameData, bs); +} + +///// + +void TBitAllocHandler::Start(size_t targetBits, float minLambda, float maxLambda) noexcept +{ + TBitStreamEncoder::TImpl* self = static_cast<TBitStreamEncoder::TImpl*>(this); + self->DoStart(targetBits, minLambda, maxLambda); +} + +float TBitAllocHandler::Continue() noexcept +{ + TBitStreamEncoder::TImpl* self = static_cast<TBitStreamEncoder::TImpl*>(this); + return self->DoContinue(); +} + +void TBitAllocHandler::Submit(size_t gotBits) noexcept +{ + TBitStreamEncoder::TImpl* self = static_cast<TBitStreamEncoder::TImpl*>(this); + self->DoSubmit(gotBits); +} + +bool TBitAllocHandler::Check(size_t gotBits) const noexcept +{ + const TBitStreamEncoder::TImpl* self = static_cast<const TBitStreamEncoder::TImpl*>(this); + return self->DoCheck(gotBits); +} + +uint32_t TBitAllocHandler::GetCurGlobalConsumption() const noexcept +{ + const TBitStreamEncoder::TImpl* self = static_cast<const TBitStreamEncoder::TImpl*>(this); + return self->DoGetCurGlobalConsumption(); +} + +} diff --git a/src/lib/bs_encode/encode.h b/src/lib/bs_encode/encode.h new file mode 100644 index 0000000..3302d10 --- /dev/null +++ b/src/lib/bs_encode/encode.h @@ -0,0 +1,71 @@ +/* + * This file is part of AtracDEnc. + * + * AtracDEnc is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * AtracDEnc is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with AtracDEnc; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include <memory> +#include <vector> +#include <functional> + +namespace NBitStream { +class TBitStream; +} + +namespace NAtracDEnc { + +class TBitAllocHandler { +public: + void Start(size_t targetBits, float minLambda, float maxLambda) noexcept; + float Continue() noexcept; + bool Check(size_t gitBits) const noexcept; + void Submit(size_t gotBits) noexcept; + + // Returns consumption of all previous encoded parts (except part from this method called) + uint32_t GetCurGlobalConsumption() const noexcept; +}; + +class IBitStreamPartEncoder { +public: + using TPtr = std::unique_ptr<IBitStreamPartEncoder>; + enum class EStatus { + Ok, // Ok, go to the next stage + Repeat, // Repeat from first stage + }; + + virtual ~IBitStreamPartEncoder() = default; + virtual EStatus Encode(void* frameData, TBitAllocHandler& ba) = 0; + virtual void Dump(NBitStream::TBitStream& bs) = 0; + virtual void Reset() noexcept {}; + virtual uint32_t GetConsumption() const noexcept = 0; +}; + +class TBitStreamEncoder { +public: + class TImpl; + explicit TBitStreamEncoder(std::vector<IBitStreamPartEncoder::TPtr>&& encoders); + ~TBitStreamEncoder(); + + void Do(void* frameData, NBitStream::TBitStream& bs); + TBitStreamEncoder(const TBitStreamEncoder&) = delete; + TBitStreamEncoder& operator=(const TBitStreamEncoder&) = delete; +private: + std::vector<IBitStreamPartEncoder::TPtr> Encoders; + TImpl* Impl; +}; + +} diff --git a/src/lib/bs_encode/encode_ut.cpp b/src/lib/bs_encode/encode_ut.cpp new file mode 100644 index 0000000..39f0ff1 --- /dev/null +++ b/src/lib/bs_encode/encode_ut.cpp @@ -0,0 +1,178 @@ +/* + * This file is part of AtracDEnc. + * + * AtracDEnc is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * AtracDEnc is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with AtracDEnc; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "encode.h" +#include <gtest/gtest.h> +#include <cmath> +#include <memory> +#include <bitstream/bitstream.h> + +using namespace NAtracDEnc; + +static size_t SomeBitFn1(float lambda) { + return sqrt(lambda * (-1.0f)) * 300; +} + +static size_t SomeBitFn2(float lambda) { + return 1 + (SomeBitFn1(lambda) & (~(size_t)7)); +} + +class TPartEncoder1 : public IBitStreamPartEncoder { +public: + explicit TPartEncoder1(size_t expCalls) + : ExpCalls(expCalls) + {} + + EStatus Encode(void* frameData, TBitAllocHandler& ba) override { + EncCalls++; + ba.Start(1000, -15, -1); + return EStatus::Ok; + } + + void Dump(NBitStream::TBitStream& bs) override { + EXPECT_EQ(EncCalls, ExpCalls); + } + + uint32_t GetConsumption() const noexcept override { + return 0; + } +private: + const size_t ExpCalls; + size_t EncCalls = 0; +}; + +template<size_t (*F)(float)> +class TPartEncoder2 : public IBitStreamPartEncoder { +public: + explicit TPartEncoder2(size_t expCalls) + : ExpCalls(expCalls) + {} + + EStatus Encode(void* frameData, TBitAllocHandler& ba) override { + EncCalls++; + auto lambda = ba.Continue(); + auto bits = F(lambda); + ba.Submit(bits); + Bits = bits; + return EStatus::Ok; + } + + void Dump(NBitStream::TBitStream& bs) override { + EXPECT_EQ(EncCalls, ExpCalls); + for (size_t i = 0; i < Bits; i++) { + bs.Write(1, 1); + } + } + + uint32_t GetConsumption() const noexcept override { + return 1 * Bits; + } +private: + const size_t ExpCalls; + size_t EncCalls = 0; + size_t Bits = 0; +}; + +class TPartEncoder3 : public IBitStreamPartEncoder { +public: + explicit TPartEncoder3(size_t expCalls) + : ExpCalls(expCalls) + {} + + EStatus Encode(void* frameData, TBitAllocHandler& ba) override { + EncCalls++; + return EStatus::Ok; + } + + void Dump(NBitStream::TBitStream& bs) override { + EXPECT_EQ(EncCalls, ExpCalls); + } + + uint32_t GetConsumption() const noexcept override { + return 0; + } +private: + const size_t ExpCalls; + size_t EncCalls = 0; +}; + +class TPartEncoder4 : public IBitStreamPartEncoder { +public: + TPartEncoder4() = default; + + EStatus Encode(void* frameData, TBitAllocHandler& ba) override { + if (EncCalls == 0) { + EncCalls++; + return EStatus::Repeat; + } + + return EStatus::Ok; + } + + void Dump(NBitStream::TBitStream& bs) override { + EXPECT_EQ(EncCalls, 1); + } + + uint32_t GetConsumption() const noexcept override { + return 0; + } +private: + size_t EncCalls = 0; +}; + +TEST(BsEncode, SimpleAlloc) { + std::vector<IBitStreamPartEncoder::TPtr> encoders; + encoders.emplace_back(std::make_unique<TPartEncoder1>(1)); + encoders.emplace_back(std::make_unique<TPartEncoder2<SomeBitFn1>>(8)); + encoders.emplace_back(std::make_unique<TPartEncoder3>(1)); + + NBitStream::TBitStream bs; + TBitStreamEncoder encoder(std::move(encoders)); + encoder.Do(nullptr, bs); + + EXPECT_EQ(bs.GetSizeInBits(), 1000); +} + +TEST(BsEncode, AllocWithRepeat) { + std::vector<IBitStreamPartEncoder::TPtr> encoders; + encoders.emplace_back(std::make_unique<TPartEncoder1>(2)); + encoders.emplace_back(std::make_unique<TPartEncoder2<SomeBitFn1>>(16)); + encoders.emplace_back(std::make_unique<TPartEncoder3>(2)); + encoders.emplace_back(std::make_unique<TPartEncoder4>()); + + NBitStream::TBitStream bs; + TBitStreamEncoder encoder(std::move(encoders)); + encoder.Do(nullptr, bs); + + EXPECT_EQ(bs.GetSizeInBits(), 1000); +} + +TEST(BsEncode, NotExactAlloc) { + std::vector<IBitStreamPartEncoder::TPtr> encoders; + encoders.emplace_back(std::make_unique<TPartEncoder1>(1)); + encoders.emplace_back(std::make_unique<TPartEncoder2<SomeBitFn2>>(11)); + encoders.emplace_back(std::make_unique<TPartEncoder3>(1)); + + NBitStream::TBitStream bs; + TBitStreamEncoder encoder(std::move(encoders)); + encoder.Do(nullptr, bs); + + EXPECT_EQ(bs.GetSizeInBits(), 993); +} + + diff --git a/src/lib/fft/kissfft_impl/kiss_fft.h b/src/lib/fft/kissfft_impl/kiss_fft.h index 7786eb6..64c50f4 100644 --- a/src/lib/fft/kissfft_impl/kiss_fft.h +++ b/src/lib/fft/kissfft_impl/kiss_fft.h @@ -44,7 +44,6 @@ extern "C" { #else # ifndef kiss_fft_scalar /* default is float */ -# error "wrong type" # define kiss_fft_scalar float # endif #endif diff --git a/src/lib/fft/kissfft_impl/tools/kiss_fftr.c b/src/lib/fft/kissfft_impl/tools/kiss_fftr.c new file mode 100644 index 0000000..8102132 --- /dev/null +++ b/src/lib/fft/kissfft_impl/tools/kiss_fftr.c @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved. + * This file is part of KISS FFT - https://github.com/mborgerding/kissfft + * + * SPDX-License-Identifier: BSD-3-Clause + * See COPYING file for more information. + */ + +#include "kiss_fftr.h" +#include "_kiss_fft_guts.h" + +struct kiss_fftr_state{ + kiss_fft_cfg substate; + kiss_fft_cpx * tmpbuf; + kiss_fft_cpx * super_twiddles; +#ifdef USE_SIMD + void * pad; +#endif +}; + +kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem) +{ + int i; + kiss_fftr_cfg st = NULL; + size_t subsize = 0, memneeded; + + if (nfft & 1) { + fprintf(stderr,"Real FFT optimization must be even.\n"); + return NULL; + } + nfft >>= 1; + + kiss_fft_alloc (nfft, inverse_fft, NULL, &subsize); + memneeded = sizeof(struct kiss_fftr_state) + subsize + sizeof(kiss_fft_cpx) * ( nfft * 3 / 2); + + if (lenmem == NULL) { + st = (kiss_fftr_cfg) KISS_FFT_MALLOC (memneeded); + } else { + if (*lenmem >= memneeded) + st = (kiss_fftr_cfg) mem; + *lenmem = memneeded; + } + if (!st) + return NULL; + + st->substate = (kiss_fft_cfg) (st + 1); /*just beyond kiss_fftr_state struct */ + st->tmpbuf = (kiss_fft_cpx *) (((char *) st->substate) + subsize); + st->super_twiddles = st->tmpbuf + nfft; + kiss_fft_alloc(nfft, inverse_fft, st->substate, &subsize); + + for (i = 0; i < nfft/2; ++i) { + double phase = + -3.14159265358979323846264338327 * ((double) (i+1) / nfft + .5); + if (inverse_fft) + phase *= -1; + kf_cexp (st->super_twiddles+i,phase); + } + return st; +} + +void kiss_fftr(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata) +{ + /* input buffer timedata is stored row-wise */ + int k,ncfft; + kiss_fft_cpx fpnk,fpk,f1k,f2k,tw,tdc; + + if ( st->substate->inverse) { + fprintf(stderr,"kiss fft usage error: improper alloc\n"); + exit(1); + } + + ncfft = st->substate->nfft; + + /*perform the parallel fft of two real signals packed in real,imag*/ + kiss_fft( st->substate , (const kiss_fft_cpx*)timedata, st->tmpbuf ); + /* The real part of the DC element of the frequency spectrum in st->tmpbuf + * contains the sum of the even-numbered elements of the input time sequence + * The imag part is the sum of the odd-numbered elements + * + * The sum of tdc.r and tdc.i is the sum of the input time sequence. + * yielding DC of input time sequence + * The difference of tdc.r - tdc.i is the sum of the input (dot product) [1,-1,1,-1... + * yielding Nyquist bin of input time sequence + */ + + tdc.r = st->tmpbuf[0].r; + tdc.i = st->tmpbuf[0].i; + C_FIXDIV(tdc,2); + CHECK_OVERFLOW_OP(tdc.r ,+, tdc.i); + CHECK_OVERFLOW_OP(tdc.r ,-, tdc.i); + freqdata[0].r = tdc.r + tdc.i; + freqdata[ncfft].r = tdc.r - tdc.i; +#ifdef USE_SIMD + freqdata[ncfft].i = freqdata[0].i = _mm_set1_ps(0); +#else + freqdata[ncfft].i = freqdata[0].i = 0; +#endif + + for ( k=1;k <= ncfft/2 ; ++k ) { + fpk = st->tmpbuf[k]; + fpnk.r = st->tmpbuf[ncfft-k].r; + fpnk.i = - st->tmpbuf[ncfft-k].i; + C_FIXDIV(fpk,2); + C_FIXDIV(fpnk,2); + + C_ADD( f1k, fpk , fpnk ); + C_SUB( f2k, fpk , fpnk ); + C_MUL( tw , f2k , st->super_twiddles[k-1]); + + freqdata[k].r = HALF_OF(f1k.r + tw.r); + freqdata[k].i = HALF_OF(f1k.i + tw.i); + freqdata[ncfft-k].r = HALF_OF(f1k.r - tw.r); + freqdata[ncfft-k].i = HALF_OF(tw.i - f1k.i); + } +} + +void kiss_fftri(kiss_fftr_cfg st,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata) +{ + /* input buffer timedata is stored row-wise */ + int k, ncfft; + + if (st->substate->inverse == 0) { + fprintf (stderr, "kiss fft usage error: improper alloc\n"); + exit (1); + } + + ncfft = st->substate->nfft; + + st->tmpbuf[0].r = freqdata[0].r + freqdata[ncfft].r; + st->tmpbuf[0].i = freqdata[0].r - freqdata[ncfft].r; + C_FIXDIV(st->tmpbuf[0],2); + + for (k = 1; k <= ncfft / 2; ++k) { + kiss_fft_cpx fk, fnkc, fek, fok, tmp; + fk = freqdata[k]; + fnkc.r = freqdata[ncfft - k].r; + fnkc.i = -freqdata[ncfft - k].i; + C_FIXDIV( fk , 2 ); + C_FIXDIV( fnkc , 2 ); + + C_ADD (fek, fk, fnkc); + C_SUB (tmp, fk, fnkc); + C_MUL (fok, tmp, st->super_twiddles[k-1]); + C_ADD (st->tmpbuf[k], fek, fok); + C_SUB (st->tmpbuf[ncfft - k], fek, fok); +#ifdef USE_SIMD + st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0); +#else + st->tmpbuf[ncfft - k].i *= -1; +#endif + } + kiss_fft (st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata); +} diff --git a/src/lib/fft/kissfft_impl/tools/kiss_fftr.h b/src/lib/fft/kissfft_impl/tools/kiss_fftr.h new file mode 100644 index 0000000..588948d --- /dev/null +++ b/src/lib/fft/kissfft_impl/tools/kiss_fftr.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved. + * This file is part of KISS FFT - https://github.com/mborgerding/kissfft + * + * SPDX-License-Identifier: BSD-3-Clause + * See COPYING file for more information. + */ + +#ifndef KISS_FTR_H +#define KISS_FTR_H + +#include "kiss_fft.h" +#ifdef __cplusplus +extern "C" { +#endif + + +/* + + Real optimized version can save about 45% cpu time vs. complex fft of a real seq. + + + + */ + +typedef struct kiss_fftr_state *kiss_fftr_cfg; + + +kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem, size_t * lenmem); +/* + nfft must be even + + If you don't care to allocate space, use mem = lenmem = NULL +*/ + + +void kiss_fftr(kiss_fftr_cfg cfg,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata); +/* + input timedata has nfft scalar points + output freqdata has nfft/2+1 complex points +*/ + +void kiss_fftri(kiss_fftr_cfg cfg,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata); +/* + input freqdata has nfft/2+1 complex points + output timedata has nfft scalar points +*/ + +#define kiss_fftr_free KISS_FFT_FREE + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/lib/libgha b/src/lib/libgha new file mode 160000 +Subproject 0c3e65863f31459c4be8a12051a8d6260cb1d30 |